learnYouAHaskell - work from learning Haskell
Thank you Miran Lipovača!
You do computations in Haskell by declaring what something is, instead of declaring how to get to it.
- Start with
ghci
. - Load and compile a source code file into the process' address space -
:l <file_path>
. - Reload currently linked files -
:r
. - Display the type of a piece of data -
:t <data>
.<data>
can also be a function name.
- prefix which is like a traditional function name followed by arguments (
fx_name A B C
). The function is prefix to the data. - infix which is like an operator such as
A + B
. The function is inbetween the data. With backticks, we can usa a binomial function as an infix or even define infix functions(div A B
→A `div` B
). - Functions/operators of only special characters (
+
,*
,/=
...) are considered infix by default. To write them as prefix we must surround them in parentheses,(+) 1 2
gives 3. - polymorphic when a function uses a type variable and therefore has a loose declaration.
!=
→/=
- Functions/names are immutable. So a function without arguments is a
#define
. - array → list - use when you have an arbitrary amount of a type of data.
- struct → tuple - use when you have structured data representing a specific thing (like a coordinate).
- void pointer → type variable
- All hard data types (not variable) have capitalized names.
- unit is just saying that it is actual data.
- constuct is some organization of units.
- Later one, we need to discuss/add notes for boxed and unboxed types.
- Not only can data be variable, but also the types. These are called type variables and functions could be variable in their arguments and return values but have a consistent flow for the types.
- e.g.
:t fst
→fst :: (a,b) -> a
which means the return value has the same type as the first value. - Allows you to write functions which are a notion around data rather than looking at the actual data. The
head
function is simply "give me the first element of a list" but cares not for the type of the element. So:t head
→head :: [a] -> a
wherea
is any type.
- e.g.
Name | Level | Typeclass (some) | Example | Range | Note |
---|---|---|---|---|---|
Boolean | unit | Enum | True , False |
[True ,False ] |
|
Int | unit | Enum, Num, Integral | 1 , 7 |
[-2^63, 2^63] (64 bit machine) | Will adapt to float in cases like 1 + 1.0 |
Integer | unit | Enum, Num, Integeral | 1 , 7 |
(-INF, INF) | less efficient than Int |
Float | unit | Enum, Num, Floating | 3.14 , 1.62 |
(-INF, INF) | |
Double | unit | Enum, Num, Floating | 3.14 , 1.62 |
(-INF, INF) | Float with double the precision |
Char | unit | Enum | 'a', '\11111'` |
['\NUL', '\1114111'` | Default supports unicode |
List | construct | [1,2,3] , ['a', 'b', 'c'] → "abc" |
unbounded | all units have the same type, String is a List of Char | |
Tuple | construct | (1,2,3) , (1, 1.1, 'a') |
single units | heterogeneous types allowed | |
Ordering | construct | Takes elements of Ord | LT , GT & EQ |
unbounded | Essentially an enum which denotes order |
A type classes is a are interfaces that types may implement such that they can be used with functions that have class constraints. They specify properties about the data type.
- e.g.
:t (==)
gives(==) :: (Eq a) => a -> a -> Bool
. The==
function can only operate on arguments which have types from which bear the Eq type class.- The first part of
(Eq a) =>
is a class constraint saying thata
needs to have the type class forEq
. - The second part of
a -> a -> Bool
just means the 2 args must have the same type and the return value is a Boolean.
- The first part of
:t elem
→(Eq a) => a -> [a] -> Bool
since it uses(==)
to determine if an element is in a list.Eq
supported types implement==
and/=
.Ord
supported types implement>
,>=
,<=
,<
(ordering stuff). Requires thatEq
is already satisfied.Show
supported types can be printed (output in String form). To print, useshow 3
→"3"
.Read
(accompanied with functionread
) is the opposite ofShow
.read
is basicallyeval
(Ruby). e.g.read "3" + 2
gives5
.- Function
read
is a bit special.:t read
→read :: (Read a) => String -> a
which means the return value could be anything. - If we don't use it directly with another defined type, it won't know how to interpret the returned value and a
no parse
exception. - To solve this, we use a type annotation which declares how we want the output to be interpreted. e.g.
read "3" :: Int
gives3
as type Int.
- Function
Enum
are sequentially ordered. Can usesucc
,pred
, etc.Bounded
are types with bounds. They are polymorphic since they work on many types.minBound :: Int
→-9223372036854775808
,minBound :: Char
→'\NUL'
.Num
are number like classes. Requires thatEq
andShow
are already satisfied.Integral
, as expected, is whole numbers and containsInt
andInteger
.Floating
includesFloat
andDouble
.
fromIntegral
is a function to turn (cast) an Integral type into aNum
so that you can use it withFloating
. While a piece of data, say5
, might have a strictest definition of something likeInt
it can be treated with broader definitions right up to type classes themselves which are just interfaces (likeNum
).
- Declarations:
- You can explicitly declare the IO types of a function (the mapping type) before writing out the function (likes headers in C) (see def.hs).
- The difference between
Int -> Int -> Int
andInt, Int -> Int
is ...
- Functions:
<function name> arg1 arg2 arg3 = <stuff with args>
- Function names must start with a lowercase character because ...
- IF:
- IF branches must always have an
else
claws. This ensures that the IF block is an expression (code that always returns something).
- IF branches must always have an
- Lists are inherent data structures to the language.
- They can be of any type but must be homogeneous.
- Lists of lists can be of different lengths but not different types of lists. Think about casted pointers in C!
- Concatenation of lists
A
andB
is defined asA ++ B
(O(|A|)
). cons
operator is a perpend insert of a single character.1:[2,3,4] --> [1,2,3,4]
(O(1)
).- You can then assume that
[1,2,3]
is equivalent to1:2:3:[]
. - Indexing into a list is with
!!
such that"abc" !! 1 --> 'b'
. - Operators
<
,>
,<=
,>=
are defined as working from the head of the list and breaking on first win. i.e.[3,2,1] > [3,2,0] --> True
. head
returns the first element.tail
returns everything after the first element (the head).last
returns the last element.init
returns everything before the last element.- ^ all of these throw exceptions on empty lists. For the functions below, you don't need to check for the length.
length
returns the number (integer) of elements in the list.null
returnsTrue
if the list is empty.reverse
reverses a list.take <int> <list>
returns the first <int> number elements for <list>.drop <int> <list>
returns <list> without the first <int> elements.minimum
returns the min element.maximum
returns the max element.sum
returns the sum of all the elements.product
is the production of all elements.A `elem` B
tells us ifA
is in the listB
.[A..B]
returns all sequential elements (if that is defined) fromA
toB
.[A,B..C]
returns a linear spacing ofA
toC
with step size of deduced asB-A
. Floats in these devices often have funky precision errors.cycle
returns an infinite repetition of the list (flattened).repeat <element>
returns an infinite list of the element or is the same ascycle [<element>]
.replicate A B
returns a list of lengthA
with every element beingB
.- We can write list comprehensions or mappings like so
[x*x | x <- [1..5]]
which returns[1,4,9,16,15]
.- Predicates can be added (aka filtering).
[x*x | x <- [1..5], even (x*x)]
gives all the even squares. - Multiple predicates/filters can be added.
[x*x | x <- [1..10], even (x*x), x*x < 50]
gives[4,16,36]
.
- Predicates can be added (aka filtering).
- You should see by now that list comprehensions are a way to implement loops. This is also true when it comes to double loops where we supply 2 lists.
[x*y | x <- [1,2,3], y <- [2,2,0]]
gives[2,2,0,4,4,0,6,6,0]
.- A loop is required to compute length, but the element-iterator is not used in the mapping so we denote it as
_
. `length' x = sum[1 | _ <- x]
- Haskell will operate on actual data and evaluate expressions only when absolutely necessary (when it needs to make a decision).
- We can define infinite lists like
take 3 [1..]
.[1..]
never finishes computing but sincetake 3
requires only a piece, it will finish and return[1,2,3]
.
- Length and types (at each index) define the type of tuple.
- There is no empty or null tuple.
- Comparators are defined.
fst x
returns the first component of tuplex
.snd x
returns the second componet of tuplex
.- ^ these only work on pairs (tuples with only two components).
zip A B
will return a list of pairs where the elements ofA
are paired up with the elements ofB
.
- This is common practice in functional programming.
- How do we calculate all the right triangles with integer sides <= 10 and perimetre = 24.
- In an imperative language we would probably have to triple loop and check for both right-ness and the length of the perimeter.
- In Haskell, building from the initial full space to a specific result is a bit more elegant.
-- all triangles of integer sides <= 10
[(a,b,c) | a <- [1..10], b <- [1..10], c <- [1..10]]
-- remove the duplicates
[(a,b,c) | a <- [1..10], b <- [1..a], c <- [1..b]]
-- make the triangles right again
[(a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]
-- remove the ones without the target perimeter
[(a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
- These are concise case statments.
- They should alwasys have a catch all claws at the end to ensure they don't throw exceptions from non-exhaustive pattern input.
- as patterns are patterns which are matched but you also get a reference to the original input -
all@(pieces:of:all)
. - Guards are the pattern matching version of boolean checks. If one is true, we output that clause.
- We can also name things in guards and define them later (
where
keyword). - where bindings can even be defined by pattern matching (see the initials function).
- where bindings can also defined entire fuctions which can then be called during the function's block.
- where bindings are local to the function but let bindings are local to only an expression in the function.
- let syntax is
let <bindings> in <expression>
. Because they are expressions you can cram these anywhere in code. - We also have a case expression (which is really what pattern matchin is) and it can be thrown in anywhere.