Declarative Go

Dylan Meeus
4 min readOct 23, 2019

--

After previously covering some more exotic concepts of functional programming such as Currying and Continuation-Passing style, maybe it’s a good idea to zoom in on something perhaps even more ‘core’ to functional programming.

With declarative programming you say what you want and not how you want it. Probably one of the most widely-used example of this way of programming would be SQL, where you tend to say something akin to:

select * from users
where age > 50

Let’s formalize this problem a bit. Imagine we have a set of users, and we are asked to return a list of all users that are older than 50. In Go, we could create a function for that:

func olderUsers(us []User) (out []User) {
for _,u := range us {
if u.age > 50 {
out = append(out, u)
}
}
}

We had to iterate over all of our users, check the predicte age > 50 and finally return the list we have created. In SQL we didn’t need to loop!

Our olderUsers function is essentially applying a filter operation to a slice of Users. Making a new function for every possible filter is, however, something that gets tiresome. You don’t necessarily want a set of functions such as: olderUsers(), youngerUsers(), maleUsers(), femaleUsers(). Because each time you’d have to implement almost the same logic.

We can make our Go example more declarative by creating a UserFilter function:

func filterUsers(f func(User)bool, us []User) (out []User) {
for _,u := range us {
if f(u) {
out = append(out, u)
}
}
return
}

With such a function we can say what we want wherever we use the function instead of looping manually. Thus the loop is only happening in one ‘function’. Go is not a declarative language, so we’ll always end up writing imperative code in some parts. But now imagine our manager approaches us and says “I want to get a list of all users which are over 50 and female”. We can just tell our code that we want this:

func managersRequest(us []User) []User {
older := filterUsers(func(u User)bool{return u.age > 50}, us)
filtered := filterUsers(func(u User)bool{return u.isFemale(), older)
return filtered
}

Some issues..

This approach works for filtering our users, but it turns out our program also has cars, and we want to filter those. Sure, we’ll implement a filterCars function where we do the loops, analogous to how we did it for users. A month later we need to do the same for Movies and Medication. (At this point I’d also question what kind of application we’re building that holds these types of information)

Furthermore, if our implementation in filterUsers changes, this is not automatically reflected in all other functions. So that’s a downside. Plus, what if we want to build other functions such as Reduce, Revert, Take, Contains ?

One way we could solve this problem is by using Code Generation, so we could generate a similar function for each data type. We’d end up with one implementation of Filter or Reverse and generate it for Users, Cars, Animals, Medication,..

Hasgo

Hasgo is a Go Code Generator that creates functions (based on Haskell) for any data type. You can simple go-get it or require it in a go.mod file. go get -u github.com/DylanMeeus/Hasgo. The attentive reader probably noticed that I started this library, what follows is really just a shameless plug for my project. 😃

First thing to get out of the way, you can safely stop reading here if you’re not interested in using such a library. You could argue that Go is a verbose language on purpose and you should not write things declaratively, or just copy-paste functions for each struct that needs such a function (because.. no generics,). Perhaps you might even say someone should have used a different language rather than Go if they wanted to code this way. And yeah.. maybe. But if you happen to like Go, and you like this way of programming, there’s really no harm in doing so as far as I’m concerned. Go allows you to be free in how you use it.

My goal here is not to elaborate on how Hasgo works, besides, I wrote a blog about that some time ago. Rather just to inform you that if programming in a declarative way is your kind of jam, there’s this library out there that makes it a bit easier. (There’s others as well, a very similar one which Hasgo is based on is Pie. Which is definitely worth checking out as well!)

With Hasgo, which comes with functions for Strings, Bools, Ints you can write functions like this:

result := IntRange(-10,10).
Abs().
Map(func(i int64)int64{return i*i}).
Filter(func(i int64)bool {return i % 2 == 0}).
Sum()

Admittedly, the lambda syntax could be easier on the eyes. But hey, it’s possible. 😅

If you enjoyed this post, consider reading buying my book “Functional Programming in Go”, which explores this topic and many more: https://www.amazon.ca/Functional-Programming-functional-testability-readability/dp/1801811164

--

--