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