Major features
- why need type systems: introduce abstraction, ignore implementation details
- strong
- an ill typed expression will raise a type error at compile time
- no implicit coercion (e.g., type casting from int to float) for function input
- static
- the compiler knows the type of every value and expression at compile time
- the combination of strong and static typing makes runtime type error impossible
- type inference: the compiler can deduce the types of almost all expressions, explicitly declare the type is optional
Basic types
- Char: Unicode character
- Bool: True or False
- Int: signed, fixed-width integer values
- Integer: signed integer of unbounded size, not used as often as Int due to expensive performance and space consumption
- Double: floating-point numbers, is typically 64 bits wide, Float is discouraged and slower than Double
:: MyTypeis type signature, if omitted, the compiler will infer the type
Function
- function application has higher precedence than using operators
- the parentheses in function arguments is necessary, e.g.,
compare (sqrt 3) (sqrt 16) - function application is left-associative
add a b = a + b, lhs is the name of the function and arguments, rhs is the body of the function- use
:load add.hsto useaddfunction - use
:cdto change the working directory, (but will lose all loaded modules in current directory) - functions don’t have
return, as a function is a single expression, not a sequence of statements =means “meaning”, the lhs is defined by the expression on the rhs
Composite data types
Lists
headreturns the first element of a listtailreturns all but the head of a list, but cannot be applied to an empty list- the list type is polymorphic as the values in a list can have any type, use a lowercase letter to declare a type variable, e.g.,
[a] - recursively substitute the type variable a specific type, e.g.,
[[Int]] - the for loop in imperative languages are achieved by traversing a list recursively
take 2 [1,2,3,4,5]returns the first 2 elements of the listdrop 3 [1,2,3,4,5]returns all but the first 3 elements of the listnullchecks whether a list is empty
Tuples
- tuples are fixed-size collections of values, each value can have a different type, e.g.,
(True, "hello") ()is a tuple of 0 elements, it has one element also written as(), pronounced as “unit” (similar to void in C)- there is no 1-element tuple
- a tuple’s type is represented by the number, positions and the types of its elements, so
(False, 'a')and('a', False)are tuples of different types - often use tuples to return multiple values of a function
fst (1, 'a')andsndfunctions return the first and second element of a pair
Function types
:type linesreturns:: String -> [String], meanslineshas the type String to list-of-String- side-effect means there is a dependency between the global state and the function behavior, essentially, side effects are invisible inputs to or outputs from functions
- Most Haskell functions are pure functions to not have side effects, e.g., the result of a function depends only on the explicit inputs
- impure functions have IO return type, e.g.,
: type readFilereturnsreadFile :: FilePath -> IO String
Variable
- a variable gives a name to an expression, once a variable is bound to an expression, its value does not change
1 2
x = 10 x = 11 -- compiler error due to multiple declarations of x
Conditional evaluation
1
2
3
4
myDrop n xs =
if n <= 0 || null xs -- predicate of type Bool
then xs -- indentation is important to continue the existing definition instead of starting a new one
else myDrop (n - 1) (tail xs) -- two branches must have the same type, else cannot be omitted
Lazy evaluation
- in
isOdd (1 + 2), the sub-expression1 + 2is not evaluated, instead, it creates a thunk to record the unevaluated expression and defer the actual evaluation until it’s really needed - the evaluation of
myDrop 2 "abcd"- bind
nto2and bindxstoabcd - evaluate the predicate
2 <= 0 || null "abcd" - evaluate
2 <= 0to False, therefore need to evaluatenull "abcd" null "abcd"evaluate to False,False || Falseevaluates to False, start to evaluateelsebranch- evaluate
myDrop (2 - 1) (tail "abcd"), note even if2-1is not evaluated at this point - bind
nto2-1and bindnstotail "abcd" - evaluate
(2-1) <= 0 || null (tail "abcd"), evaluate2 -1as 1 and1 <= 0as False - evaluate
tail "abcd"asbcdand evaluatenull "bcd"as False, still evaluateelsebranch - …
- in the second recursion application, bind
nto1-1andxstotail "bcd", t0 <=0evaluates to True, so thethenbranch is evaluates myDrop (1 - 1) (tail "bcd") = tail "bcd"- return from the first recursive application, substitute
else myDrop (n-1) (tail xs)withelse (tail "bcd") myDrop (2-1) (tail "abcd") = tail "bcd", return from the original applicationmyDrop 2 "abcd" = tail "bcd"- in the end,
myDrop 2 "abcd"returnstail "bcd", if it isprint (myDrop 2 "abcd"), it will then evaluatetail "bcd"
- bind
Polymorphism
last :: [a] -> ameans the function is polymorphic,ais a type variable, means the list can be of any type- when applying
lasta list of Char, the compiler substitutesCharfor eacha, and the last now has the type[Char] -> Char, this is parametric polymorphism (e.g., a function can have parameters that can be later bound to real values) - other polymorphism
- subtype polymorphism is widely used in object-oriented languages, where a base class defines a set of behaviors that its subclasses can modify and extend
- coercion polymorphism allows a value of one type to be implicitly converted into another type
- parametric polymorphism makes the real type inaccessible, e.g., the function type
: (a, b) -> acan only return the first element of a pair
Function with more than 1 argument
take :: Int -> [a] -> [a]isInt -> ([a] -> [a])(-> is right associative) means it takes an Int and returns another function- it can also be understood as it takes two arguments and returns a list