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,640