Idris for Scala developers

{slides,github}.com/cb372/idris-for-scala-devs

Install Idris

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

  1. touch foo.idr
  2. open foo.idr in your editor
  3. write one line: 
  4. press <leader>-d (in vim)
  5. 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

  • 1,586
Loading comments...

More from Chris Birchall