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
In Elm, every value, variable and function has a type. Ex
x : Int
x = 1
y : Float
y = 2.0
Elm has the following primitive (i.e., built-in) types
True : Bool
"Hello" : String
'c' : Char
5 : Int
3.1415 : Float
5 + "Hello"
-- TYPE mismatch
-- I cannot do addition with String values like this one
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
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]]
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")
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)
There is a special type String that is used to group Char values using quotations, Ex:
"Some String"
Debug.toString x
In a functions type, this is specified using the \(\rightarrow\) operator, Ex:
not : Bool -> Bool
head : List a -> Maybe a
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
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!!!"
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
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
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
and : Bool -> Bool -> Bool
and x y = case (x,y) of
(True,True) -> True
(False,False) -> False
(True,False) -> False
(False,True) -> False
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
Try implementing logical or, which is True if either of its arguemnts are True
and x y = case (x,y) of
(_,_) -> False
(True,True) -> True
and x y = case (x,y) of
(b,b) -> b
(True,True) -> True
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
-- 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
(+) : number -> number -> number
(++) : appendable -> appendable -> appendable
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
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]
add :: (Int,Int) -> Int
add (x,y) = x + y
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::[])))
head : List a -> Maybe a
head list = case list of
(x::xs) -> Just x
[] -> Nothing
-- Challenge: how to define tail
sndHead : List a -> Maybe a
sndHead list = case list of
(x0::x1::xs) -> Just x1
_ -> Nothing
\x -> x + x
-- Definition 1
add x y = x+y
-- Definition 2
add = \x -> (\y -> x + y)
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]
sum1 can be more efficiently defined as::
sum1 xs = map (\x -> x + 1) xs
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]