Idris for Scala developers
{slides,github}.com/cb372/idris-for-scala-devs
Install Idris
Mac:
brew install idris
Please install the editor plugin for vim, emacs, Sublime, Atom, VS Code, ... (but not IntelliJ!)
Test your installation
$ idris
____ __ _
/ _/___/ /____(_)____
/ // __ / ___/ / ___/ Version 1.1.1
_/ // /_/ / / / (__ ) http://www.idris-lang.org/
/___/\__,_/_/ /_/____/ Type :? for help
Idris is free software with ABSOLUTELY NO WARRANTY.
For details type :warranty.
Idris> 1 + 1
2 : Integer
Test your editor
- touch foo.idr
- open foo.idr in your editor
- write one line:
- press <leader>-d (in vim)
- another line should magically appear
foo : String -> Int
foo : String -> Int
foo x = ?foo_rhs
Idris
- Pure-functional Haskell-like language
- First-class, dependent types
- Supports multiple codegen backends
- Executable binary via C
- JS
- JVM
- ...
Interesting features
- Interactive editing
- First class, dependent types
- Proofs
- Codata and infinite data structures
- Totality checking
- Strictness and laziness
Interactive editing
Key binding (vim) | Description |
---|---|
<leader>-d | Write a skeleton definition for the type signature under the cursor |
<leader>-t | Show type of the thing under the cursor |
<leader>-c | Write a case split on the variable under the cursor |
<leader>-o | Proof search, i.e. guess a value to replace a hole |
(there are others, but I use these the most)
Interactive editing
In the editor, write a function type signature:
tailOrEmptyList : List a -> Bool -> List a
With the cursor over the function name, use <leader>-d to add a skeleton definition:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList xs x = ?tailOrEmptyList_rhs
Interactive editing
With the cursor over the hole,
use <leader>-t to see some types:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList xs x = ?tailOrEmptyList_rhs
^
a : Type
xs : List a
x : Bool
--------------------------------------
tailOrEmptyList_rhs : List a
Interactive editing
With the cursor over xs,
use <leader>-c to case split on that variable:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList xs x = ?tailOrEmptyList_rhs
^
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = ?tailOrEmptyList_rhs_1
tailOrEmptyList (y :: xs) x = ?tailOrEmptyList_rhs_2
Interactive editing
With the cursor over the first hole,
use <leader>-o to guess an implementation:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = ?tailOrEmptyList_rhs_1
^
tailOrEmptyList (y :: xs) x = ?tailOrEmptyList_rhs_2
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = []
tailOrEmptyList (y :: xs) x = ?tailOrEmptyList_rhs_2
Wow, the compiler just wrote some code for us!
Interactive editing
Now do a case-split on x using <leader>-c:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = []
tailOrEmptyList (y :: xs) x = ?tailOrEmptyList_rhs_2
^
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = []
tailOrEmptyList (y :: xs) False = ?tailOrEmptyList_rhs_1
tailOrEmptyList (y :: xs) True = ?tailOrEmptyList_rhs_3
Interactive editing
Finally we write the rest of the implementation manually and tidy up the patterns a bit:
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] x = []
tailOrEmptyList (y :: xs) False = ?tailOrEmptyList_rhs_1
tailOrEmptyList (y :: xs) True = ?tailOrEmptyList_rhs_3
tailOrEmptyList : List a -> Bool -> List a
tailOrEmptyList [] _ = []
tailOrEmptyList (x :: xs) False = []
tailOrEmptyList (x :: xs) True = xs
Dependent types
Refresher: path-dependent types in Scala
case class Flight(id: String) {
case class Seat(id: String)
def seat(id: String) = Seat(id)
def book(seat: Seat): Unit =
println(s"OK, booked seat ${seat.id}")
}
val outwardFlight = Flight("BA007")
val returnFlight = Flight("BA008")
val outwardSeat = outwardFlight.seat("37A")
val returnSeat = returnFlight.seat("37A")
println(outwardSeat == returnSeat) // false
outwardFlight.book(outwardSeat) // OK
//outwardFlight.book(returnSeat) // doesn't compile
Refresher: path-dependent types in Scala
trait ResourceFactory {
type T
def open(name: String): T
}
object URLResources extends ResourceFactory {
type T = java.net.URL
def open(name: String) = new java.net.URL(name)
}
object FileResources extends ResourceFactory {
type T = java.io.File
def open(name: String) = new java.io.File(name)
}
def openResource(name: String, factory: ResourceFactory): factory.T =
factory.open(name)
openResource("foo.txt", FileResources) // returns a File
First class types
val x = String
x : Type
x = String
Scala
Idris
error: object java.lang.String is not a value
First class types
Functions can take types as arguments
(although you can't do much with them)
Functions can return types as results
(this is much more interesting)
foo : Type -> Bool
foo _ = False
foo : Bool -> Type
foo True = String
foo False = Int
Dependent types
In other words, types can be computed.
Types can depend on values.
foo : Bool -> Type
foo True = String
foo False = Int
Silly example
Let's write a function that decides whether a given value is less than 3
But we'll make type safety optional:
data Preference = StronglyTyped | StringlyTyped
sealed trait Preference
case object StronglyTyped extends Preference
case object StringlyTyped extends Preference
equivalent in Scala
stringly-typed/stringly.idr
Silly example
write functions to calculate the input and output types based on the preference:
inputType : Preference -> Type
inputType StronglyTyped = Integer
inputType StringlyTyped = String
outputType : Preference -> Type
outputType StronglyTyped = Bool
outputType StringlyTyped = String
Silly example
type signature of our function:
isLessThanThree : (pref : Preference) ->
(inputType pref) ->
(outputType pref)
Silly example
implement using pattern matching:
isLessThanThree StronglyTyped x = x < 3
isLessThanThree StringlyTyped x = boolToString verdict where
verdict : Bool
verdict = x == "zero" ||
x == "one" ||
x == "two" ||
pack(take 5 (unpack x)) == "minus"
boolToString : Bool -> String
boolToString False = "false"
boolToString True = "true"
Proper example: Vector
data Nat =
Z |
S Nat
First we'll need natural numbers:
Idris> Z
0 : Nat
Idris> S(Z)
1 : Nat
Idris> S(S(Z))
2 : Nat
Proper example: Vector
data Vect : (len : Nat) -> (elem : Type) -> Type where
Nil : --TODO
(::) : --TODO
number of elements
type of elements
vect/vect.idr
Proper example: Vector
data Vect : (len : Nat) -> (elem : Type) -> Type where
Nil : Vect Z elem
(::) : (x : elem) ->
(xs : Vect len elem) ->
Vect (S len) elem
*vect> the (Vect 2 Int) (3 :: 4 :: Nil)
[3, 4] : Vect 2 Int
*vect> the (Vect 1 Int) (3 :: 4 :: Nil)
(input):1:21-23:When checking argument xs to constructor Main.:::
Type mismatch between
Vect (S n) elem (Type of x :: xs)
and
Vect 0 Int (Expected type)
Specifically:
Type mismatch between
S n
and
0
Proper example: Vector
Try implementing the following functions for working with Vectors:
- head/tail
- take/drop
- (++) to concat two vectors
- index (gets the element at index i)
- Hint: import Data.Fin
- Fin represents a finitely bounded natural number
Encoding state transitions in types
A typical Scala World attendee:
Sleeping
Hiking
Scala-ing
wake up
go hiking
go to bed
go to bed
How could we encode this situation in Scala?
- Pattern match on current state
- Use phantom types
Sleeping
Hiking
Scala-ing
wake up
go hiking
go to bed
go to bed
scala-world-attendee/scala/
In Idris we have a similar choice
- Pattern match and return an Either
- Use a proof of valid state transition
Sleeping
Hiking
Scala-ing
wake up
go hiking
go to bed
go to bed
scala-world-attendee/idris/
Proofs
Idris is not a full-blown theorem prover,
but has support for proving things about your programs
addingZeroChangesNothing : (n : Nat) -> n + 0 = n
addingZeroChangesNothing Z = Refl
addingZeroChangesNothing (S k) = cong {f=S} (addingZeroChangesNothing k)
Proofs are used to:
- Convince the compiler of something that's true but only obvious to a human
- Add constraints to your types
- Define pre/post-conditions for functions
- Encode your assumptions
Exercise: write a function that inserts a value into a sorted list.
The function should require a proof that the list is sorted.
sorted/sorted.idr
Codata and infinity
Support for infinite data structures like Stream is built into the language with the keyword
codata
lists : Int -> List Int -> Stream (List Int)
lists n xs = xs :: (lists (n+1) (xs ++ [n]))
take 5 (lists 1 [])
-- [[], [1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] : List (List Int)
codata Stream : Type -> Type where
(::) : (e : a) -> Stream a -> Stream a
Exercise: construct an infinite binary tree
1
2 2
3 3 3 3
...
Totality checking
In Scala we get (limited) pattern match exhaustivity checking:
sealed trait A
case object B extends A
case object C extends A
def whatever(x: A): String = x match {
case B => "yeah"
//case C => "wow"
}
warning: match may not be exhaustive. It would fail on the following input: C
Idris provides more comprehensive totality analysis
e.g. pattern matching on integers:
total
whatever : Int -> String
whatever 1 = "wow"
whatever 5 = "yeah"
Main.whatever is not total as there are missing cases
Totality checking
e.g. checking for infinite recursion:
mutual
total
neverending : Int -> Int
neverending x = story (x - 1)
story : Int -> Int
story x = neverending (x - 1)
Main.neverending is possibly not total due to recursive path Main.neverending --> Main.story --> Main.story
Totality checking
Exercise: fix the code in total/fib.idr to make the function total
Strictness and laziness
Like Scala, and unlike Haskell, Idris is eager by default
Can mark an argument as Lazy,
similarly to in Scala
x: => Int
ifThenElse : Bool -> Lazy a -> Lazy a -> a
ifThenElse True t e = t
ifThenElse False t e = e
Idris for Scala developers
By Chris Birchall
Idris for Scala developers
- 3,973