Protocols are a very powerful feature of Elixir. Iāve introduced them before here, and I looked at how you might use them to solve the expression problem here.
Today I want extend an idea I touched on in that last article and explain it better. It doesnāt matter if you havenāt read the previous article, but for context at the end of it I tried to show an example of triple dispatch. The idea was that you could decide which specific function gets executed according to the types of three arguments passed to a function. Usually we use single dispatch ā we pass an argument into a function and because that argument is a string (say), it does one thing, if it were a different type, then it would do another. This is the essence of polymorphism ā a single interface, but varying implementations depending on the type of one thing. But what would it look like if we needed the specific implementation of a function we wanted to run to be determined by the types of three things? Is there ever even a case where that may be useful? Well letās seeā¦
Our challenge is to implement a library to help us zip together all kinds of data types. To look at what zipping a data type would do, letās take a list as an example. Letās say we want to zip them together but apply a transformation as we do that. The transformation will be to add
. Zipping would be taking the first element of list_1
, then adding it to the first element of list_2
, then taking the second element of list_1
and adding it to the second element of list_2
. We keep adding the elements from each list pairwise until we run out of elements in the first list. We are returned a new list containing the results of those transformations. We could implement it like this:
Now letās imagine we want to implement this in such a way that it is extensible. That should be simple enough if we implement the Zip
as a protocol, then users of our library could implement Zip
for their own data types:
This is certainly okay, but there is more we can do. We have allowed extensibility in one direction ā for the type of collection that we can zip together. But there are three dimensions to this problem:
- The collections we are zipping together (think list, stream, mapā¦)
- The operation we want to perform when we zip (add, subtract, joinā¦)
- The datatype of the elements in the collections (string, integer, decimalā¦)
Using protocols we can create a library that is extendable in every one of those directions, without violating the open closed principle. It also comes with a huge benefit as we will see, even if the style is a little strange.
We already have the type of collection we can zip together extensible, now letās have a go at making the operation we want to perform extensible. We will do that by also making it a protocol.
Okay, so now letās bring this all together. We slightly change the way we call Zip.apply
now, passing in the protocol name as the last argument, and calling calculate
on that:
This is very cool. In one fell swoop all three dimensions are extensible. To see how, letās add an implementation of the Add
protocol for the decimal
data type. This will be good for if we want to add two lists of decimals:
Okay, what about if we want a new operation? Well we just define it as a protocol with a calculate
function. Letās do subtract:
Amazing. But now is the really cool thing. Letās implement the Zip
protocol for a map:
Once thatās done, we get all of those operations we defined for the list available for map, for free!
Iāve been experimenting with this approach over on a branch of my zip library github. I wonder if Iāll ever use this in a real applicationā¦?