The Ascent To Idris

Zainab Ali

@_zainabali_

val people : Map[String, Person] = ...

val peopleAndMe : Map[String, Person] = people.put(
      "Zainab" -> Person(...)
    )

...

// but I know I exist
val me : Option[Person] = peopleAndMe.get("Zainab")
// unsafe code
val me : Person = peopleAndMe("Zainab")

The Valley of Agony

  • All good programs should compile
  • All bad programs shouldn't
  • Sometimes a bad program will compile
  • Sometimes a good program won't

More powerful types

Idris

data MyDataType : Type where
   MyConstructor : (myLabel : String) -> MyDataType

myFunction : MyDataType -> String
myFunction (MyConstructor a) = a

myNumber : Nat
myNumber = 2
data Vect : Nat -> Type -> Type where
   Nil  : Vect 0 a
   (::) : a -> Vect n a -> Vect (n + 1) a 


dogsILike : Vect 2 String
dogsILike = "Schnauzer" :: "Beagle" :: Nil

dogsIHate : Vect 0 String
dogsIHate = Nil

-- Type error!
cats : Vect 1 String
cats = Nil

Types Dependent on Values

The Journey

  • Begin with values
  • Constraining types
  • Types dependent on types
  • Types dependent on values

The Problem : Accounts

  • Valid operations are credit / debit / transfer
  • Only integer amounts
  • Accounts must be created with a positive balance
  • Accounts can only be credited with a positive amount
  • Accounts can only be debited with a positive amount
  • Must record the number of operations
  • Transfers must preserve the total amount
case class Account(balance: Int, opCount: Int)

def create(initialBalance: Int): 
      BalanceShouldBePositive Either Account = ...

def credit(amount: Int, account: Account): 
      AmountShouldBePositive Either Account = ...

def debit(amount: Int, account: Account): 
      AmountShouldBePositive Either Account = ...

def transfer(amount: Int, 
             from: Account, 
             to: Account): 
      AmountShouldBePositive Either (Account, Account) = ...

In Scala

val right = create(100)
// has type AmountShouldBePositive Either Account

val left = create(0)
// has type AmountShouldBePositive Either Account
data Account : Type where
  MkAccount : (balance : Nat) -> (opCount : Nat) -> Account

create : Nat -> 
         Either BalanceShouldBePositive Account
create initialBalance = ...

credit : Nat -> 
         Account -> 
         Either AmountShouldBePositive Account
credit amount account = ...

debit : Nat ->
        Account -> 
        Either AmountShouldBePositive Account
debit amount account = ...

In Idris

transfer : Nat -> 
           Account -> 
           Account -> 
           Either AmountShouldBePositive (Account, Account)
transfer amount from to = ...

Constraining Types

  • Accounts must be created with a positive balance
  • Accounts can only be credited with a positive amount
  • Accounts can only be debited with a positive amount
create : (a: Nat) -> 
         (prf : a `GT` 0) -> 
         Account
create initialBalance _ = ...

In Idris

-- Type error!
create 0 ?whatProof
amount : Nat
amount = ...

case (isGT 0 amount) of
   Yes prf => create amount prf
   No _    => ...

Scala

libraryDependencies += "eu.timepit" %% "refined" % theVersion
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
def create(initialBalance: Int Refined Positive): 
      Account = ...
// Yay!
val account: Account = create(100)
// Fails!
val account: Account = create(-5)
val amount : Int = ...

val refinedPositive : String Either (Int Refined Positive) = 
      refineV[Positive](amount)

refinedPositive.map { positiveAmount =>
  create(positiveAmount)
}

Types Dependent On Types

  • Must record the number of operations
data Account : Type where
  MkAccount : (balance : Nat) -> 
              (opCount : Nat) -> Account

Idris

data Account : (opCount : Nat) -> Type where
  MkAccount : (balance : Nat) -> 
              (opCount : Nat) -> 
              Account opCount
fiveOps : Account 5
fiveOps = MkAccount 10 5
create : (a : Nat) -> 
         (prf : a `GT` 0) -> 
         Account 0
create initialBalance _ = ...

In Idris

-- Type error!
create : (a : Nat) -> 
         (prf : a `GT` 0) -> 
         Account 0
create initialBalance _ = MkAccount initialBalance 3
credit : (a : Nat) -> 
         Account n -> 
         (prf : a `GT` 0) -> 
         Account (n + 1)
credit amount account _ = ...

In Idris

In Scala

  • Literal types
  • Implicits
  • Singleton ops
5.type
case class Account[OpCount <: Int with Singleton](
             balance : Int
           )
case class Account(balance : Int, opCount: Int)
scalaOrganization := "org.typelevel"

scalacOptions += "-Yliteral-types"

Typelevel Scala

val fiveOps : Account[5] = Account[5](3)
def create(amount: Int Refined Positive): Account[0] = ...
def credit[N <: Int with Singleton](
      amount: Int Refined Positive, 
      account: Account[N]
    ) : ??? = ...
trait IncCount[A <: Account[_]] {
  type Out <: Account[_]
  
  def apply(account: A): Out
}

object IncCount {
  type Aux[A <: Account[_], Out0 <: Account[_]] = 
    IncCount[A] {
      type Out = Out0
    }
}

Typeclasses

implicit val incCount: IncCount.Aux[Account[0], Account[1]] =
  new IncCount[Account[0]] {

    type Out = Account[1]
    
    def apply(a: Account[0]): Out = Account[1](a.balance)
  }
def credit[N <: Int with Singleton](
      amount: Int Refined Positive, 
      account: Account[N])(
      implicit incCount: IncCount[Account[N]]
    ): incCount.Out = ...
libraryDependencies += "eu.timepit" %% "singleton-ops" % theVersion

Singleton Ops

import singleton.ops._
object IncCount {

  implicit def incCount[N <: Int with Singleton, 
                        IncN <: Int with Singleton](
    implicit op: OpInt.Aux[N + 1, IncN]
  ): Aux[Account[N], Account[IncN]] = 
    new IncCount[A] {

      type Out = Account[IncN]
      
      def apply(account: A): Account[IntN] = 
         Account(account.balance)
    }
}

Types Dependent on Values

  • Transfers must preserve the total amount
data Account : (opCount : Nat) -> Type where
  MkAccount : (balance : Nat) -> 
              (opCount : Nat) -> 
              Account opCount

In Idris

data Account : (opCount : Nat) -> 
               (balance : Nat) -> Type where
  MkAccount : (balance : Nat) -> 
              (opCount : Nat) -> 
              Account opCount balance
create : (a : Nat) -> 
         (prf : a `GT` 0) -> 
         Account 0 a
create initialBalance _ = ...

credit : (a : Nat) -> 
         Account n balance -> 
         (prf : a `GT` 0) -> 
         Account (n + 1) (balance + a)
credit amount account _ = ...

In Idris

-- Type error!
debit : (a : Nat) -> 
         Account n balance -> 
         (prf : a `GT` 0) -> 
         Account (n + 1) (balance - a)
debit amount account _ = ...

In Idris

In Idris

debit : (a : Nat) -> 
         Account n balance -> 
         (prf : a `GT` 0) -> 
         (prf1 : a `LTE` balance) ->
         Account (n + 1) (balance - a)
debit amount account _ _ = ...
transfer : (a : Nat) ->
           Account n nBalance ->
           Account m mBalance ->
           (prf : a `GT` 0) ->
           (prf1 : a `LTE` nBalance) ->
           (Account (n + 1) (balance - a),
            Account (m + 1) (balance + a))
transfer amount from to _ _ = ...

In Idris

In Scala?

Take a look at

Thank you!

Questions?