Learning Elm Part 1

Types and Conditionals

What Is A Type?

  • A type is a name for a collection of related values. For example, in Elm the basic type

        Bool
    
  • contains the two logical values

        False      True
    

What Is A Type?

In Elm, every value, variable and function has a type. Ex

  x : Int
  x = 1

  y : Float
  y = 2.0

Primitive Types

Elm has the following primitive (i.e., built-in) types

True : Bool
"Hello" : String
'c' : Char
5 : Int
3.1415 : Float

Type Errors

  • Elm is strictly typed (every variable has a type that cannot change)
  • The following is a type error
5 + "Hello"
    -- TYPE mismatch
    -- I cannot do addition with String values like this one

Types in Elm

  • For every expression e, it has a type t, written as

    e : Type
    
  • It’s good practice (although not necessary) to provide a type annotation above each declaration

    f : Int -> Int
    f x = x + 1
    

Lists

Every list type has another type (the type it contains) attached to it

xs : List Int
xs = [1,2,3]

ys : List Char
ys = ['a','b']

zs : List (List Float)
zs = [[1.0],[2.0,3.0]]

Tuples vs Lists

  • A List is a sequence of values of the same type, Ex:

              [1,'a',"String"]
                  Error
    
  • A tuple, specified using brackets, is a sequence of values that can be of different types. Ex:

              (1,'a',"String")
    

Tuple and List Types

  • A Lists type is specified by brackets, and the type of the elements it contains. Ex:

              ['a','b','c','d']  : List Char
              [True,False,True]  : List Bool
    
  • Because tuples can contain different types, the tuples type specifies each element. Ex:

        (False,True)        : (Bool,Bool)
        ('a',True,['b',c']) : (Char,Bool,List Char)
    

Strings

  • There is a special type String that is used to group Char values using quotations, Ex:

      "Some String"
    
  • You can turn any value into a String using
Debug.toString x

Function Types

  • A function takes arguments of certain types and returns a value of a certain type.
  • In a functions type, this is specified using the \(\rightarrow\) operator, Ex:

    not     : Bool -> Bool
    head    : List a  ->  Maybe a
    
  • Note: a is not a specific type, we’ll talk about this next tutorial

Type Signatures

When writing your own functions, you should always specify a functions type beforehand

fst   : (a,b) -> a
fst (x,y) = x

snd   : (a,b) -> b
snd (x,y)  = y

Conditional Expressions

  • If expressions allow you to define control structures over Bool values. They are defined with the following syntax

        if Bool then val1 else val2
    
  • For example,

        dbzDialogue : Int -> String
        dbzDialogue powerLevel = if powerLevel < 9000
                                    then "Lame..."
                                    else "ITS OVER 9000!!!"
    

Nested Expressions

Conditional expressions can be nested (put inside another conditional expression).

signum : Int -> Int
signum n = if n < 0 then -1 else
                if n == 0 then 0 else 1

signum takes an integer n, and returns \(-1\) if n is negative, 0 if n is equal to zero, and 1 if n is positive

Note on If Expression

if expressions are just that - EXPRESSIONS. This means

  • every if expression has a type

            (if True then 0 else 1) : Char
    
  • Every if has an else

            if True then 0 -- Syntax Error
    
  • The then and else cases must return the same type

Pattern Matching

Many functions have a particularly clear definition using Pattern Matching with a case expression

not    : Bool -> Bool
not x  = case x of
                True -> False
                False -> True

Pattern Matching on Multiple Variables

  • To pattern match on multiple variables, you can group them togther in a tuple. For Example:
and : Bool -> Bool -> Bool
and x y = case (x,y) of
            (True,True) -> True
            (False,False) -> False
            (True,False) -> False
            (False,True) -> False

Wild Cards

This can be defined more compactly using the wildcard operator \((\_)\), which means any value.

and x y = case (x,y) of
                (True,b) -> b
                (False,_) -> False

Challenge

Try implementing logical or, which is True if either of its arguemnts are True

Pattern Matching - Order Matters

  • Patterns are matched in order. For example, the following definition returns False no matter what.
and x y = case (x,y) of
                (_,_) -> False
                (True,True) -> True
  • Patterns may not repeat variables. The following gives an error:
and x y = case (x,y) of
                (b,b) -> b
                (True,True) -> True

Polymorphic Functions

A function is called polymorphic (“of many forms”) if its type contains one or more type of variables.

        length :: List a -> Int

The function length takes a list and returns its size. a can be any type. For example, in

        length [False,True]   -- a is Bool
        length [1,2,3,4]      -- a is Int

This allows the function length to work on any type of list

Polymorphic Functions: Examples

-- takes a list of any type and returns a list of
-- that same type
reverse :: List a -> List a

-- takes a list of any type and returns a value of
-- that same type
head :: List a -> a

-- takes a function of any type to any other type,
-- a list of the input type of the function
-- and return a list of the output type of the function
map :: (a -> b) -> List a -> List b

Constrained Type Variables

  • Elm has a few special type variables, the most common one is number which could be substituted for Float and Int
(+) : number -> number -> number
  • There is also appendable which could be substituted for String and List a
(++) : appendable -> appendable -> appendable

Curried Functions

Currying is the process of transforming a function that takes multiple arguments into a function that takes a single argument, for example:

add  :: Int -> Int -> Int
add x y = x + y

-- is the same as
add  :: Int -> (Int -> Int)
add x y = x + y

-- So, calling add on only its first argument
add5  :: Int - > Int
add5 = add 5

-- returns a function with only one argument

Curried Functions

  • In Elm, functions are curried by default
  • Warning Foreshadowing currying is particularly useful for working with Higher Order Functions

      add x y = x + y
      List.map (add 1) [1,2,3,4]
    

Uncurried Functions

  • It’s possible to specify an uncurried function through use of a tuple
  • For Example:
add  :: (Int,Int) -> Int
  • What does the implementation of add look like?
add (x,y) = x + y

List Patterns

  • Internally, every non empty list is constructed by use of an operator \((::)\) called cons that adds an element to the start of a list.

                [1,2,3,4] == 1::(2::(3::(4::[])))
    
  • Functions can be defined using \(x::xs\) patterns::
head       : List a -> Maybe a
head list = case list of
                (x::xs) -> Just x
                [] -> Nothing

-- Challenge: how to define tail

List Patterns

  • You can extend list patterns beyond the first element
sndHead     : List a -> Maybe a
sndHead list = case list of
                   (x0::x1::xs) -> Just x1
                   _ -> Nothing

Lambda Expressions

  • Functions can be constructed without a name by using lamda expressions
            \x -> x + x
  • Lambda expressions are useful when defining functions that return a function as a result
-- Definition 1
add x y = x+y
-- Definition 2
add = \x -> (\y -> x + y)

Map

Map takes a function and a list and applies that function on each element in a list, returning another list

add1    : Int -> Int
add1 x  = x + 1

sum1    : List Int -> List Int
sum1 xs = map add1 xs

So,

        sum1 [1,2,3] == [2,3,4]

Map and Lambda Expressions

  • Defining a function only to be used in another function seems a little unnecessary. This is where lambda expressions come in.
  • sum1 can be more efficiently defined as::

                sum1 xs = map (\x -> x + 1) xs
    

Defining Map (Intro to Recursion)

  • In order to define map, we need to recurse through it’s input list

        map : (a -> b) -> List a -> [b]
        map f list = case list of
                         (x::xs) -> f x :: map f xs
                         []      -> []
    
  • Consider the following evaluation

        map (1+) [1,2,3] => map (1+) (1::2::3::[])
                        => (1+1)::(map (1+) (2::3::[]))
                        => (1+1)::(1+2)::(map (1+) (3::[]))
                        => (1+1)::(1+2)::(1+3)::(map (1+) [])
                        => (1+1)::(1+2)::(1+3)::[]
                        => [2,3,4]