Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Ok I'll take a stab at a super high level crash course:
- A note! Haskell has weird syntax. But it actually uses this syntax is cool ways, as opposed to some languages that have weird syntax in an effort to be different.
- Let's consider the basic unit of Haskell programming -- the function!
- Here is a function which returns its input:
- f x = x
- Notice that there are no parens or types or anything. Since functions are used so heavily, polluting them with parens would make your code look uglier than lisp.
- Now notice that I didn't include a type signature. In haskell, you can omit them if you want. The compiler will figure out the most general type. Think of this as C++11 `auto` keyword on steroids.
- If I were to include the type signature, it would look like this:
- f :: a -> a
- f x = x
- Let's break down those symbols.
- `::` just means "is of the type". So `f :: ...` is "function f is of the type..."
- `a` is a type variable. Think of this like C++ templates! In C++, you an equivalent function is
- template <typename T>
- T f(T x) { return x; }
- Where `T` is the same as our `a` in Haskell. In Haskell, it's common to use the first few letters of the alphabet for type variables.
- The `->` syntax is a bit funny, but it makes sense when you learn about *function currying*, which I will not explain now because it can be intimidating to some. For now, just think of the type signature as
- arg1Type -> arg2Type -> ... -> argNType -> ResultType
- So for our function `f` of type
- f :: a -> a
- That means "The function f takes some argument of type `a`, and returns a value of the same type `a`"
- Now let's consider a more complex function.
- f x = 2 * x
- As you can see, this is a doubling function. It takes a value and doubles it. We can imagine that the value it takes can't be any arbitrary type, because not all types can be doubled! So it doesn't make much sense to pass in a string.
- In Java style, we might say that the type must implement the interface `Multipliable` or something. It turns out, Haskell has a system that is very roughly similar to Java interfaces or in C++, a class with a bunch of virtual methods.
- Haskell calls these Typeclasses. So there is a typeclass that specifies that types in the typeclass must implement some basic numeric methods, such as +, *, etc. This typeeclass is called `Num`.
- So we can imagine, our doubling function *only* takes types that implement `Num`. Our type signature reads:
- f :: Num a => a -> a
- f x = x * 2
- So everything between the :: and the => is typeclass specifications. So in this one, I just specify that type `a` must implement `Num`. Let's say I also required `a` to have the equivalent of a Java `toString` method, I would say:
- f :: (Show a, Num a) => a -> a
- So now `a` must have a `toString` style method and must have numeric methods.
- Now we are ready to approach the `primes` bit.
- The first foreign looking thing is [2..]. This is an *infinite* list from 2 to infinity. How can one have an infinite list? The answer is laziness.
- Whenever I go and ask for the 10000th value, the list must extend itself to that value if it hasn't yet. So the list only grows for as much as you ask for, but in theory it will grow until you run out of memory.
- The [m..n] syntax is just nice syntactic sugar because it's used so much. [1..10] == [1,2,3,4,5,6,7,8,9,10]
- [1..] == [1,2,3,4,5,....infinity]
- So this is what we know so far:
- primes = sieve [2..]
- Which means "primes is equal to the function sieve, called on an infinite list from 2 to infinity"
- Now you ask, well what the hell is the function `sieve`?
- Haskell has a nice sugar for defining scoped functions within other functions with the `where` keyword.
- Consider the doubling function. I could instead do:
- f x = x * (three - one)
- where
- three = 3
- one = 1
- So it just let's you define values that get used in the function body in a convenient place. This is akin to math jargon where people say "blah blah x something blah where x is blah".
- So `sieve` is a scoped function (cannot be accessed from outside of `primes` that takes a list as an argument and apparently produces the primes that are in that list. How does it do it?
- We see some weird syntax in the definition of `sieve`:
- sieve (p:xs) = ...
- What is this? It's pattern matching. Let me use fake C++ as an example. Consider the recursive factorial function:
- int fact(int n) {
- if(n == 0) return 1;
- return n * fact(n-1);
- }
- Now imagine if C++ let you do this instead:
- int fact(0) { return 1; }
- int fact(int n) { return n * fact(n-1); }
- That is, you make a special case function body when the argument is 0. At runtime, if the arg is 0, it uses the special case body, otherwise it falls through to the general body.
- So in Haskell, we could implement factorial like
- fact :: Num a => a -> a
- fact 0 = 1
- fact n = n * fact (n-1)
- Very clean and sexy! But now you say "Yeah, but I could just use an if statement or a switch..."
- Now we go into the funny syntax and power of that funny syntax! Not only can pattern matching match on values, but it can decompose those values. What do I mean? Again, let me use some fake C++ as an example.
- Consider the C++ function
- int f(pair<int, int> my_pair) {
- int x = my_pair.first;
- int y = my_pair.second;
- return x*y;
- }
- Ignore that there is no real reason to pull the values out into `x` and `y`. In a more complex function, I'm sure you can understand why it would be tedious to type `my_pair.first` over and over again and we would want to pull it into `x` or something.
- Now imagine if C++ let us do this:
- int f(pair<x, y>) {
- return x*y;
- }
- That is, f takes a pair, and *in the type declaration* we decompose the pair into its first and second parts, assigning them to the variables `x` and `y`. In Haskell, this is trivial.
- Consider a direct translation of the C++:
- f :: (Int, Int) -> Int
- f my_pair = fst my_pair * snd my_pair
- Now the idiomatic Haskell, with pattern matching decomposition:
- f :: (Int, Int) -> Int
- f (x,y) = x*y
- Slick, right?
- So what is `sieve (p:xs)` decomposing?
- In Haskell, the default [] list is a linked list (there are random access arrays in various libraries). That means we can stick a value onto the front of the list in O(1) time. The way we stick a value onto the front of the list is with the `:` operator.
- So
- x = [1,2,3]
- y = 0:x
- `y` is now [0,1,2,3]
- Just like we can compose lists with the `:` operator, we can *decompose* them via pattern matching with it.
- So consider the function (I'll omit the type signature for brevity)
- f (p:xs) = print (p, xs)
- Let's say we give `f` the argument [1,2,3,4]. What call to `:` would we have to make to get this list?
- [1,2,3,4] == 1:[2,3,4]
- So when we send [1,2,3,4] into `f`, it gets decomposed into ` 1:[2,3,4] ` where 1 gets put into `p` and [2,3,4] gets put into `xs`.
- This is a very common idiom in Haskell. Let's say I wanted to make a function that added 1 to every value in a list. Ignoring that there are much cooler/efficient ways to do this in Haskell, let's make a specialized function for it.
- addOne (x:xs) = x+1 : addOne xs
- So let's read through that. In our pattern matching decomposition, we pull off the first value of the list and store it in `x`, and we take the rest of the list and store it in `xs`.
- Now in our function body, we recompose a list with different values. Those values are `x+1` for the first value, and `addOne xs` for the second value. So we add one to the first value, and stick it into the front of the rest of the list after the rest has had 1 added to its values. You might notice now, that there is no bottom-out case for this recursion. So let's add one.
- addOne [] = []
- addOne (x:xs) = x+1 : addOne xs
- So we pattern match to catch the special case of the empty list, which returns the empty list, and then we decompose the general case as previously explained.
- So `sieve` is pulling off the first value, storing it in `p`, and taking the rest of the list and storing it in `xs`.
- It is returning...
- p : sieve [x | x <- xs, x `mod` p /= 0]
- So, that is `p` stuck onto the front of...
- sieve [x | x <- xs, x `mod` p /= 0]
- And we know that sieve returns all of the primes within its argument, so in English...
- p stuck onto the front of all the primes in [x | x <- xs, x `mod` p /= 0]
- So now what the hell is [x | x <- xs, x `mod` p /= 0]? It is a list comprehension.
- Python has these too, along with a few others, and they are quite handy. If you are familiar with set builder notation in mathematics, they often say things like
- For all whole numbers x such that x mod 2 = 0
- to denote even numbers. Or in fancy math notation...
- { x | x ∈ Z, x % 2 = 0 }
- Where ∈ means "is an element of" and Z == all integers. So Haskell's list comprehension notation is very similar. It is:
- [ some expression of x | x <- some list to iterate on, some test]
- So that even numbers thing could be:
- [ x | x <- [0..], x `mod` 2 == 0 ]
- Notice that we are using an infinite list! So the result will be an infinite list, since we are iterating infinitely.
- In python we'd say
- [ x for x in range(0,N) if x%2==0 ]
- where N is whatever big number you want. If you don't know python, range(n,m) == [n,n+1,n+2...m-1]
- So back to [x | x <- xs, x `mod` p /= 0]
- That reads in english "all x in xs, such that x mod p does not equal 0". Haskell uses /= rather than != because it looks closer to the math slashed-equals sign for inequality.
- So now we have again
- sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
- So that reads in english:
- Sieve is a function which takes a single list argument and returns the primes in that list. It decomposes the list into the first value, and the rest of the values. The first value goes into `p`, the rest go into `xs`. It returns a list that is equal to p stuck onto all of the primes in the list defined by the following list comprehension: All values `x` in `xs` such that `x mod p` does not equal 0.
- So then we have `primes = sieve [2..]` which we now know means "primes = all the primes in the of numbers from 2 to infinity", which should be all the primes.
Advertisement
Add Comment
Please, Sign In to add comment