- Kurt Lewin
It's all about how expressive type system we want/need/can live with - where is the point of impracticality?
- Kurt Lewin
It's all about how expressive type system we want/need/can live with - where is the point of impracticality?
Visualizes three "dimensions" of type-system abstraction. What is this abstraction? Informally, it's mutual dependency of types and terms:
val x = 2
val y = 2
val z: Int = x * y
//or
def multiply(x: Int, y: Int): Int = x * y
val double: Int => Int = multiply(_, 2)
twice: (Int -> Int) -> Int -> Int
twice f x = f (f x)
quadruple: Int -> Int
quadruple = twice (\x => x * 2)
def identity[A](x: A) = x
def twice[A](a: A)(f: A => A): A = f(f(a))
twice: (a -> a) -> a -> a
twice f x = f (f x)
quadruple: Int -> Int
quadruple = twice (\x => x * 2)
trait Function1[-T1, +R] {
def apply(v1: T1): R
}
trait Invariant[F[_]] {
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
}
data List : Type -> Type where
Nil : List elem
(::) : elem -> List elem -> List elem
interface Functor (f : Type -> Type) where
map : (func : a -> b) -> f a -> f b
trait Dep {
type V
val value: V
}
def dep(that: Dep): that.V = that.value
// dep: (that: Dep)that.V
data Vect : Nat -> Type -> Type where
Nil : Vect Z a
(::) : a -> Vect k a -> Vect (S k)
↑ - adds term-type
→ - adds type-term
↗ - adds type-type
Alternatively, using just six typing rules (four general rules valid for all systems + two specific rules that differentiate between them) one can derive a large family of type-systems.
All "corners" of λ-cube can be represented as instances of PTS (as well as many others like System U where all types are inhabited).
single syntactic category for regular terms and types
$$\Pi x:A.B$$ family of types which assigns to each term x of sort A a type B ( x )
pi: (x: t) -> (B: t -> Type) -> Type
pi x B = B(x)
funType: (A:Type) -> (B: Type) -> Type =
pi A (const B)
polyType: (A: Type) -> (constr: Type -> Type) -> Type =
pi A constr
polyTypeKind3: (A: Type) -> (B: Type) -> (constr: Type -> Type -> Type) -> Type =
pi A (\a => pi B (constr a))
depVectorOfReals: (n: Nat) -> Type =
pi n (\n => Vect n Double)
{-
Idris> polyType String List
polyType String List : Type = List String
Idris> polyTypeKind3 String Nat Either
polyTypeKind3 String Nat Either : Type = Either String Nat
Idris> depVectorOfReals 10
depVectorOfReals 10 : Type = Vect 10 Double
-}
$$ \Box $$ describes type kinds ie. types of type constructors, like: $$ \star \rightarrow \star \rightarrow \star $$
In our type systems we may want to introduce following abstractions:
First two are clearly types. But what is \( \star \to \star \)? Probably not a type just as List or Map is not a type. Hence, sort of kinds
Type formation rule restricts which sorts we are allowed to quantify over. In turn, this restricts which abstractions can be introduced by the abstraction rule
Only term-term dependencies are allowed. The only type constructor is function type
The following can be derived
def length(s: String) = ???
def isValid(s: String): Boolean = length(s) < 5
//no polymorphism
def intIdentity(i: Int) = i
def stringIdentity(s: String) = s
def andThen(f: Int => String, g: String => Boolean) = (x: Int) => g(f(x))
We can compute quite a lot, but this is not exactly practical for development because:
THERE ARE NO "REAL" TYPES
THERE IS NO POLYMORPHISM
Let's add type operators to simply-typed lambda calculi. We do this by allowing another pair of sorts to appear in typing rules: \( (\Box, \Box) \) . In addition to previous rules, we now can type:
The following can be derived
//this we can derive
trait Either[A, B]
type Id[A] = A
trait List[A]
//but, we cannot use it generically because
//term-level polymorphism is not derivable in lambda_omega, so
def length(l: List[Int]): Int = ???
// no def length[A](l: List[A]): Int = ???
def head(l: List[Int]): Either[String, Int] =
if (length(l) > 0) Right(l(0)) else Left("No such element")
// no def head[A](l: List[A]): Either[String, A]
We still DEMAND generic programming!
Polymorphism is added by allowing \( ( \Box, \star ) \) combination (term-type dependency). With this rule, we obtain the famous System F (Hindley-Milner is a subset of System F)
The following can be derived
Combining system F with type operators, we obtain system Fω, where we have what we're used to in modern functional programming.
Actually, we have much more because we can unleash full power of lambda calculus on type level.
System Fω allows universal quantification, abstraction and application at higher kinds.
Type systems of languages like Scala and Haskell deliver slightly less than Fω
For example in system F one can achieve higher-rank polymorphism, which is not possible (directly) in Scala (possible in Idris though)
def pair[A, B](f: A => (A, A),
b: B): (B, B) =
f(b)
/*
error: type mismatch;
found : b.type
(with underlying type B)
required: A
We can't say, in fictional Scala
def pair[B](f: A => (A, A){forAll A},
b: B): (B, B) */
mkPair: ({a: _} -> a -> (a, a)) ->
b ->
(b, b)
mkPair f b = f(b)
{-
Idris> mkPair (\a => (a, a)) 10
(10, 10) : (Integer, Integer)
Idris> mkPair (\a => (a, a)) "Hello"
("Hello", "Hello") : (String, String)
-}
The same proof translated to Scala/Idris could be:
trait &[A, B] {
def apply[γ](x: A => B => γ): γ
}
type AND[A, B] = A & B
def K1[A, B](x: A)(y: B) = x
def K2[A, B](x: A)(y: B) = y
// A AND B => A
def proof1[A, B](x: A AND B): A =
x[A](K1)
// A AND B => B
def proof2[A, B](x: A AND B): B =
x[B](K2)
data And : a -> b -> Type where
AND : (pi: {c: Type} ->
(a -> b -> c) -> c
) -> And a b
prf1 : And a b -> a
prf1 (AND pi) = let
k = \x: a, y: b => x
in pi(k)
prf2 : And a b -> b
prf2 (AND pi) = let
k = \x: a, y: b => y
in pi(k)
We haven't yet explored the possibility of allowing \( (\star , \Box) \) rule. This enables type-term dependency.
Motivations (things we are not able to express):
Question arises - whether additional type-safety is worth the effort of adapting it in languages that do not support it intrinsically?
Is it even possible?
- Miles Sabin
data PowerSource = Petrol | Pedal | Electric
data Vehicle : PowerSource -> Type where
Bicycle : Vehicle Pedal
Motorcycle : (fuel: Nat) -> Vehicle Petrol
Car : (fuel: Nat) -> Vehicle Petrol
Bus : (fuel: Nat) -> Vehicle Petrol
Tram : Vehicle Electric
wheels : Vehicle power -> Nat
wheels Bicycle = 2
wheels (Motorcycle _) = 2
wheels (Car _) = 4
wheels (Bus _) = 4
wheels Tram = 8
refuel : Vehicle Petrol -> Vehicle Petrol
refuel (Car _) = Car 100
refuel (Bus _) = Bus 200
refuel (Motorcycle _) = Motorcycle 50
sealed trait Vehicle {
def wheels: Int
}
sealed trait PedalVehicle extends Vehicle
sealed trait ElectricVehicle extends Vehicle
sealed trait PetrolVehicle extends Vehicle {
def fuel: Int
def refuel(): PetrolVehicle
}
case object Bicycle
extends PedalVehicle {val wheels = 2}
case object Tram
extends ElectricVehicle {val wheels = 8}
case class Motorcycle(fuel: Int)
extends PetrolVehicle {
val wheels = 2
def refuel() = Motorcycle(50)
}
//...
enumerated dep.
encoded as subtypes
data PowerSource = Petrol | Pedal | Electric
data Vehicle : PowerSource -> Type where
Bicycle : Vehicle Pedal
Motorcycle : (fuel: Nat) -> Vehicle Petrol
Car : (fuel: Nat) -> Vehicle Petrol
Bus : (fuel: Nat) -> Vehicle Petrol
Tram : Vehicle Electric
wheels : Vehicle power -> Nat
wheels Bicycle = 2
wheels (Motorcycle _) = 2
wheels (Car _) = 4
wheels (Bus _) = 4
wheels Tram = 8
refuel : Vehicle Petrol -> Vehicle Petrol
refuel (Car _) = Car 100
refuel (Bus _) = Bus 200
refuel (Motorcycle _) = Motorcycle 50
sealed trait Vehicle {
def wheels: Int
}
sealed trait PowerSource { self: Vehicle => }
trait Pedal
extends PowerSource { self: Vehicle => }
trait Electric
extends PowerSource { self: Vehicle => }
trait Petrol { self: Vehicle =>
def fuel: Int
def refuel(): Vehicle with Petrol
}
case object Bicycle
extends Vehicle with Pedal
{val wheels = 2}
case object Tram
extends Vehicle with Electric
{val wheels = 8}
case class Car(fuel: Int)
extends Vehicle with Petrol {
val wheels = 4
def refuel() = Car(100)
}
encoded as mixin
data PowerSource = Petrol | Pedal | Electric
data Vehicle : PowerSource -> Type where
Bicycle : Vehicle Pedal
Motorcycle : (fuel: Nat) -> Vehicle Petrol
Car : (fuel: Nat) -> Vehicle Petrol
Bus : (fuel: Nat) -> Vehicle Petrol
Tram : Vehicle Electric
wheels : Vehicle power -> Nat
wheels Bicycle = 2
wheels (Motorcycle _) = 2
wheels (Car _) = 4
wheels (Bus _) = 4
wheels Tram = 8
refuel : Vehicle Petrol -> Vehicle Petrol
refuel (Car _) = Car 100
refuel (Bus _) = Bus 200
refuel (Motorcycle _) = Motorcycle 50
sealed trait Vehicle[PS <: PowerSource] {
def wheels: Int
}
sealed trait PowerSource
trait Pedal extends PowerSource
trait Electric extends PowerSource
trait Petrol extends PowerSource
//...
case class Bus(fuel: Int)
extends Vehicle[Petrol] {
val wheels = 4
def refuel() = Bus(200)
}
def refuel(vehicle: Vehicle[Petrol])
:Vehicle[Petrol] = vehicle match {
case c@Car(_) => c.refuel()
case b@Bus(_) => b.refuel()
case m@Motorcycle(_) => m.refuel()
}
}
phantom type
data Nat = Z | S Nat
-- S ( S (Z) )
-- 2 : Nat
-- n: Nat = 500
-- n : Nat = 500
head : Vect (S len) elem -> elem
head (x::xs) = x
sealed trait Nat {
type N <: Nat
}
case object Z
extends Nat {type N = Z.type}
case class S[P <: Nat]()
extends Nat {type N = S[P]}
object Nat {
type _0 = Z.type
type _1 = S[_0]
type _2 = S[_1]
type _3 = S[_2]
val _0: _0 = Z
val _1: _1 = S[_0]
val _2: _2 = S[S[_0]]
val _3: _3 = S[S[S[_0]]]
}
recursive type
type member carrying "result"
data Nat = Z | S Nat
-- S ( S (Z) )
-- 2 : Nat
-- n: Nat = 500
-- n : Nat = 500
readNat: IO (Maybe Nat)
readNat = do input <- getLine
pure (parsePositive input)
sealed trait Nat {
type N <: Nat
}
case object Z
extends Nat {type N = Z.type}
case class S[P <: Nat]()
extends Nat {type N = S[P]}
object Nat {
//...
implicit def apply(i: Int): Nat = macro ...
def toInt[N <: Nat] = macro ...
def toInt(n: Nat) = macro ...
}
automatic rep of literals
macros / literals only!
data Nat = Z | S Nat
-- S ( S (Z) )
-- 2 : Nat
-- n: Nat = 500
-- n : Nat = 500
readNat: IO (Maybe Nat)
readNat = do input <- getLine
pure (parsePositive input)
sealed trait Nat {
type N <: Nat
}
case object Z
extends Nat {type N = Z.type}
case class S[P <: Nat]()
extends Nat {type N = S[P]}
object Nat {
//...
transparent def toNat(n: Int): Nat =
n match {
case 0 => Z
case n if n > 0 => S(toNat(n - 1))
}
transparent def toInt[N <: Nat]: Int =
anyValue[N] match {
case _: Z => 0
case _: S[n] => toInt[n] + 1
}
}
automatic rep of literals
in Dotty it might be easier due to the so-called transparent functions / literals only!
data Nat = Z | S Nat
-- S ( S (Z) )
-- 2 : Nat
-- n: Nat = 500
-- n : Nat = 500
readNat: IO (Maybe Nat)
readNat = do input <- getLine
pure (parsePositive input)
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
sealed trait Nat {
type N <: Nat
}
case object Z
extends Nat {type N = Z.type}
case class S[P <: Nat]()
extends Nat {type N = S[P]}
object Nat {
//...
def apply(i: Int): Option[Nat] = {
@scala.annotation.tailrec
def succ(j: Int, n: Nat = _0): Nat = {
val s = S[n.N]
if (j == 0) s else succ(j - 1, s)
}
if (i < 0) None
else if (i == 0) Some(_0)
else Some(succ(i - 1))
}
}
alternatively - but: no type preservation and we can't pattern match on type level anyway
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
-- natToFin 2 3
-- Just (FS (FS FZ)) : Maybe (Fin 3)
-- natToFin 0 3
-- Just FZ : Maybe (Fin 3)
sealed trait Fin[SK <: S[_]]
case class FZ[SK <: S[_]]()
extends Fin[SK]
case class FS[K <: S[_], F <: Fin[K]]()
extends Fin[S[K]]
type indexed by integer
type-level recursion
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
trait FinToNat[F <: Fin[_]] {
type Out <: Nat
def apply(): Out
}
object FinToNat {
type Result[F <: Fin[_], N <: Nat] =
FinToNat[F] {type Out = N}
// ...
}
encode the "result" type, known as Aux pattern
lift computation to a type
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
object FinToNat {
type Result[F <: Fin[_], N <: Nat] =
FinToNat[F] {type Out = N}
implicit def caseFZ[N <: S[_]]:
Result[FZ[N], _0] = new FinToNat[FZ[N]] {
type Out = _0
def apply() = _0
}
implicit def caseFS[K <: S[_],
F <: Fin[K],
SoFar <: Nat](
implicit finToNatK: Result[F, SoFar]):
Result[FS[K, F], S[SoFar]] =
new FinToNat[FS[K, F]] {
type Out = S[SoFar]
def apply() = S[SoFar]()
}
def apply[F <: Fin[_]](
implicit finToNat: FinToNat[F]):
Result[F, finToNat.Out] = finToNat
}
encode computation as implicit search
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
sealed trait Fin[SK <: S[_]]
case class FZ[SK <: S[_]]()
extends Fin[SK]
case class FS[K <: S[_], F <: Fin[K]]()
extends Fin[S[K]]
object Fin {
def finToNat[F <: Fin[_]](f: F)(
implicit finToNat: FinToNat[F]):
finToNat.Out = finToNat()
}
/*
scala> FS[_4, FZ[_4]]
res1: FS[Nat._4,FZ[Nat._4]] = FS()
scala> Fin.finToNat(res1)
res2: S[Z.type] = S()
*/
encode computation as implicit search
preserve structure representation
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
trait NatToFin[M <: Nat, N <: S[_]] {
type Out <: Fin[N]
def apply(): Out
}
object NatToFin {
type Result[M <: Nat,
N <: S[_],
F <: Fin[N]] =
NatToFin[M, N] {
type Out = F
}
// ...
}
encode the "result" type, known as Aux pattern
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
object NatToFin {
//...
implicit def caseZSj[J <: S[_]]:
Result[_0, J, FZ[J]] =
new NatToFin[_0, J] {
type Out = FZ[J]
def apply(): Out = FZ[J]()
}
implicit def caseSkSj[K <: Nat, J <: S[_]](
implicit natToFinKJ: NatToFin[K, J]):
Result[S[K], S[J], FS[J, natToFinKJ.Out]] =
new NatToFin[S[K], S[J]] {
type Out = FS[J, natToFinKJ.Out]
def apply(): Out = FS[J, natToFinKJ.Out]
}
def apply[M <: Nat, N <: S[_]](
implicit natToFin: NatToFin[M, N]):
Result[M, N, natToFin.Out] = natToFin
}
encode computation as implicit search
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S k)
finToNat : Fin n -> Nat
finToNat FZ = Z
finToNat (FS k) = S (finToNat k)
natToFin : Nat -> (n : Nat) -> Maybe (Fin n)
natToFin Z (S j) = Just FZ
natToFin (S k) (S j) with (natToFin k j)
| Just k' = Just (FS k')
| Nothing = Nothing
natToFin _ _ = Nothing
object Fin {
//...
def natToFin[M <: Nat, N <: S[_]](
implicit natToFin: NatToFin[M, N]):
natToFin.Out = natToFin()
def natToFin[M <: Nat, N <: S[_]](m: M,
n: N)(
implicit natToFin: NatToFin[m.N, n.N]):
natToFin.Out = natToFin()
}
/*
scala> Fin.natToFin(_2, _3)
res1: FS[S[S[Z.type]],...] = FS()
scala> Fin.natToFin(_6, _2)
error: could not find implicit value
for parameter natToFin:
NatToFin[Nat._6.N,Nat._2.N]
Fin.natToFin(_6, _2)
*/
data Vect : (len : Nat) ->
(elem : Type) ->
Type where
Nil : Vect Z elem
(::) : (x : elem) -> (xs : Vect len elem) ->
Vect (S len) elem
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
head : Vect (S len) elem -> elem
head (x::xs) = x
sealed trait Vect[Len <: Nat, +Elem] {
type Repr <: Vect[Len, Elem]
}
case object NIL extends Vect[_0, Nothing] {
type Repr = NIL.type
def ::[Elem](elem: Elem):
Cons[_0, Elem, this.type] =
Cons(elem, this)
}
case class Cons[Len <: Nat,
Elem,
XS <: Vect[Len, Elem]](
head: Elem, tail: XS)
extends Vect[S[Len], Elem] {
type Repr = Cons[Len, Elem, XS]
def ::[Elem0 >: Elem](head: Elem0):
Cons[S[Len], Elem0, this.type] =
Cons(head, this)
}
data Vect : (len : Nat) ->
(elem : Type) ->
Type where
Nil : Vect Z elem
(::) : (x : elem) -> (xs : Vect len elem) ->
Vect (S len) elem
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
head : Vect (S len) elem -> elem
head (x::xs) = x
sealed trait Vect[Len <: Nat, +Elem] {
type Repr <: Vect[Len, Elem]
def head(implicit ...): Elem = ???
//this match { case Cons(x, xs) =>
def tail(implicit ...): ??? = ???
}
pattern match on type level - only non-empty vectors
doesn't compile - you can't pattern match on generic data types
these have to come from the implicit evidence - (S len) is not enough
data Vect : (len : Nat) ->
(elem : Type) ->
Type where
Nil : Vect Z elem
(::) : (x : elem) -> (xs : Vect len elem) ->
Vect (S len) elem
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
head : Vect (S len) elem -> elem
head (x::xs) = x
trait IsCons[V <: Vect[Len, Elem],
Len <: Nat,
Elem] {
type XS <: Vect[_, Elem]
def x(vect: V): Elem
def xs(vect: V): XS
}
object IsCons {
type Result[V <: Vect[S[Len], Elem],
Len <: Nat,
Elem,
XS0 <: Vect[Len, Elem]] =
IsCons[V, S[Len], Elem] { type XS = XS0 }
}
missing type-level pattern match
capture type of tail
"unapply"
constraints
data Vect : (len : Nat) ->
(elem : Type) ->
Type where
Nil : Vect Z elem
(::) : (x : elem) -> (xs : Vect len elem) ->
Vect (S len) elem
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
head : Vect (S len) elem -> elem
head (x::xs) = x
sealed trait Vect[Len <: Nat, +Elem] {
type Repr <: Vect[Len, Elem]
protected def _this: Repr
def head[Elem0 >: Elem](
implicit ev: IsCons[Repr, Len, Elem0]):
Elem0 =
ev.x(_this)
def tail[Elem0 >: Elem](
implicit ev: IsCons[Repr, Len, Elem0]):
ev.XS =
ev.xs(_this)
}
these come from the implicit evidence - (S len) is not enough
*> :let v = 1 :: 2 :: Nil
*> :t v
v : Vect 2 Integer
*> head . tail $ v
2 : Integer
*> tail . tail $ v
[] : Vect 0 Integer
*> tail . tail . tail $ v
Type mismatch between
Vect 2 Integer (Type of v)
and
Vect (S (S (S len))) elem (Expected type)
Specifically:
Type mismatch between
0
and
S len
scala> val v = 1 :: 2 :: NIL
v: Cons[S[Z.type],
Int,
_ <: Cons[Nat._0,Int,NIL.type]] =
Cons(1,Cons(2,NIL))
scala> v.tail.head
res2: Int = 2
scala> v.tail.tail
res3: NIL.type = NIL
scala> v.tail.tail.head
<console>:13: error:
could not find implicit value for parameter
ev: IsCons[v.tail.tail.Repr,Nat._0,Elem0]
v.tail.tail.head
^
scala> v.tail.tail.tail
<console>:13: error:
could not find implicit value for parameter
ev: IsCons[v.tail.tail.Repr,Nat._0,Elem0]
v.tail.tail.tail
^
Hey, but this is cheating! All these examples assume the size of collection to be statically known. My collections come from the Internet (database/file)!
This seems somewhat problematic: how to represent vectors of unknown length if length is part of the type?
In dependent typing theory there is a device for this called Σ type (a.k.a. dependent pair)
data DPair : (a : Type) -> (a -> Type) -> Type where
MkDPair : (x : a) -> P x -> DPair a P
-- syntactic sugar for this is `**`
-- for example
filter : (elem -> Bool) -> Vect len elem -> (p : Nat ** Vect p elem)
vectFromTheInternetOrDatabaseOrFile : IO (len ** Vect len String)
doSthWithSuchVector : (length ** Vect length String) -> Maybe String
doSth (Z ** _) = Nothing
doSth (S (S (S Z)) ** vec3) = Just $ head . tail $ vec3
doSth (S k ** nonEmpty) = Just $ head nonEmpty
-- but
doSth (S k ** nonEmpty) = Just $ head . tail $ nonEmpty
{-
Type mismatch between
(\length => Vect length String) (S k) (Type of nonEmpty)
and
Vect (S (S len)) String (Expected type)
Specifically:
Type mismatch between
k
and
S len
-}
Σ type can be expressed in Scala (awkwardly though)
trait DType[A] {
type T
}
object DType {
type **[A, T0] = DType[A] { type T = T0 }
def apply[A, T0](): A ** T0 = new DType[A] { type T = T0 }
}
abstract class DPair[A, B](implicit P: A ** B) {
def x: A
def y: B
}
sealed trait State
case class LoggedIn(user: User) extends State
case object LoggedOut extends State
class LoggedInActions(user: User) {
def sayHello() = s"Hello ${user.name}!"
}
class LoggedOutActions {
def logIn(password: String): User = ???
}
implicit val loggedIn: LoggedIn ** LoggedInActions = DType()
implicit val loggedOut: LoggedOut.type ** LoggedOutActions = DType()
val dPairLoggedIn = DPair(LoggedIn(User("Marcin")))(
x => new LoggedInActions(x.user))
val dPairLoggedOut = DPair(LoggedOut, new LoggedOutActions())
val noCheating = DPair(LoggedOut, new LoggedInActions(User("Marcin")))
// Error: could not find implicit value for
// parameter P: LoggedOut.type ** LoggedInActions
import Nat._
implicit def mkDepVect[N <: Nat, A]: N ** Vect[N, A] = DType()
val anyVec = DPair(_2, (1 :: 2 :: NIL)) //OK
val noCheating = DPair(_3, (1 :: 2 :: NIL)) //does not compile
//Error:(53, 24) could not find implicit value for
//parameter P: Nat._3 ** Vect[Nat._2,Int]
So theoretically we could represent vectors of length dependent on a "variable" in Scala. Unfortunately, ...
data Nat = Z | S Nat
-- S ( S (Z) )
-- 2 : Nat
-- n: Nat = 500
-- n : Nat = 500
readNat: IO (Maybe Nat)
readNat = do input <- getLine
pure (parsePositive input)
tail : Vect (S len) elem -> Vect len elem
tail (x::xs) = xs
sealed trait Nat {
type N <: Nat
}
case object Z
extends Nat {type N = Z.type}
case class S[P <: Nat]()
extends Nat {type N = S[P]}
object Nat {
//...
def apply(i: Int): Option[Nat] = {
@scala.annotation.tailrec
def succ(j: Int, n: Nat = _0): Nat = {
val s = S[n.N]
if (j == 0) s else succ(j - 1, s)
}
if (i < 0) None
else if (i == 0) Some(_0)
else Some(succ(i - 1))
}
}
alternatively - but: no type preservation and we can't pattern match on type level anyway
scala> u.weakTypeOf[_2.N]
res1: reflect.runtime.universe.Type = Nat._2.N
scala> res1.dealias
res2: reflect.runtime.universe.Type = S[Nat._1]
scala> implicitly[IsSucc[_2.N]]
res16: IsSucc[Nat._2.N] = IsSucc$$anon$1@24a6b595
scala> val n = Nat(2).get
n: Nat = S()
scala> u.weakTypeOf[n.N]
res3: reflect.runtime.universe.Type = n.N
scala> res3.dealias
res4: reflect.runtime.universe.Type = n.N
scala> val m: Nat = _2
m: Nat = S()
scala> u.weakTypeOf[m.N]
res5: reflect.runtime.universe.Type = m.N
scala> res5.dealias
res6: reflect.runtime.universe.Type = m.N
scala> implicitly[IsSucc[m.N]]
error: could not find implicit value for parameter e: IsSucc[m.N]
this is the bridge you can't cross: if you ever loose your singleton type, compiler won't be able to keep track of all the structure - you won't recover it from a runtime value
In Scala you can't assert that a type structure is to be deduced from a runtime structure, while in Idris you can.
Even though you can express dependent vectors in Scala, it is not practical because you can only express them if their length is statically known (not really the case for vectors) and give up all the transformations that can't keep track of size (ie. filter)
data Format = Number Format
| Str Format
| Lit String Format
| End
PrintfType : Format -> Type
PrintfType (Number fmt) = (i : Int) -> PrintfType fmt
{- ... -}
PrintfType (Lit str fmt) = PrintfType fmt
PrintfType End = String
printfFmt : (fmt : Format) -> (acc : String) -> PrintfType fmt
printfFmt (Number fmt) acc = \i => printfFmt fmt (acc ++ show i)
{- ... -}
printfFmt End acc = acc
toFormat : (xs : List Char) -> Format
toFormat [] = End
toFormat ('%' :: 'd' :: chars) = Number (toFormat chars)
{- ... -}
printf : (fmt : String) -> PrintfType (toFormat (unpack fmt))
printf fmt = printfFmt _ ""
data Format = Number Format
| Str Format
| Lit String Format
| End
PrintfType : Format -> Type
PrintfType (Number fmt) = (i : Int) -> PrintfType fmt
{- ... -}
PrintfType (Lit str fmt) = PrintfType fmt
PrintfType End = String
printfFmt : (fmt : Format) -> (acc : String) -> PrintfType fmt
printfFmt (Number fmt) acc = \i => printfFmt fmt (acc ++ show i)
{- ... -}
printfFmt End acc = acc
toFormat : (xs : List Char) -> Format
toFormat [] = End
toFormat ('%' :: 'd' :: chars) = Number (toFormat chars)
{- ... -}
printf : (fmt : String) -> PrintfType (toFormat (unpack fmt))
printf fmt = printfFmt _ ""
value is seamlessly lifted into type
*> :t printf "%s = %d"
printf "%s = %d" : String -> Int -> String
*> :t printf "%s = %f"
printf "%s = %f" : String -> Double -> String
data Format = Number Format
| Str Format
| Lit String Format
| End
PrintfType : Format -> Type
PrintfType (Number fmt) = (i : Int) -> PrintfType fmt
{- ... -}
PrintfType (Lit str fmt) = PrintfType fmt
PrintfType End = String
printfFmt : (fmt : Format) -> (acc : String) -> PrintfType fmt
printfFmt (Number fmt) acc = \i => printfFmt fmt (acc ++ show i)
{- ... -}
printfFmt End acc = acc
toFormat : (xs : List Char) -> Format
toFormat [] = End
toFormat ('%' :: 'd' :: chars) = Number (toFormat chars)
{- ... -}
printf : (fmt : String) -> PrintfType (toFormat (unpack fmt))
printf fmt = printfFmt _ ""
bridge you can't cross (macro ?)
sealed trait Format
case class Num[F <: Format](rest: F)
extends Format
//...
case class Str[F <: Format](rest: F)
extends Format
case class Lit[F <: Format](string: String, rest: F)
extends Format
case object End extends Format
trait PrintfType[F <: Format] {
type Out
def apply(fmt: F): String => Out
}
object PrintfType {
type Result[F <: Format, Out0] = PrintfType[F] { type Out = Out0 }
implicit val endFmt: Result[End.type, String] = new PrintfType[End.type] {
type Out = String
def apply(fmt: End.type) = identity
}
// ...
}
//...
implicit def numberFmt[Fmt <: Format, Rest](
implicit printfFmt: Result[Fmt, Rest])
: Result[Num[Fmt], Int => Rest] =
new PrintfType[Num[Fmt]] {
type Out = Int => Rest
def apply(fmt: Num[Fmt]) =
(acc: String) => (i: Int) => printfFmt(fmt.rest)(acc + i)
}
//...
implicit def litFmt[Fmt <: Format, Rest](
implicit printfFmt: Result[Fmt, Rest]): Result[Lit[Fmt], Rest] =
new PrintfType[Lit[Fmt]] {
type Out = Rest
def apply(fmt: Lit[Fmt]) =
(acc: String) => printfFmt(fmt.rest)(acc + fmt.string)
}
def apply[F <: Format](format: F)(
implicit printfType: PrintfType[F]): printfType.Out =
printfType(format)("")
val fmt1 = Str(Lit(" = ", Num(End)))
val t1: String => Int => String = PrintfType(fmt1)
println (t1("answer")(10))
val fmt2 = Str(Lit(" = ", Dbl(End)))
val t2: String => Double => String = PrintfType(fmt2)
println (t2("answer")(10.0))
sprintf looks more practical because you can enjoy increased type-safety with no downside being limited to String literals (but you must employ a macro to parse format string)
iterato.rs
medium.com/iterators
slides from this presentation are available here: goo.gl/JsXGB6
Thanks for coming