Gracefully Mimicking Clojure’s Threading Macros in Golang

Introduction

Wise men said if you can’t find happiness in your life, it’s not solely the other’s fault because you are the only one who is responsible for that. They said in order to achieve happiness you have to figure out a way and create it by your own instead of finding it nowhere.

One of wonderful things is by looking at how Clojure executes a set of functions sequentially in a lovely manner and it feels so oddly satisfying which Golang has none to offer the same thing.

I actually love both programming languages but lately I code in Clojure more often than Golang since I joined Gojek. There’s a feature in Clojure that I love the most. It’s called Threading Macros. What the hell is it, actually?

In an oversimplified explanation, it’s a pattern that can help you to set up a set of functions and let each of them doing data transformation when executed, which the transformed data then will be passed as an argument to the next function in the set to generate new information.

Image 1: Threading Macros in Clojure

As shown in the Image 1, the Clojure’s Threading Macro has a set of functions which comprises:

  • (get-restaurants) that return a list of restaurant
  • (get-menus) that return a list of menu from a list of restaurant returned by (get-restaurants)
  • (get-food-with-highest-price) that return food with highest price from a list of menu returned by (get-menus)

We can see the data streamed from one to another function with a new form so neatly!

By the way, if you’d like to find out more about Clojure’s Threading Macros, go find it on Clojure’s Documentation Official Page.

Yeah, I know. We all are going to love this feature. Who doesn’t? It introduces a syntactic sugar touch to our code base, which makes things easier to read or to express. Oops… beware of diabetes! 🤗

All must agree that statically typed programming languages are usually better in terms of performance rather than the dynamically typed ones, but that’s not the case because we sometimes are longing for the simplicity of writing code too!

I tried to carry that kind of simplicity from Clojure to Golang by introducing a small library called Pipe, which I developed recently. It basically does the same thing as Clojure’s Threading Macros and copies its concept.

Let’s get dirty, show me the code!

Here we go. We just imported the github.com/parinpan/pipe library at the very top of the file. This library has two core functions to make the same functionality of Clojure’s Threading Macros running in Golang, they are: pipe.Do() and pipe.Apply()

  • the pipe.Do() function accepts a set of pipe.Apply() functions to register all the functions for sequential execution.
  • the pipe.Apply() function accepts any function with its arguments or else we could leave them blank, since by default the passed function’s argument will be the latest return value of previous executed function in the set.

Okay, it’s still fine if all the functions only have one argument so that this library can pass the latest return value of previous executed function to it. Now, the question is: how about the functions that have more than one argument? 👀🧐

Relax. Let me show you the code again. We have the answer! 🙈

Let’s see what’s changed here. Now some functions accept more than one argument.

  • getMenusWithFilter(…) function accepts restaurant location at the 1st argument, a list of restaurant at the 2nd argument, and restaurant established year at the 3rd argument
  • getFoodByName(…) function accepts a list of menu at the 1st argument and food name at the 2nd argument

So, now, how the library can know at what index it should pass the argument with the return value of previous executed function? Should it be at the function argument’s 1st index, 2nd index, or nth index?

If you noticed, from the code above there are some pipe.Pass() function passed in the pipe.Apply() which we know already that it accepts a function and its arguments. So, the library will replace the argument value with the carrier return value at an argument’s index where the pipe.Pass() was being passed.

The special thing is pipe.Pass() can also accept a function to alter the carrier return value to a new form before being executed, which shown below.

You can think pipe.Pass() is an equivalent to the Clojure’s anonymous function “%” symbol.

# this is clojure!(-> (get-restaurant)
(#(get-menus-with-filter "Bay Area"
% 1940))
(#(get-food-name
% "BigMac"))
(#(say (format "I love %s so much!"
%)))

Isn’t it cool, huh? 😎

After going thru the cool stuff, we must thank to Golang’s reflection feature, which makes it all possible to happen! Yeah, the library is actually only an abstraction on top of it to hide all the complexities behind.

Let’s look at a sneak peek where it happened by looking at this code snippet:

returnValue := reflect.ValueOf(function).Call([]reflect.Value{arg1, arg2, arg3})[0]

Basically, the library uses the Golang’s reflect.ValueOf() function to call all functions that we’ve registered one by one using the combination of pipe.Do() and pipe.Apply() function together with their arguments.

After we got the returnValue from the reflect.ValueOf() execution, we’re going to use that value again as the next executed function’s argument. 👻

Tested on MacBook Pro 16 inch 2019. Processor: 2,3 GHz 8-Core Intel Core i9. Memory: 16 GB 2667 MHz DDR4.

From the image above, the result is:

  • ExecutedWithoutPipe was computed 60.675.291 times in 166ms with a duration of 18.3ns on each operation.
  • ExecutedWithPipe was computed 603.291 times in 166ms with a duration of 1825ns on each operation.

No contest. No wonder. We already know who’s the winner at the first place. Executing the operation without using Pipe performs 100x better than using it. Golang’s reflection is definitely expensive.

It’s back to you again. We can still consider 1825ns on each operation as quite fast depends on the context and situation.

Clojure’s Threading Macros offers a neat way to construct a series of function’s execution, which is so syntactic sugar and encourages us to love coding again. 😍

Mimicking that feature in Golang is not really difficult, it’s just a matter of how we can build an abstraction on top of Golang’s reflection feature, which indeed the cost is very expensive in terms of performance.

But as long as this can make you happy and code more. Why not? Okay, this is an opinion✌️

If you have any inquiry, please reach me on Twitter or LinkedIn. Oh yeah, if you are interested in this library. You can find it on: https://github.com/parinpan/pipe

Thanks!

An #AlwaysInBeta Software Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store