Week 7: Haskell

With this post, I've come to the end of my series going through the book Seven Languages in Seven Weeks by Bruce Tate. It was a good experience and I feel like I learned quite a bit (but have a lot more to learn—this really only scratched the surface of working with these languages).

Week 1 ˙ Week 2 ˙ Week 3 ˙ Week 4 ˙ Week 5 ˙ Week 6 ˙ Week 7

Haskell is the final language in Seven Languages in Seven Weeks. It is a language that takes a purist approach to the functional paradigm and the author considers it the most difficult one in the book to learn. Having it at the end of the book was a good choice since it has a lot of similar features to the functional languages covered earlier (especially Erlang and Clojure). Here are some of Bruce Tate's comments about Haskell:

Haskell supports a wide variety of functional capabilities including list comprehensions, lazy computing strategies, partially applied functions, and currying. In fact, by default, Haskell functions process one parameter at a time, using currying to support multiple arguments.

He also gives this explanation of functions in Haskell:

The centerpiece of the whole Haskell programming paradigm is the function. Since Haskell has strong, static typing, you'll specify each function in two parts: an optional type specification and the implementation.

Here are my notes on Haskell:

  • I was using the "Glasgow" compiler, which launches with ghci and exits with :quit. See here for commands at the interpreter prompt. One that was used in the book is :set +t to show the type of each variable/result (turn it off with :unset +t).
  • Indentation is important in Haskell programs.
  • A string is a list of characters. Concatenate strings with ++.
  • There is a variable called it that always holds the most recent result in the interpreter.
  • let variablename = value can be used to declare a variable. Or a function. where is used to declare something in a local context only.
  • You can write functions in a .hs file then load them using :load filename.hs (unload them using :m).
  • The first line of a function can be functionname :: Type -> Type to declare the input and output data types. If you omit this line, Haskell will infer them.
  • The remainder of a function declaration should be functionname input = output. Haskell functions can be given multiple definitions (on separate lines) and will pattern-match (like Erlang) to the first one that fits the supplied inputs. Another option is to use "guards" (also on separate lines): | (condition) = expression_when_true (the last condition should be otherwise).
  • Tuples are comma-separated items in round parentheses and have a fixed lengths. Lists can be any length (even infinite, since each item is only evaluated as needed) and go in square brackets.
  • Haskell has a lot of useful ways for manipulating lists. A colon matches what is before it to the first item in a list and what comes after to the rest of a list (i.e. head:tail); this can be used to get the first item from an existing list, or to construct a list by prepending a new item to the beginning. Ranges are written as [start .. end]. State the first two items before the double dots if the increment is not 1. Leave the end off to make it an infinite series (from which you can grab or discard the first n items using take n or drop n, respectively). List comprehensions can be used, with similar syntax to Erlang: [result_expression | variable(s) <- List, filter(s)] (filters are boolean expressions).
  • Anonymous functions are written as (\parameter(s) -> function_expression) in Haskell. They can be applied to lists (so can named functions) using filter function [list] or map function [list] to find items in the list that meet certain conditions or to transform each element with the given function, respectively (zip, zipWith, foldl provide other possibilities).
  • Two (or more) functions can be "composed", where the result of the second is passed to the first, by separating them with a period.
  • Under the hood, Haskell converts functions of several arguments into several single-argument functions chained together. This partial application can be very convenient at times. For example (and an example is given below), you can define a new function based on an existing function with some of the arguments filled in. "Sections" of functions can also be used on lists with map—the items in the list are given as the final argument to the function.
  • I skimmed over some of the material in the book about types, classes, and monads in Haskell; even if I hadn't, I think these concepts would take more than a week to get comfortable with.

Here are some examples that should help clarify some of these features in Haskell:

Prelude> map (\x -> x * x) [1,2,3]
Prelude> map (* 2) [1,2,3]
Prelude> let prod x y = x * y
Prelude> prod 3 4
Prelude> map (prod 3) [1, 2, 3]
Prelude> let double = prod 2
Prelude> double 3

(Prelude> is the prompt in the interpreter when no modules are loaded.)

This example shows mapping an anonymous function (squaring each element) and then a section (multiplying by two) to a list. Next a function called prod is defined that multiplies two numbers together. After using it to calculate 3 x 4, it is mapped (as a "section" with one input filled in) onto a list. Then a new function is defined based on partial application of the first function.

The next example is a .hs file that defines some functions:

module Main where

    denom :: (Integer, Integer) -> Bool
    denom (n, d) = (mod n d) == 0

    factors n = [x | x <- [1..n], denom(n,x)]

    is_prime :: Integer -> Bool
    is_prime n = length (factors n) == 2

    commonfactors (x, y) = [z | z <- [1..(x+y)], denom(x,z), denom(y,z)]

    greatestcommon :: (Integer, Integer) -> Integer
    greatestcommon (x, y) = last (commonfactors (x, y))

The first function tests whether a number is a denominator of another. It takes a tuple of two integers as an input and returns True or False. The next function calculates the factors of a number using a "list comprehension"; the denom test function is used as a filter. To test if an integer is prime (using the approach from this answer), the third function checks whether a number has only two factors (i.e. one and itself). The last two functions calculate the common factors of two integers (from another list comprehension; note that the range in the list generator should only go to the maximum of x or y rather than their sum but I didn't know the function for that) and their greatest common factor (as the last element in a list of common factors). Note that I didn't declare type on all of the methods (at the time I couldn't figure out how to declare the type when the result is in a list; now I think it should be Integer -> [Integer] for the factors function, but I haven't tested that) but Haskell is able to infer it.

To continue this example, this shows loading these functions and applying them:

Prelude> :load is_prime.hs
[1 of 1] Compiling Main             ( is_prime.hs, interpreted )
Ok, modules loaded: Main.
*Main> denom(20,5)
*Main> denom(20,3)
*Main> factors 24
*Main> factors 23
*Main> is_prime 23
*Main> commonfactors (18, 60)
*Main> greatestcommon (18, 60)
*Main> filter is_prime [1..100]

The final line applies filter and the is_prime test function to a list/range of numbers from 1 to 100.

Haskell is easy to Google for, so the only link I'll share is this site where you can try it online.

I've now come to the end of Seven Languages in Seven Weeks. It was a good experience. Obviously a week wasn't enough time to become proficient with any of the languages covered, but it was enough to get my feet wet. Also, I feel that seeing some key concepts (e.g. types, object-oriented vs. functional paradigms, recursion, common tools—like filtering and mapping—for dealing with lists, and what concurrency is even if I didn't go too far into it) repeated in multiple languages helped me understand some new things about programming. I liked learning new techniques for problem solving like list comprehensions and pattern-matching/destructuring in some of the functional languages.

Over seven-ish weeks (not counting an intermission), I'd guess I put at least 40 hours into Seven Languages in Seven Weeks. That makes it roughly equivalent to the time that would be spent in lectures in a 1 semester undergraduate course. While working full time it can be hard to find this kind of time, but it is encouraging to know that 40 hours focusing on something is enough to make noticeable progress in your skills. I'd recommend this book to others that want to improve their programming know-how. It's probably not ideal for an absolute beginner but if you have a language or two that you're half-way comfortable in you should be able to get something out of it. And if your programming background is a lot stronger than mine, I still think you could find something interesting in this book; I think it works on multiple levels. For example, I skimmed/skipped over a lot of the subsections on concurrency (except in Io), because that topic is more advanced than my skill level (and more relevant for real-time applications, which aren't really my area of interest), but for someone else seeing how different languages approach concurrency might be a highlight of the book.

I'm coming from a perspective where any programming I do is small-scale scripting for my personal use, so I find that setting up classes (in object-oriented languages) and strong, explicit typing add too much overhead. Keeping that perspective in mind, here are some of my feelings on the languages covered in Seven Languages in Seven Weeks:

  • Ruby is the most popular language covered in the book (even 8 years after it was written). It wasn't my favourite (object-oriented isn't my preferred paradigm) but the fact that it is so widely used probably makes it worth being familiar with.
  • I had the most fun with Io and Clojure. Io was the quickest to pick up the basics of and I found its simplicity made some concepts clearer. I'd recommend it in education for that reason. In Clojure, I appreciated the aesthetics—it is so succinct.
  • Prolog gets an honourable mention from me for solving problems in a unique way. It has niche applicability.
  • Scala seems really strong for handling XML, but it's a prime example of the overhead required for setting up classes and dealing with strong typing.
  • Haskell and Erlang seem quite similar (obviously Clojure also follows the functional paradigm but its prefix notation gives it a different feel), with the former being more general and the latter primarily used in telecommunications as far as I can tell.

Well, this concludes the longest series I've done to date on this blog. While I've been going through it, I've also finished reading some books, so for my next posts I'm planning to write some reviews.