Search code examples
scaladependent-typepath-dependent-typestructural-typingscala-3

How to add type-checking to a function extraction summary


I'm discovering Dotty and I'd love to come up with a typed version of my algorithms. I want to achieve the following that I can do easily in JavaScript. It's basically a condensed way to extract a property of a record or an array:

function Field(key, action) {
  return {
    apply(record) {
      return action.apply(record[key]);
    }
  };
}
var Identity = { apply(record) { return record; } };

console.log(Field(3, Field("a", Identity)).apply([0, 1, 2, {a: "Hello"}]))
// Prints out "Hello"

I have a bunch of functions like Field I'm trying to type. Here is what I tried so far. Records or objects are modelled as a structural type { def get(k: Key): KeyMapper[Key] } which essentially tries to statically obtain the type of the field if the type of the input is known statically, as in this question. Here is my first successful attempt, and below what remains and fails.

trait Apply[A, B] {
  def apply(a: A): B
}
case class Identity[A]() extends Apply[A, A] {
  def apply(a: A) = a
}

case class Field
  [Key: ClassTag,
   KeyMapper[_],
   Record <: { def get(k: Key): KeyMapper[Key]},
   B](key: Key, subAction: Apply[KeyMapper[Key], B]) extends Apply[Record, B] {
    def apply(record: Record) = subAction(record.get(key))
}

So far so good, it compiles without type error. Now, I wish to integrate the type definitions Key and KeyMapper as part of the record, so that I only have two type parameters, not four, and the code is easier to maintain. I tried the following:

trait Record {
  type KeyMapper[T]
  type Key
  def apply(k: Key): KeyMapper[Key]
}
case class Field[A <: Record, U](key: A#Key, subAction: Apply[A#KeyMapper[A#Key], U]) extends Apply[A, U] {
    def apply(record: A): U = subAction(record(key))

I get the following error::

[error]    |    def apply(record: A): U = subAction(record(key))
[error]    |                                               ^^^
[error]    |                                       Found:    (Down.this.key : A#Key)
[error]    |                                       Required: record.Key

Ok, so far I don't see any other way than casting key with .asInstanceOf[record.Key], and then I get the following error:

[error] 43 |    def apply(record: A): U = subAction(record(key.asInstanceOf[record.Key]))
[error]    |                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error]    |                                  Found:    record.KeyMapper[record.Key]
[error]    |                                  Required: A#KeyMapper[A#Key]

Ok, I'm a bit disappointed, but I add a cast to A#KeyMapper[A#Key]. Then I get the error:

[error] 42 |  case class Field[A <: Record, U](key: A#Key, subAction: Apply[A#KeyMapper[A#Key], U]) extends Apply[A, U] {
[error]    |                                        ^
[error]    |                                        A is not a legal path
[error]    |                                        since it is not a concrete type

Hum so I read a bit and see that type projection was deprecated and removed from Dotty, so I need to have a concrete value. Here is my next attempt:

trait RecordAndEdit { outer =>
  type Val <: {
    def get(k: outer.Key): outer.Map[k.type]
  }
  type Key
  type Map[_]
}
class Field[U](val rOps: RecordAndEdit)(val key: rOps.Key, val subAction: Apply[rOps.Map[rOps.Key], U]) extends Apply[rOps.Val, U] {
  def apply(record: rOps.Val): U = subAction(record.get(key))
}

I get the error

[error] 35 |    def apply(record: rOps.Val): U = subAction(record.get(key))
[error]    |                                               ^^^^^^^^^^^^^^^
[error]    |Structural access not allowed on method get because it has a method type with inter-parameter dependencies

At this point, I don't understand how I can solve this error message, because I want the get method to have a return type that depends on the input type. Any clue?


Solution

  • Ok, thanks to the comments, I was able to carefully design the following answer, that does not need projection types, but uses dependent types, as in this answer:

      trait Apply[Input, Output]:
        def apply(k: Input): Output
      
      trait WithDomain[X] {
        type Key
        type KeyMapper[_ <: Key]
        def get(x: X, k: Key): KeyMapper[k.type]
      }
      
      class Field[Input, Output](using val g: WithDomain[Input])(val key: g.Key, val next: RecordEdit[g.KeyMapper[key.type], Output]) extends Apply[Input, Output]:
        def apply(r: Input): Output =
          next(g.get(r, key))
          
      object Field:
        def apply[Input, Output](using g: WithDomain[Input])(key: g.Key, next: RecordEdit[g.KeyMapper[key.type], Output]): RecordEdit[Input, Output] = 
          new Field[Input, Output]()(key, next)
       
      class Identity[T] extends RecordEdit[T, T]:
        def apply(r: T) = r
      
      object Identity:
        def apply[T]() = new Identity[T]()
    

    And everything works as expected, for example:

      class Node(val tag: String, val children: Array[Node] = Array())
    
      given as WithDomain[Node] { self =>
        type Key = "tag" | "children"
        type Mapping[X <: self.Key] = (X match {
          case "tag" => String
          case "children" => Array[Node]
        })
        def get(x: Node, k: self.Key): self.Mapping[k.type] = k match {
          case _: "tag" => x.tag
          case _: "children" => x.children
        }
      }
      
      given[T] as WithDomain[Array[T]] {
        type Key = Int
        type Mapping[Int] = T
        def get(x: Array[T], k: Int): T = x(k)
      }
    
      println(Field[Node, String]("children",
                Field[Array[Node], String](0,
                   Field[Node, String]("tag", Identity())))(
               Node("hello world", Array(Node("hi world")))))
    

    Note that I also switched to the new indentation-style for Dotty, which I think is great.