Idris for (im)practical Scala programmers
"There is nothing more practical than a good theory"
- Kurt Lewin
It's all about how expressive type system we want/need/can live with - where is the point of impracticality?
"There is nothing more practical than a good theory"
- Kurt Lewin
It's all about how expressive type system we want/need/can live with - where is the point of impracticality?
Two theoretical tools for exploring type systems
- λ-cube - visual
- PTS (Pure Type Systems) framework - definitional
λ-cube
λ-cube
Visualizes three "dimensions" of type-system abstraction. What is this abstraction? Informally, it's mutual dependency of types and terms:
- term - term (typed lambda calculi)
- terms - type (polymorphism)
- type - type (higher-order types)
- type - term (dependent types)
Relating abstractions
Term - term
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)
Relating abstractions
Term - type
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)
Relating abstractions
Type - type
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
Relating abstractions
Type - term
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)
λ-cube
↑ - adds term-type
→ - adds type-term
↗ - adds type-type
\( \lambda ^ \to \) examples
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
\( \lambda \underline{\omega} \)
Let's add type operators to simply-typed lambda calculi. We do this by allowing type-type dependency.
Thus, we can derive complex types (but, we cannot use them generically because term-level polymorphism is not derivable in this type system)
\( \lambda \underline{\omega} \) examples
//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!
System F
Polymorphism is added by allowing term-type dependency. With this rule, we obtain the famous System F (Hindley-Milner is a subset of System F)
Type inference is undecidable in system F, whereas in HM one can always infer the most general type of a given program
System 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)
-}
System Fω
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.
Type checking is undecidable in Fω. (Type checking of normalizing terms is decidable, but you can't know beforehand)
System Fω - examples
Proof of A&B=>A 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)
System Fω
System Fω allows universal quantification, abstraction and application at higher kinds.
Type systems of languages like Scala and Haskell deliver slightly less than Fω
PTS
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).
PTS
PTS
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-type $$
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
-}
PTS
$$ \Box $$ describes type kinds ie. types of type constructors, like: $$ \star \rightarrow \star \rightarrow \star $$
Sorts - types and kinds
In our type systems we may want to introduce following abstractions:
- \( (\lambda m:A.Fm): (A \to B) \) where \( F:A \to B \)
- \( (\lambda \alpha : \star . \lambda a:\alpha.a): (\Pi \alpha : \star . (\alpha \to \alpha)) \) (polymorphic identity function)
- \( (\lambda \alpha : \star . \alpha \to \alpha):(\star \to \star) \) (polymorphic Id type)
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
PTS
PTS
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
\( \lambda ^ \to \)
Only term-term dependencies are allowed. The only type constructor is function type
\( \lambda ^ \to \) examples
The following can be derived
- \( A:\star , B:\star, b:B \vdash (\lambda a:A.b):B \)
- \( A:\star , B:\star, b:B, c:A \vdash ((\lambda a:A.b)c):B \)
- \( A:\star, B:\star \vdash (\lambda a:A.\lambda b:B.a):((A \to B) \to A) \)
\( \lambda \underline{\omega} \)
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:
\( \lambda \underline{\omega} \) examples
The following can be derived
- \( \vdash (\lambda \alpha :\star.\alpha ):( \star \to \star ):\Box \)
- \( \alpha:\star , f:\star \to \star \vdash f\alpha:\star \)
- \( \alpha:\star \vdash (\lambda f:\star \to \star.f\alpha):((\star \to \star) \to \star) \)
System F
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)
System F examples
The following can be derived
- \( \alpha : \star \vdash (\lambda a:\alpha .a ):( \alpha \to \alpha ) \)
- \( \vdash (\lambda \alpha : \star.\lambda a:\alpha .a ): \Pi \alpha : \star . \alpha \to \alpha : \star \)
- \( A:\star , b:A \vdash (\lambda \alpha : \star.\lambda a:\alpha .a )Ab:A \)
System Fω
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ω - examples
λ-cube
We haven't yet explored the possibility of allowing \( (\star , \Box) \) rule. This enables type-term dependency.
Dependent typing
Motivations (things we are not able to express):
- collections of known length
- type of dates where the range of the day is restricted according to the month
- functions like sprintf
- type-safe structural correctness (eg. compile-time balanced trees)
Dependent typing
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?
"(...) the combination of singleton types, path-dependent types and implicit values means that Scala has surprisingly good support for dependent typing"
- Miles Sabin
Idris vs Scala
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
Idris vs Scala
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
Idris vs Scala
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
Idris vs Scala
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"
Idris vs Scala
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!
Idris vs Scala
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 {
//...
rewrite def toNat(n: Int): Nat = rewrite n
match {
case 0 => Z
case n if n > 0 => S(toNat(n - 1))
}
rewrite def toInt[N <: Nat]: Int =
rewrite erasedValue[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 rewrite functions / literals only!
Idris vs Scala
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
Idris vs Scala
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
Idris vs Scala
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)
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
Idris vs Scala
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)
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]()
}
}
encode computation as implicit search
Idris vs Scala
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)
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
Idris vs Scala
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S 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
Exercise left to the reader :-)
Idris vs Scala
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 }
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
}
Idris vs Scala
data Fin : (n : Nat) -> Type where
FZ : Fin (S k)
FS : Fin k -> Fin (S 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)
*/
Idris vs Scala
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)
}
Idris vs Scala
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
Idris vs Scala
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
Idris vs Scala
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
Idris vs Scala
*> :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
^
Questions?
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)!
Σ type
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)
Σ type
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
Σ 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
}
Σ type - Scala examples
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, ...
If you recall this slide ...
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
... this means that
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
Scala vs Idris
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)
Scala vs Idris
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
Scala vs Idris
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 ?)
Scala vs Idris
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
}
// ...
}
Scala vs Idris
//...
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))
Scala vs Idris
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)
That's all Folks!
iterato.rs
medium.com/iterators
slides from this presentation are available here: https://bit.ly/2IXhhb0
Thanks for coming
Idris for impractical Scala programmers
By Marcin Rzeźnicki
Idris for impractical Scala programmers
- 1,130