Path-dependent types
Marcin Rzeźnicki
mrzeznicki@iterato.rs
... we use this stuff
Path-dependent types
- Od Scali 2.10
- Dla language lawyers (za Scala Reference Book):
Paths are not types themselves, but they can be a part of named types and in that
function form a central role in Scala’s type system. -
The base types of a type selection S #T are determined as follows (...) T must be a
(...) type, which is defined in some class B . Then the
base types of S #T are the base types of T in B seen from the prefix type S.
W ogólności ...
W Scali typy zagnieżdżone są związane z konkretną instancją typu zewnętrznego (outer), a nie z samym typem zewnętrznego (jak w Javie). Składnia p1.p2.T wskazuje na typ T poprzez ścieżkę prowadzącą przez konkretne typy zewnętrzne - stąd nazwa path-dependent
Najprostsza ścieżka
object MyService {
class MyServiceLogger {
def log(message: String) = println("1: "+message)
}
val logger = new MyServiceLogger
}
logger ma typ: MyService.MyServiceLogger
Trochę komplikacji
trait Service {
trait Logger {
def log(message: String): Unit
}
val logger: Logger
}
object MyService extends Service {
class MyServiceLogger extends Logger {
def log(message: String) = println("1: "+message)
}
override val logger = new MyServiceLogger
}
logger ma dalej typ: MyService.MyServiceLogger <: MyService.Logger <: Service#Logger
typ: ?.Logger (? to nasza ścieżka)
definicja typu MyService.MyServiceLogger <: MyService.Logger
Trochę komplikacji
trait Service {
trait Logger {
def log(message: String): Unit
}
val logger: Logger
}
object MyService1 extends Service {
class MyService1Logger extends Logger {
def log(message: String) = println("1: "+message)
}
override val logger = new MyService1Logger
}
object MyService2 extends Service {
override val logger = MyService1.logger
}
logger ma typ MyService1.MyServiceLogger
???
Gdzie jest błąd?
class A {
class B
var b: Option[B] = None
}
val a1 = new A
val a2 = new A
val b1 = new a1.B
val b2 = new a2.B
a1.b = Some(b1)
a2.b = Some(b1)
Jak i po co używać?
Przykład ukradziony z Neophyte's Guide to Scala
object Universe {
case class Character(name: String)
}
class Universe(name: String) {
import Universe.Character
def createLoveStory(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)
}
val starTrek = new Universe("Star Trek")
val starWars = new Universe("Star Wars")
val quark = Universe.Character("Quark")
val jadzia = Universe.Character("Jadzia Dax")
val luke = Universe.Character("Luke Skywalker")
val yoda = Universe.Character("Yoda")
Jak i po co używać?
Problem !!!
starTrek.createLoveStory(lovestruck = jadzia, objectOfDesire = luke)
Jak i po co używać?
class Universe(name: String) {
case class Character(name: String)
def createLoveStory(
lovestruck: Character,
objectOfDesire: Character): (Character, Character) = (lovestruck, objectOfDesire)
}
val starTrek = new Universe("Star Trek")
val starWars = new Universe("Star Wars")
val quark = starTrek.Character("Quark")
val jadzia = starTrek.Character("Jadzia Dax")
val luke = starWars.Character("Luke Skywalker")
val yoda = starWars.Character("Yoda")
PDT to the rescue
Jak i po co używać?
starTrek.createLoveStory(lovestruck = jadzia, objectOfDesire = luke)
found : starWars.Character
required: starTrek.Character
starTrek.createFanFictionWith(lovestruck = jadzia, objectOfDesire = luke)
Nie ma problemu :-)
def createLoveStory(u: Universe)(lovestruck: u.Character, objectOfDesire: u.Character) =
(lovestruck, objectOfDesire)
Alternatywnie - dependent method types
PDT + parametry typów
Problem: chcemy zakodować type-safe operacje na liczbach całkowitych mod N + chcemy żeby były tak transparentne jak to możliwe. Nie można mieszać liczb z różnych ciał modN tj. nie ma sensu operacja (i mod N) + (i mod M)
Rozwiązanie: Możemy reprezentować pola Galoise'a (liczby mod N) jako typy. Wtedy, z definicji PDT, każdy typ "ze środka" takiego pola będzie unikalny (bedzie miał inną ścieżkę)
PDT + parametry typów
case class Z(modulus: Int) {
sealed class Modulus {
val value = modulus
}
}
class IntMod[N <: Z#Modulus] private(val value: Long) extends AnyVal
val z1 = Z(10000103)
val z2 = Z(20000208)
val i1 = 10: IntMod[z1.Modulus]
val i2 = 100000000: IntMod[z1.Modulus]
val i3 = 10: IntMod[z2.Modulus]
val i4: IntMod[z2.Modulus] = i1
error: type mismatch;
found : IntMod[z1.Modulus]
required: IntMod[z2.Modulus]
PDT + implicity
case class Z(modulus: Int) {
sealed class Modulus {
val value = modulus
}
object Modulus {
implicit val mod = new Modulus()
}
}
class IntMod[N <: Z#Modulus] private(val value: Long) extends AnyVal
object IntMod {
implicit def intAsIntModN[N <: Z#Modulus](i: Int)(implicit modulus: N): IntMod[N] =
new IntMod[N](i % modulus.value)
implicit def longAsIntModN[N <: Z#Modulus](i: Long)(implicit modulus: N): IntMod[N] =
new IntMod[N](i % modulus.value)
implicit def intModN2Long(a: IntMod[_]): Long = a.value
implicit def intModN2Int(a: IntMod[_]): Int = a.value.toInt
}
PDT - mały catch
Article is really cool, but I'm not sure if given example is clear enough. I have a problem with the situation when I create two instances of Z which are semantically equal
val z1 = Z(23)
val z2 = Z(23)
I would expect that
implicitly[z1.Modulus =:= z2.Modulus]
is `true` but is not.
Case-study - Effect.io
Pamiętać: zmiana ekranu !!! :-)
Path-dependent types
By Marcin Rzeźnicki
Path-dependent types
- 1,030