Today we'll be looking into Kmett's adjunctions library, particularly the meat of the library in Data.Functor.Adjunction.
This post as a literate haskell file here, so if you prefer to have the code running in ghci as you read along then go for it! Like any good haskell file we need half a dozen language pragmas and imports before we get started.
{-# language DeriveFunctor #-}
{-# language TypeFamilies #-}
{-# language MultiParamTypeClasses #-}
{-# language InstanceSigs #-}
{-# language FlexibleContexts #-}
module Battleship where
import Data.Functor (void)
import Data.Functor.Adjunction
import Data.Functor.Rep
import Data.Distributive
import Control.Arrow ((&&&))
I've been struggling to understand this library for a little while now and have been poking at it from different angles trying to gain some intuition. My previous post on Zippers using Representable and Cofree is part of that adventure so I'd suggest you read that first if you haven't yet.
Like most higher-level mathematic concepts Adjunctions themselves are just an abstract collection of types and shapes that fit together in a certain way. This means that they have little practical meaning on their own, but provide a useful set of tools to us if we happen to notice that some problem we're working on matches their shape. The first time I dug into adjunctions I went straight to the typeclass to check out which requirements and methods it had. Here are the signatures straight from the source code in Data.Functor.Adjunction
class (Functor f, Representable u) => Adjunction f u | f -> u, u -> f where
unit :: a -> u (f a)
counit :: f (u a) -> a
leftAdjunct :: (f a -> b) -> a -> u b
rightAdjunct :: (a -> u b) -> f a -> b
Hrmm... not the most illuminating. Unfortunately there's not much in the way of documentation to help us out, but that's because the type signatures pretty much explain how to USE adjunctions, but tragically they don't tell us WHERE or HOW to use them. For this I think examples are the most useful, and that's where I'll try to help out.
The first place to look for examples is in the 'instances' section of the type-class itself, let's see what's in there:
Adjunction Identity Identity
Adjunction ((,) e) ((->) e)
Adjunction f g => Adjunction (IdentityT f) (IdentityT g)
Adjunction f u => Adjunction (Free f) (Cofree u)
Adjunction w m => Adjunction (EnvT e w) (ReaderT e m)
Adjunction m w => Adjunction (WriterT s m) (TracedT s w)
Adjunction f g, Adjunction f' g') => Adjunction (Compose f' f) (Compose g g')
(Adjunction f g, Adjunction f' g') => Adjunction (Sum f f') (Product g g') (
Hrmm, still not the most helpful, most of these instances depend on
some underlying functor ALREADY having an adjunction so those won't tell
us how to implement one. I see one for
Adjunction Identity Identity
, but something tells me that's
not going to provide much depth either. Let's dive into the one
remaining example: Adjunction ((,) e) ((->) e)
This one looks a little funny if you're not used to type sigs for
functions and tuples, but it gets a lot easier to read if we substitute
it into the typeclass methods. To specialize for the tuple/function
adjunction we'll replace every f a
with (e, a)
and each u a
with e -> a
:
-- Tuple/Function adjunction specializations:
tfUnit :: a -> (e -> (e, a))
tfCounit :: (e, (e -> a)) -> a
tfLeftAdjunct' :: ((e, a) -> b) -> a -> (e -> b)
tfLeftAdjunct, tfRightAdjunct' :: (a -> (e -> b)) -> (e, a) -> b tfRightAdjunct,
Hrmm, okay! That's a bit confusing but it's something we can work with. Let's try to implement the functions! We'll implement our specialized versions so as not to collide with the existing instance.
Unit and Counit are good starting points for understanding an adjunction. The minimal definition of an adjunction is (unit AND counit) OR (leftAdjunct AND rightAdjunct). That lets us know that unit and counit can themselves represent the entire adjunction (i.e. leftAdjunct and rightAdjunct can be implemented in terms of unit and counit; or vice versa).
Starting with unit
we see from the type
a -> (e -> (e, a))
that we need to take an arbitrary
'a' and embed it into a function which returns a tuple of the same type
as the function. Well, there's pretty much only one way I can think to
make this work!
= \e -> (e, a) tfUnit a
Solid! We just converted the type signature into an implementation.
One down, three to go. This may not provide much insight, but don't
worry we'll get there yet. Next is counit which essentially does the
opposite, exactly one implementation seems clear to me:
(e, (e -> a)) -> a
= eToA e tfCounit (e, eToA)
If we stop here for a minute we can notice a few things, we built
this adjunction out of two functors, (e, a)
and
e -> a
. These functors have a unique relationship to one
another in that they both hold pieces of the whole picture, the
tuple has an 'e' but doesn't know what to do with it, while
e -> a
knows what to do with an 'e' but doesn't have one
to work with! Only when we pair the functors together do we have the
full story!
The next thing to notice is that these functors are only readily
useful when nested in a specific ordering, we can write a counit which
takes (e, (e -> a)) -> a
, BUT if we tried to put the
function on the outside instead: (e -> (e, a)) -> a
;
we have no way to get our 'a' out without having more information since
the 'e' is now hidden inside! This non-symmetric relationship shows us
that the nesting of functors matters. This is why we refer to the
functors in an adjunction as either left adjoint
or
right adjoint
; (f
and u
respectively).
In our case (e,)
is left adjoint and
(e ->)
is right adjoint. This is probably still a bit
confusing and that's okay! Try to hold on until we get to start playing
Battleship and I promise we'll have a more concrete example! One more
thing first, let's see how leftAdjunct and rightAdjunct play out for our
tuple/function adjunction.
Here's a refresher of the types:
tfLeftAdjunct :: ((e, a) -> b) -> a -> (e -> b)
tfRightAdjunct :: (a -> (e -> b)) -> (e, a) -> b
Now that we've written 'unit' and 'counit' we can implement these other functions in terms of those. I'll provide two implementations here; one using unit/counit and one without.
= fmap f . tfUnit
tfLeftAdjunct f = tfCounit . fmap f tfRightAdjunct f
= \e -> eaToB (e, a)
tfLeftAdjunct' eaToB a = aToEToB a e tfRightAdjunct' aToEToB (e, a)
We can see from the first set of implementations that
leftAdjunct
somehow 'lifts' a function that we give it from
one that operates over the left-hand functor into a result within the
right-hand functor.
Similarly rightAdjunct
takes a function which results in
a value in left-hand functor, and when given an argument embedded in the
left-hand functor gives us the result. The first set of implementations
know nothing about the functors in specific, which shows that if we
write unit and counit we can let the default implementations take over
for the rest.
If you're keen you'll notice that this adjunction represents the curry and uncurry functions! Can you see it?
tfLeftAdjunct :: ((e, a) -> b) -> a -> (e -> b)
curry :: ((a, b) -> c) -> a -> b -> c
tfRightAdjunct :: (a -> (e -> b)) -> (e, a) -> b
uncurry :: (a -> b -> c) -> (a, b) -> c
I haven't gotten to a point where I can prove it yet, but I believe all adjunctions are actually isomorphic to this curry/uncurry adjunction! Maybe someone reading can help me out with the proof.
Again, it's fun to see this play out, but where are the practical applications?? Let's play a game. It's time to see if we can match these shapes and patterns to a real(ish) problem. We're going to make a mini game of Battleship, an old board game where players can guess where their opponents ships are hiding within a grid and see if they can hit them! We'll start by setting up some data-types and some pre-requisite instances, then we'll tie it all together with an Adjunction!
data Row = A | B | C
deriving (Show, Eq)
data Column = I | II | III
deriving (Show, Eq)
-- I'm going to define this as a Functor type to save time later, but for now
-- we'll use the alias Coord;
data CoordF a = CoordF Row Column a
deriving (Show, Eq, Functor)
type Coord = CoordF ()
Each cell can hold a Vessel of some kind, maybe a Ship or Submarine; It's also possible for a cell to be empty.
data Vessel = Ship | Sub | Sunk | Empty
deriving (Show, Eq)
We'll start with a 3x3 board to keep it simple, each row is represented by a 3-tuple. We've learned by now that making our types into Functors makes them more usable, so I'm going to define the board as a functor parameterized over the contents of each cell.
data Board a = Board
(a, a, a)
(a, a, a)
(a, a, a)deriving (Eq, Functor)
I'm going to add a quick Show instance, it's not perfect but it lets us see the board!
instance (Show a) => Show (Board a) where
show (Board top middle bottom) =
" I | II | III\n"
++ "A " ++ show top ++ "\n"
++ "B " ++ show middle ++ "\n"
++ "C " ++ show bottom ++ "\n"
Here's a good starting position, the board is completely empty!
startBoard :: Board Vessel
= Board
startBoard Empty, Empty, Empty)
(Empty, Empty, Empty)
(Empty, Empty, Empty) (
It's at this point we want to start making guesses using a Coord and seeing what's in each position! How else are we going to sink the battleship? Well, when we start talking about 'Indexing' into our board (which is a functor) I think immediately of the Representable typeclass from Data.Functor.Rep. Don't let the name scare you, one of the things that Representable gives you is the notion of indexing into a functor.
instance Representable Board where
-- We index into our functor using Coord
type Rep Board = Coord
-- Given an index and a board, pull out the matching cell
index (Board (a, _, _) _ _) (CoordF A I _) = a
index (Board (_, a, _) _ _) (CoordF A II _) = a
index (Board (_, _, a) _ _) (CoordF A III _) = a
index (Board _ (a, _, _) _) (CoordF B I _) = a
index (Board _ (_, a, _) _) (CoordF B II _) = a
index (Board _ (_, _, a) _) (CoordF B III _) = a
index (Board _ _ (a, _, _)) (CoordF C I _) = a
index (Board _ _ (_, a, _)) (CoordF C II _) = a
index (Board _ _ (_, _, a)) (CoordF C III _) = a
-- Given a function which describes a slot, build a Board
= Board
tabulate desc CoordF A I ()), desc (CoordF A II ()), desc (CoordF A III ()))
(desc (CoordF B I ()), desc (CoordF B II ()), desc (CoordF B III ()))
(desc (CoordF C I ()), desc (CoordF C II ()), desc (CoordF C III ())) (desc (
If you find it easier to implement unit and counit (which we'll
explore soon) you can implement those and then use
indexAdjunction
and tabulateAdjunction
provided by Data.Functor.Adjunction as your implementations for your
Representable instance.
For Representable we also have a prerequisite of Distributive from Data.Distributive, All Representable functors are also Distributive and this library has decided to make that an explicit requirement.
No problem though, it turns out that since every Representable is
Distributive that Data.Functor.Rep has a distributeRep
function which provides an appropriate implementation for us for free!
We just need to slot it in:
instance Distributive Board where
= distributeRep distribute
Phew! A lot of work there, but now we can do some cool stuff! Let's say that as a player we want to build a game board with some ships on it. We now have two choices, we can either define a board and put some ships on it, or define a function which says what's at a given coordinate and use that to build a board. Let's do both, for PEDAGOGY!
myBoard1 :: Board Vessel
= Board
myBoard1 Empty, Empty, Ship)
(Sub, Empty, Sub)
(Ship, Empty, Empty)
(
-- Now we'll define the same board using a function
define :: Coord -> Vessel
CoordF A III _) = Ship
define (CoordF B I _) = Sub
define (CoordF B III _) = Sub
define (CoordF C I _) = Ship
define (-- Otherwise it's Empty!
= Empty
define _
-- Now we build up a board using our descriptor function.
-- Notice that (myBoard1 == myBoard2)
myBoard2 :: Board Vessel
= tabulate define myBoard2
Okay this is already pretty cool; but I DID promise we'd use an adjunction here somewhere, but for that we need TWO functors. Remember how CoordF is actually a functor hidden undernath Coord? We can use that! This functor doesn't make much sense on its own, but the important bit is that it's a functor which contains part of the information about our system. Remember that only one of our functors needs to be Representable in an Adjunction, so we can take it easy and don't need to worry about Distributive or Representable for CoordF
Now for the good stuff; let's crack out Adjunction and see if we can write an instance!
I'm lazy, so I'm going to rely on Representable to do the dirty work, Embedding an a into a Board filled with coordinates and values doesn't make a ton of sense, but the most sensible way that I can think of to do that is to put the a in every slot where the Coord represents the index of the cell its in.
instance Adjunction CoordF Board where
unit :: a -> Board (CoordF a)
= tabulate (\(CoordF row col ()) -> CoordF row col a) unit a
Counit actually makes sense in this case! We have our two pieces of info which form the parts of the adjunction; The board contains the values in ALL positions and the CoordF contains info which tells us exactly WHICH position we're currently interested in.
For counit I'm just going to use index to pull the value out of the underlying board.
counit :: CoordF (Board a) -> a
CoordF row col board) = index board (CoordF row col ()) counit (
Done! We've written our Adjunction, let's keep building to game to see how we can use the system! Here're the other type sigs for our Adjunction:
leftAdjunct :: (CoordF a -> b) -> a -> Board b
rightAdjunct :: (a -> Board b) -> CoordF a -> b
First let's observe unit and co-unit in action!
unit
Always does the naive thing, so if we pass it a
Vessel it'll just set the whole board to that value; note that each slot
is also labelled with its index!
> unit Ship :: Board (CoordF Vessel)
λA | B | C
I (CoordF A I Ship,CoordF A II Ship,CoordF A III Ship)
II (CoordF B I Ship,CoordF B II Ship,CoordF B III Ship)
III (CoordF C I Ship,CoordF C II Ship,CoordF C III Ship)
If we already have our game board and also have an index then counit folds down the structure by choosing the index specified by the outer CoordF Functor.
-- Remember our board:
> myBoard1
λA | B | C
I (Empty,Empty,Ship)
II (Sub,Empty,Sub)
III (Ship,Empty,Empty)
> counit . CoordF A III $ myBoard1
λShip
So what about leftAdjunct and rightAdjunct? Conceptually you can think of these as functions which let you operate over one piece of information and the Adjunction will form the other piece of information for you! For instance leftAdjunct:
leftAdjunct :: (CoordF a -> b) -> a -> Board b
lets you build a value in the right adjoint functor by specifying how
to handle each index, this is similar to tabulate
from from
Representable. Earlier we used tabulate to generate a game board from a
shoot function, we can do the same thing using leftAdjunct, we could
re-implement our shoot
function from above in terms of
leftAdjunct:
myBoard3 :: Board Vessel
= leftAdjunct define () myBoard3
Right adjunct works similarly, but in reverse! Given a way to create a board from a solitary value we can extract a value from the board matching some CoordF. Just like leftAdjunct lines up with 'tabulate', rightAdjunct lines up with 'index', but with a smidge of extra functionality.
rightAdjunct :: (a -> Board b) -> CoordF a -> b
I don't have any illuminating uses of rightAdjunct for our Battleship example, but you can use it to reimplement 'index' from Representable if you like!
myIndex :: Board a -> CoordF () -> a
= rightAdjunct (const board) coord myIndex board coord
Cool, now let's try and make this game a little more functional!
Already we've got most of the basics for a simple game of battleship, earlier we defined a game board in terms of a 'firing' function, now let's write a function which takes a game board and mutates it according to a player's layout.
War has changed over the years so our version of battleship is going to be a bit more interesting than the traditional version. In our case each player places ships OR submarines on each square, and when firing on a square they may fire either a torpedo (hits ships) OR a depth charge (hits subs).
This means that we need a way to check not only if a cell is
occupied, but also if the vessel there can be hit by the weapon which
was fired! For this we'll take a look at the useful but vaguely named
zapWithAdjunction
function.
This function has its roots in an 'Pairing' typeclass which
eventually was absorbed by Adjunction. The idea of a Functor Pairing is
that there's a relationship between the structure of the two paired
functors regardless of what's inside. Sounds like an adjunction right??
zapWithAdjunction
looks like this:
zapWithAdjunction :: Adjunction f u => (a -> b -> c) -> u a -> f b -> c
or for our types:
zapWithAdjunction :: (a -> b -> c) -> Board a -> CoordF b -> c
So it pairs a Board and Coord together, but applies a function across the values stored there. It uses the adjunction to do this, so it will automagically choose the 'right' value from the Board to apply with the value from the CoordF!
First we need weapons!
data Weapon = Torpedo | DepthCharge
deriving (Show, Eq)
Now we can write something like this:
checkHit :: Vessel -> Weapon -> Bool
Ship Torpedo = True
checkHit Sub DepthCharge = True
checkHit = False
checkHit _ _
shoot :: Board Vessel -> CoordF Weapon -> Bool
= zapWithAdjunction checkHit shoot
And of course we can try that out!
> myBoard1
λA | B | C
I (Empty,Empty,Ship)
II (Sub,Empty,Sub)
III (Ship,Empty,Empty)
> shoot myBoard1 (CoordF A III Torpedo)
λTrue
> shoot myBoard1 (CoordF A III DepthCharge)
λFalse
It's really unique how Adjunctions let us specify our data as a functor like this!
Now what if we want to see what happens at each spot in the board if we hit it with a Torpedo OR a DepthCharge? No problem;
hitMap :: Board (Bool, Bool)
= fmap (flip checkHit Torpedo &&& flip checkHit DepthCharge) myBoard1 hitMap
We use (&&&) from Control.Arrow which combines two functions which take the same input and makes a single function which returns a tuple!
(&&&) :: Arrow a => a b c -> a b c' -> a b (c, c')
Now we've got a Board (Bool, Bool)
, Since the right
adjoint functor (Board) is distributive, flipping the the tuple to the
outside is trivial:
hitMap' :: (Board Bool, Board Bool)
= unzipR hitMap hitMap'
Now we've got two Boards, showing where we could get a hit if we used a Torpedo or DepthCharge respectively.
Most of the functions we've written are a bit contrived. Sometimes the adjunction-based approach was a bit clunkier than just writing a simple function to do what you needed on a Board, but I hope this provides some form of intuition for adjunctions. Good luck!
Hopefully you learned something 🤞! If you did, please consider checking out my book: It teaches the principles of using optics in Haskell and other functional programming languages and takes you all the way from an beginner to wizard in all types of optics! You can get it here. Every sale helps me justify more time writing blog posts like this one and helps me to continue writing educational functional programming content. Cheers!