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?

The Ascent to Idris

By Zainab Ali

The Ascent to Idris

Type level programming greatly increases the robustness of our code, but is difficult to express in Scala. Idris, a language where types are first class, is much better at expressing such concepts. But Scala has come a long way in the past few years. This talk takes a closer look at type level programming in Idris and Scala. We start with a basic problem defined using values, and slowly add types to it in both languages. By using singleton and refined types, as well as typelevel functions, we'll rise to the challenge of Idris

  • 3,463