title |
---|
24 Days of GHC Extensions: Rebindable Syntax |
As the first week of 24 Days of GHC Extensions (or as I like to call it, 24 DOGE) comes to an end, it's with great pleasure that I give the stage to our first guest poster - Benjamin Kovach. Ben has two posts lined up for us, so without further ado, lets look at his first extension.
{-# LANGUAGE RebindableSyntax, NoMonomorphismRestriction #-}
import Prelude hiding ((>>), (>>=), return)
import Data.Monoid
import Control.Monad ((<=<))
import Data.Map as M
Today we'll be talking about
RebindableSyntax
. RebindableSyntax
allows you to rewrite your own versions of operators in the Prelude (with
different types, even!). At first glance, this doesn't seem incredibly useful:
Why would we want to rebind operators when we can just define new functions or
operators to handle the functionality we want? Well, there are a couple of cases
where overloading an operator drastically changes how you can write code. In
particular, we'll be overloading >>
and return
in order to make do
notation do
-- basically -- whatever we want it to!
Recall that:
do x <- a == a >>= \x -> g x
g x
and:
do x == x >> y
y
As a first example of how we can leverage RebindableSyntax
, let's say you want to sum up some numbers. You could write a chain of +
s, but why would you do that when you can leverage do
notation and just write out numbers instead?
addNumbers = do
80
60
10
where (>>) = (+)
I'm joking, of course, but this produces exactly what you'd expect: 150
.
If you've been using Haskell for long enough, or have some experience with abstract algebra, you'll know that Integers form a Monoid
using 0 as the identity element and addition as the binary operator. Why not generalize the above, using Data.Monoid
's Sum
data type. For the next couple of examples, we'll use the following top-level bindings:
(>>) = mappend
return = mempty
We can perform the same computation as above using the Sum
wrapper:
someSum :: Sum Int
someSum = do
Sum 80
Sum 60
Sum 10
return
We're explicitly using return
just to illustrate that we can use it as the identity in the same way we use it as the identity in monads, but it's optional in this case. We can also use the Product
wrapper to multiply elements in sequence:
someProduct :: Product Int
someProduct = do
Product 10
Product 30
Why not try something non-numeric?
tummyMuscle :: String
tummyMuscle = do
"a"
"b"
Cool, we can use do
notation now to handle monoidal computations! What else can we do?
If you're coming from an imperative programming language, you might be wondering if we can apply a bunch of functions in sequence. Well, sure we can! Using flipped composition as >>
, we can apply functions to an input in sequence and output the result. For the next few examples, we'll use the following bindings:
(>>) = flip (.)
return = id
arithmetic = do
(+1)
(*100)
(/300)
return
Here, the input is numeric and all functions operate on a number. What if we want to take a list and output a string? No problem:
check = do
sum
sqrt
floor
show
As long as the domain/range pairs match up in the usual way, we have no problem. So now we can compose normal functions, that's cool! But what if we want to compose -- say -- Kleisli
arrows (functions of the form Monad m => a -> m b
)? We can do this as well, why not?
(>>) = (<=<)
kleisliExample :: (Ord a, Ord b) => Map b c -> Map a b -> a -> Maybe c
kleisliExample mp1 mp2 = do
flip M.lookup mp1
flip M.lookup mp2
Another thing you might be wondering is if we can replace >>=
with =>>
, and return
with extract
(from Control.Comonad
) and get some meaningful expression from it. The short answer is: Not really. If you think about the following equivalence:
do x <- a == a >>= \x -> g x
g x
Recall that (=>>) :: Comonad w => w a -> (w a -> b) -> w b
. If we replace >>=
with =>>
, we get:
a =>> \x -> g x
See the problem? The x
we're "extracting" is just the a
we passed in. So do{ x <- a; g x }
is exactly equivalent to g a
, which isn't very useful.
It's a relatively common problem in Haskell to think you have a Monad
instance for some data type, but in reality, additional constraints make this impossible. A good example is Set
from Data.Set
.
One might expect that Set
s admit a monad instance given that []
does -- we want to be able to, for example, write this:
import Data.Set as S
main = print $ do
x <- S.fromList [1, 2, 3]
y <- S.fromList [4, 5, 6]
return (x * y)
...but that doesn't work because Set
s require Ord
constrained elements. However, we can write a function
setBind :: Ord b => Set a -> (a -> Set b) -> Set b
setBind s f = S.foldr S.union S.empty (S.map f s)
...which obeys the monad laws as long as b
is orderable. Using RebindableSyntax
, we can use this as >>=
and pretend that Set
is a real monad.
main = print $ do
x <- S.fromList [1, 2, 3]
y <- S.fromList [4, 5, 6]
return (x * y)
where (>>=) = setBind
return = S.singleton
Another useful feature of RebindableSyntax
is that it allows "extended" monads to be used as if they were just plain old monads. For instance, Indexed Monads come equipped with a >>>=
operator and an ireturn
function which behave -- as one might imagine -- similarly to >>=
and return
. Using RebindableSyntax
we can bind the normal monadic functions to these and have nice readable code that performs functions outside the scope of regular monads. To talk about this at a deeper level is beyond the scope of this post, but if you're interested, check out Ollie Charles's blog post on using indexed free monads to quickcheck JSON.
Finally, I wanted to shamelessly plug my own work and mention my drum machine language Bang
, which uses two separate operators to compose beats sequentially (<>
) and concurrently (><
). Using these operators as >>
, we can compose drum machine patterns in a readable way using do
notation (to run this example you'll have to cabal install bang
):
music1, music2 :: Music Dur PercussionSound
music1 = do
m4 bd bd bd bd
hc
m4 bd qr bd qr >< m4 hc hc hc hc
where (>>) = (<>)
music2 = quintuplets $ 5 #> do
bd
cc
where (>>) = (><)
music :: IO ()
music = bang $ 4 #> do
music1
music2
where (>>) = (<>)
This will play music1
's rows sequentially, music2
s rows concurrently, and finally play music1
, then music2
, sequentially, in music
.
This post is part of 24 Days of GHC Extensions - for more posts like this, check out the calendar.