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?
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:
And everything works as expected, for example:
Note that I also switched to the new indentation-style for Dotty, which I think is great.