How do I say „Here's a trait Lookup
with a get(key: String): Option[V]
method which Map[String, V]
already happens to implement”?
I tried
trait Lookup[V] {
def get(key: String): Option[V]
}
but then I get
[error] 30 | override def get(): Lookup[mine.Object] = Map(
[error] | ^
[error] |Found: Map[String, Infrastructure]
[error] |Required: dsd.ctms.afs.validation.Lookup[Infrastructure]
and I don't understand why. I can define trait Lookup[V] {def get(key: String): V}
and then it's implemented by Map
, but that is not the semantics I mean to capture.
There is is more than one way of defining relationship between the types.
That you are thinking of is called structural typing or ducktyping: if a value has all the parameters and methods that some type requires, it is of this type.
Another is nominal typing: this value is of this type if I specify that it is, either because I created it as a value of this type, or its type is defined as subtype of this type, similarities do not matter.
Out of the box, Scala (actually the whole JVM, if you don't use runtime reflection) uses nominal typing:
Map
is defined as a separate type, you have to create a value as instance of Map or some of its subtypes to consider it Map
Lookup
, not the Lookup
to claim that Map
is extending itThat is until we explicitly opt-in into runtime reflection and structural types (and refined types, but let's skip this topic):
type Lookup[V] = {
def get(key: String): Option[V]
}
def codeThatNeedsLookup[M <: Lookup[Int]](value: M): Int = {
value.get("value a").getOrElse(0) +
value.get("value b").getOrElse(0) +
value.get("value c").getOrElse(0)
}
Then Scala will be able to pass Maps as instances of Lookup... but at the cost of runtime reflection and performance penalty, because under the hood it would do something like
val map: Any //all the type information that refinement has
map.getClass
.getMethod("get", classOf[String])
.invoke(map).asInstanceOf[Option[String]]
In practice, hardly ever there is a need to use runtime reflection, as most of the time people would resort to some type class or rearrange their API:
trait Lookup[M, V] {
def get(map: M)(key: Key): Option[V]
}
object Lookup {
given mapLookup[V]: Lookup[Map[String, V], V] =
new Lookup[Map[String, V], V] {
def get(map: Map[String, V])(key: Key): Option[V] = map.get(key)
}
// givens for other structures that should be supported
extension [M](map: M) {
def get[V](using l: Lookup[M, V])(key: String): Option[V] =
l.get(map)(key)
}
}
// Instead of M <: Lookup[Int] I am using M : Lookup[*, Int]
def codeThatNeedsLookup[M : Lookup[*, Int]](value: M): Int = {
value.get("value a").getOrElse(0) +
value.get("value b").getOrElse(0) +
value.get("value c").getOrElse(0)
}
This way I can avoid runtime reflection (bindings are resolved during compilation) and keep flexibility (I can add get(String)
method to virtually any type I want this way, I can hardcode these types or use generics like here).