3

I'm trying to encode a dependent map using a list of dependent tuples. Here is what I have that does not work:

  class DTuple[Key, ValueMap[_ <: Key]](val first: Key)(val second: ValueMap[first.type])
  
  type DKey = "Tag" | "Versions" | "Author"

  type DMapping[X <: DKey] = X match {
      case "Tag" => String
      case "Versions" => Array[String]
      case "Author" => String
    }
  
  def mkString(d: DTuple[DKey, DMapping]) = d.first match {
    case _: "Tag" => "#" + d.second
    case _: "Versions" => d.second.mkString(",")
    case _: "Author" => "@" + d.second
  }

All I get is

[error] -- [E008] Not Found Error: Main.scala:21:35
[error] 21 |    case _: "Versions" => d.second.mkString(",")
[error]    |                          ^^^^^^^^^^^^^^^^^
[error]    |      value mkString is not a member of Main.DMapping[(d.first : Main.DKey)]

I can't think of a good way to pattern match d.second so that its type depends on d.first. I could add .asInstanceOf[Array[String]] and .asInstanceOf[String] but that is not the goal here, I'm trying to type-check code.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Mikaël Mayer
  • 10,425
  • 6
  • 64
  • 101
  • I haven't really used dotty yet, but scala doesn't have a `.join()` method and intellij seems to think dotty doesn't either. I'm guessing you meant to use `.mkString()`? – Marth Oct 22 '20 at 22:04
  • 1
    There's also another problem: you declare the `DKey` type to have a `"Version"` member, but you use `"Versions"` (plural) in your code afterwards. It's seems a bit weird to me that dotty doesn't complain in the `type DMapping` definition though. – Marth Oct 22 '20 at 22:06
  • `implicitly[d.first.type =:= "Version"]` didn't work either. It looks like typeclasses are your best bet. – user Oct 22 '20 at 22:10
  • Thanks @Marth I just fixed the two things, error remains the same. – Mikaël Mayer Oct 22 '20 at 22:17
  • @user How would you encode simple dependent tuples in a typeclass ? I'm curious and very newbie. – Mikaël Mayer Oct 22 '20 at 22:18
  • 1
    Something like [this](https://scastie.scala-lang.org/paHmwu3BTtagEQVCjbKtWw)? It's very messy, though – user Oct 22 '20 at 22:45
  • @user that's very nice and clever ! I don't think it's messy. You can post it as a solution, I think I'll accept it. – Mikaël Mayer Oct 22 '20 at 23:43
  • Oh I see what's not working here. I wish I can use these tuple types in a List, and I'm not sure about the upper bound with the encoding you suggested. Let me try. – Mikaël Mayer Oct 22 '20 at 23:46
  • I just gave it a try, but adding K <: DKey makes the type appear until assign, meaning I cannot use a `List[DTuple[DKey, DMapping]]` with different keys, as it is my original intent :-( – Mikaël Mayer Oct 23 '20 at 00:00
  • Writing an upper bound `List[DTuple[DKey DKey, DMapping]]` does not help either, since it cannot find the implicit instances then. – Mikaël Mayer Oct 23 '20 at 00:00
  • 1
    If you want to do this for a list, you might want to try Shapeless, although just using `asInstanceOf` is likely easier – user Oct 23 '20 at 13:51

1 Answers1

2

There's probably a better and easier way to do this, but:

import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

// This is a case case solely for the unapply method, you could implement it on your own
case class DTuple[Key, ValueMap[_ <: Key]](first: Key)(val second: ValueMap[first.type])

type DKey = "Tag" | "Versions" | "Author" | "BuildTime"

type DMapping[X <: DKey] = X match {
  case "Tag" => String
  case "Versions" => Array[String]
  case "Author" => String
  case "BuildTime" => ZonedDateTime
}

// the DTuple("<value>") is used at runtime to check the string (DKey) value
// the DTuple["<value>", DMapping] type hint makes dotty see the `d` value as the correct type, hence infering the type of d.second too
def mkString(dt: DTuple[DKey, DMapping]): String = dt match {
  // this would fail at runtime as e.g DTuple("Tag") would enter this case (the `DTuple["BuildTime", DMapping]` is unchecked at runtime
  // case d: DTuple["BuildTime", DMapping] => d.second.format(ISO_ZONED_DATE_TIME)

  // this doesn't compile because `d.second`'s type is still 'DMapping[(d.first : DKey)]', not 'DMapping["BuildTime"]'
  // case d@DTuple("BuildTime") => d.second.format(ISO_ZONED_DATE_TIME)

  case d@DTuple("Tag"): DTuple["Tag", DMapping] => d.second
  case d@DTuple("Versions"): DTuple["Versions", DMapping] => d.second.mkString(", ")
  case d@DTuple("Author"): DTuple["Author", DMapping] => d.second.toString
  case d@DTuple("BuildTime"): DTuple["BuildTime", DMapping] => d.second.format(ISO_ZONED_DATE_TIME)
}

object Main extends App {
  List(
    DTuple[DKey, DMapping]("Versions")(Array("1.0", "2.0")),
    DTuple[DKey, DMapping]("Tag")("env=SO"),
    DTuple[DKey, DMapping]("Author")("MK"),
    DTuple[DKey, DMapping]("BuildTime")(ZonedDateTime.now())
  ).foreach { dt =>
    println(mkString(dt))
  }
}

prints

1.0, 2.0
env=SO
MK
2020-10-23T21:04:06.696+02:00[Europe/Paris]
Marth
  • 23,920
  • 3
  • 60
  • 72
  • It works, but it's raising warnings about `: DTuple[...]` which cannot be checked at run-time. Maybe I should switch to Coq or Agda at some point. Thanks for trying – Mikaël Mayer Oct 23 '20 at 19:29
  • 1
    You do get a warning but it is actually checked at run-time, but at the value-level (not the type-level like `DTuple["Tag", DMapping]` is). Though blatantly wrong `case _ => ` compile, e.g `case d@DTuple("Versions"): DTuple["Tag", DMapping] => d.second` compiles and raises a runtime error (`d.second` is infered to be a `String` but is actually an `Array[String]`). There's probably a way to do it, but I won't be the one to find it :) – Marth Oct 23 '20 at 20:04