Update: The post you are about to read explains just a tiny portion of what Arrows are, pure functions are just one instance of this quite complex classtype. I’m not trying to say that Arrows are just pure functions, I’m saying that in your program, they might be just that. Just to clarify, this post simplify the concept of Arrows — for you the novice reader — to digest it easily. I would recommend reading the comments just to know where to go next.
Of course, I’m not saying there is no logical reason to call function composition “Arrows”, it is just an abstraction and as we should know by now, this abstractions serve as the foundations of high level (sometimes useful) theory, but even so, it is not easy for the average Joe to understand all this mombo-jombo of Category Theory.
I really got the idea not so long ago, and not by studying Category Theory papers1, but by reading this webpost about prime number checking, I saw that the (really smart) author was using the Arrow operators (»>), (&&&), (***) as composition of functions and suddenly it snapped my head around.
So… if you are familiar with how function composition works, this should be of no surprise to you:
>> let plus5times3 = (*3) . (+5) >> :t plus5time3 >> plus5time3 :: Integer -> Integer
But what happens if we rename the (.) high-order function with (»>) and instead of applying from right to left, we do it (the more natural way) from left to right:
>> :m + Control.Arrow >> :m + Data.Char >> let plus5times3A = (+5) >>> (*3) >> :t plus5times3A >> plus5times3A :: Integer -> Integer
I have declared the exact same thing, I just changed the order of the partially applied functions, and renamed the (.) function to something more fancier (»>).
So what? Is this what Arrows is all about? I would say almost, you see you can do more powerful things with the Arrow abstraction.
First, let’s imagine we have a split function, that converts an input value into a tuple with the value repeated twice
>> let split x = (x, x)
Now, we could use some methods of the Control.Arrow package, to start doing alterations on just a position of a tuple
>> first length ([1,2,3], "3 numbers") >> (3, "3 numbers") >> second (map toUpper) ([1,2,3], "3 numbers") >> ([1,2,3], "3 NUMBERS") >> let plus5AndOriginal = split >>> first (+5) >> let originalAndTimes3 = split >>> second (*3) >> :t plus5AndOriginal >> plus5AndOriginal :: Integer -> (Integer, Integer) >> :t originalAndTimes3 >> originalAndTimes2 :: Integer -> (Integer, Integer) >> plus5AndOriginal 5 >> (10, 5) >> originalAndTimes3 5 >> (5, 15)
Ok, that looks cool and all, but what is this useful for? Let’s create some more functions and show you.
>> let plus5AndTimes3 = split >>> first (+5) >>> second (*3) >> plus5AndTimes3 7 >> (12, 21)
With this notation, we just don’t have function composition, but we have functions that can get one value, and return multiple applications of functions to the same value we passed as a parameter, e.g plus5AndTimes3 is returning the application of two functions (+5) and (*3) to the input 7. We go with the famous drawing to give more insights on what is going on:
(first) ---- (+5) --> 12 | 7 ---(split) | (second) ---- (*2) --> 21
This pattern is so common, we have a function (&&&) that does the split, first and second for us
>> let plus5AndTimes3' = (+5) &&& (*3)
This is translated to “we apply the function (+5) AND the function (*3) to the given input”, is named (&&&) after this AND statement.
|---- 12 | 7 ---(+5) &&& (*3) | |---- 21
If we want to apply functions to the first value, we use the first function
>> plus5AndTimes3' >>> first (`mod` 2) $ 7 >> (0, 21) |----> 12 --> (first (`mod` 2)) --> 0 | 7 ---(+5) &&& (*2) | |---- 21
If we want to apply functions to the second value, we use the second function
>> plus5AndTimes3 >>> second (`div` 2) $ 7 >> (12, 10) |----> 12 --> | 7 --- (+5) &&& (*2) | |---- 14 --> (second (`div` 2)) --> 10
What if we want to apply functions to both values at the same time? We could use the first and second functions consecutively, however there is a better function for that called (***), and this is how it would be used:
>> plus5AndTimes3 >>> first (`mod` 2) >>> second (`div` 2) $ 7 >> (0, 10) >> plus5AndTimes3 >>> (`mod` 2) *** (`div` 2) $ 7 >> (0, 10)
At this point, you should see that there is no dark magic in the Arrow interface, once you realize is just function compositions, this operators looses the weird property and become something more natural.
One last bit, this works pretty good when you want to have multiple values at the same time from the same source, but how you go and use those values together in a function?, What if we use a “joinA” function?
>> let joinA fn (a, b) = fn a b >> plus5AndTimes3 >>> joinA (+) $ 7 >> 33 |------ 12 -----| | | 7 --- (plus5AndTimes2) joinA (+) -> 33 | | |------ 21 -----|
The real world use cases of this are beyond my knowledge, but I think it is a matter of time before I can start thinking on ways to solve problems using this approach, at least I have the foundations with all the “Category Theory” stripped out, although I’m pretty sure reading the theory would get me pretty far in the future.
This works because functions are an instance of the Arrow typeclass, I can imagine there could be other implementations using the same typeclass that would do distributed programming on each arrow, or fancier stuff like that, and all this on the background because we are using an interface. Neat stuff.