A little adventure with Haskell and Go

I recently decided to brush up on my functional programming skills. My day job increasingly involves building and operating large-scale distributed systems and I became interested in the intersection between this and functional programming.

There are all sorts of reasons why using a functional programming language to build distributed systems is beneficial, but I won't go into details right now (maybe in a future post). For the moment, I'll relate what happened when I decided to brush up on my functional knowledge and then compare it to the imperative style.

I had learned Haskell in my university days as a young, wide-eyed undergrad, so I decided to start there and revisit it. To get back in the saddle, I decided to write a simple -- no, wait -- a very simple task manager.

Brushing up then going further

While developing the Haskell-based task manager (which I'd christened "Hask Manager"), I was reminded why the functional style appealed to me so much. Being a computer scientist by training, I see it as being closer to the theoretical underpinnings of computer science than the more popular imperative style is. I also believe the functional style brings numerous important benefits over the imperative style.

Detour: Remind me - what's functional programming?

In days gone by, when dinosaurs roamed the earth, these dinosaurs programmed in languages like assembly, Fortran or C. Writing an algorithm in languages like these is a bit like writing a recipe: explicitly prescribing each little step and each change in state to build up a whole series of steps (to be executed strictly in order!) and organise them into blocks called subroutines or functions. This is known as imperative programming and most of the popular languages in history have been imperative.

An imperative approach to doubling a list of numbers would look something like this (in Go):

// returns [2, 4, 6, 8, 10]
func getDoubles() []int {
	doubles := make([]int, 0)
	for _, n := range []int{1, 2, 3, 4, 5} {
		doubles = append(doubles, n * 2)
	}
	return doubles
}

But some other dinosaurs took a different approach called functional programming. Their approach was built around the notion of functions, although unlike "functions" from other languages like C (which are really procedures), their functions were based on the mathematical idea of a function: an expression that has no state or mutable variables, and has no side effects when run. Every time you execute a function with the same set of arguments in such a language, the result is always the same. These ideas developed into languages like LISP and Scheme.

A functional approach to doubling a list of numbers would look something like this (in Haskell):

-- returns [2, 4, 6, 8, 10]
getDoubles = map (* 2) [1, 2, 3, 4, 5]

Today, there's not such a hard distinction between functional and imperative languages. As time goes on, imperative languages incorporate more and more ideas from the functional approach.

Back to the story...

At this point, I was only playing. I had no grand aim in mind for this little side project and so it grew organically as I continued to play. Once the Haskell task manager was up and running, I decided to try and compare the functional style of Haskell with a suitable non-functional language. I decided to rewrite Hask Manager using an imperative language instead. For this, I chose the Go language (Google's flagship programming language) and this version of the task manager I called "Go Get Things Done".

Go logo by Renée French

Go logo by Renée French

I had no strong reasons for choosing Go, but a few things made it feel suitable:

  • I'd been learning and using Go for about a year (I've made some contributions to Kubernetes and CoreOS, which are built using Go).
  • Not only is it imperative, but it avoids a lot of functional ideas (although it has taken a few here and there).
  • It's a young language which means its designers had a lot of existing software engineering wisdom to draw on. The Go authors have explicitly stated that they are quite ruthless in deciding whether a feature should be implemented in the language. If they've being choosing wisely, it should be a language with no outdated, legacy concepts. This makes it a "worthy" object of comparison.

What did I end up writing?

Both versions of the program are functionally the same. Nevertheless, I tried to stick to each language's best practices as best I could. The resulting program is really simple. It understands commands to maintain a task list, such as:

  • all
  • add <description> <date>
  • todo
  • today
  • show <id>
  • tick|untick <id>
  • quit

Both versions are available on my GitHub account:

What are these STOPs in the code?

As I was familiarising myself with Haskell, and later when I was comparing the two versions, I wrote comments in the code. I have a leaky memory, so I find writing stuff down is the best way for me learn and have it stick.

In case anyone else is interested in learning the basics of either language, or just looking through the code comparison, I put the phrase "STOP" in each comment and a number denoting the order in which I recommend you follow them. So, if you open the repo in an IDE and search for the phrase "STOP", you should get a nice convenient list of stops to go through.

Interesting notes

In no particular order, some observations I had as I was writing these programs.

Learnability

I believe functional programming is harder and takes more investment to learn than the imperative style.

Experienced programmers who already know the imperative approach but not the functional will have a lot of assumptions and deeply embedded habits that will need overcoming.

But I suspect a novice would fare little better if they learned the functional style first. Functional programming involves some quite abstract mathematical thinking that is not highly intuitive for a beginner. Conversely, imperative programming is more about small concrete steps. All in all, a beginner can grasp an imperative language quicker and achieve something in it after less investment.

Finding mistakes

One thing that makes Haskell harder to learn is the abstract error messages which I found difficult to decipher when learning. However, once I got the hang of things, I found that the time spent understanding and correcting errors in my programs became much shorter.

I even got the feeling that, with experience, correcting errors in the Haskell code took less time than the Go code. In Haskell, once an error was understood, the fix was usually clear. In Go more time was spent running through a piece of faulty code procedurally, trying to work out the problem and trying multiple changes to fix it.

In fact, the rather unsettling "works after first attempt" (that frightening occurrence when the first draft of your code works without a problem) happened a few times after writing a Haskell function, something that I find happens a lot less often in Go (or any imperative language).

Functional as a prototype

I've read online a few examples of people developing a Haskell version of a program as a prototype before then re-implementing it in a more 'mainstream' language. (I've also read examples of people finding that their Haskell prototypes worked just fine, thank you, and subsequently junking plans to re-implement.)

If that were your desire (although I think Haskell is a perfectly suitable implementation language), I see the advantages of the approach. I noticed a few places where the Go version of the task manager took inspiration from the Haskell version and was an improvement over the version that I probably would have written.

One example is the commands.go file, which is a sort of backend for updating task data. I can imagine that I would have written the functions in there as less generic versions (e.g. GetTodaysTasks or GetUndoneTasks) had I not first written a Haskell version which, by the language's nature, encouraged me to write the functions generically. That backend turned out more like a database API, with a small number of generic functions (AddTask, QueryTask, UpdateTask etc.). This meant that the backend could stay concise and reuseable, and still allow the programmer to easily come up with an endless variety of different operations by filling in the blanks of the generic functions. However, the Go syntax makes this same functionality less readable than its Haskell counterpart.

// main.go
if strings.HasPrefix(input, "todo") {
	return gogtd.CmdQueryMany(
		func(tl *gogtd.TaskList) *gogtd.TaskList {
                        return tl.Filter(gogtd.IsPending)
                },
	)
}
 
// commands.go
func CmdQueryMany(f func(*TaskList) *TaskList) string {
	tasks := GetTasksFromFile()
	resultSet := f(tasks)
	return resultSet.String()
}
--- Main.hs
interpret input
    | "todo" `isPrefixOf` input = cmdQueryMany getPending
 
--- Commands.hs
cmdQueryMany :: (TaskList -> TaskList) -> IO String
cmdQueryMany f = do
    tasks <- getTasksFromFile
    return showTaskList(f(tasks))

Making Go more functional

Just because support for the functional style is limited in Go, doesn't mean you can't add a little bit yourself. For example, the standard functional primitives (map, filter and reduce/fold) may be missing, but with first-class functions in Go you can roll your own (see Filter in task_list.go).

func (tl *TaskList) Filter(f func(t *Task) bool) *TaskList {
	newTl := NewTaskList()
	for id, t := range tl.Tasks {
		if f(t) {
			newTl.Tasks[id] = t
		}
	}
	return newTl
}

Of course, this example not as powerful as filter from Haskell, because it can only be used to filter a TaskList (whereas filter in Haskell is generic).

Conciseness

The Haskell version is more concise, but this is hardly surprising. Functional programming is generally thought of as being more expressive, allowing you to achieve more per line of code.

Side effects in Haskell

Once you introduce a side effect into one function, it can seep into other parts of your program. Just by using such an "impure" function, another function allows side effects to influence its behaviour.

You can write functions with or without side effects in both Haskell and Go. A key difference between Haskell and a language like Go is that Haskell is explicit about which functions deal with side effects. Once a function has allowed side effects in, it must be marked as such, and that "bubbles up" any other functions that use it.

Safety in Haskell

Similarly to its strictness in dealing with side effects, Haskell is also strict when dealing with potential errors. If your function calls another function that might be unable to return a value (via a Maybe) or might return an error (via an Either), you must deal with it. There's no getting away with ignoring potential error values as there is in Go.

Summary

That's all for now. I hope in the future to do some more stuff along this theme... maybe writing a comparison that makes use of concurrency features, explaining in greater detail how functional programming works, or expanding on how it can be used in distributed systems.

Watch this space.

 

karl

 

One thought on “A little adventure with Haskell and Go

Leave a Reply

Your email address will not be published. Required fields are marked *