Recently, I came across a use for indexed monads to do some mutations on a foreign data type and keep track of its actual type as it changed, with a final operation for extracting out the result. While my implementation of this is hacky, there are some cool uses of IxMonad out there that provide useful restrictions, like the way response state is represented in Hyper.
For this purpose, I made a demo where you can strictly specify how a hamburger should be built using IxMonad.
Never mind all the messy cooking bits, the actual building of a burger has multiple stages in my mind:
- An empty plate
- A bottom bun
- A patty
- Some or no cheese
- ...same for lettuce, tomato, and pickles
- A top bun
And we can represent that as data types with no constructors:
data Ready data EmptyPlate data BottomBunOn data PattyOn data CheeseOn data OnionOn data LettuceOn data TomatoOn data PicklesOn data TopBunOn
As for what the ingredients actually are, they can just be anything, so I'll make a newtype for string, with a convenent alias for the list of ingredients that make up the spec of a burger recipe.
newtype Ingredient = Ingredient String derive instance newtypeIngredient :: Newtype Ingredient _ type BurgerSpec = List Ingredient
First, we start off with a familiar newtype definition:
newtype IxBurgerBuilder i o spec = IxBurgerBuilder spec
As an indexed structure, it needs to have indexing parameters, which we have here as
o. The generic
spec is here to allow just about anything to be shoved into the burger builder, so one could work with not just
List, but just about any structure you want to shove in.
Next, we'll need a way to extract the spec from our type to use in any other context, like many other IxMonad implementors. This is pretty normal looking:
runIxBurgerBuilder :: forall prev next spec. IxBurgerBuilder prev next spec -> spec runIxBurgerBuilder (IxBurgerBuilder spec) = spec
The one thing left to do now is to implement IxMonad like so:
instance ixMonadIxBurgerBuilder :: IxMonad IxBurgerBuilder where ipure = IxBurgerBuilder ibind (IxBurgerBuilder spec) f = IxBurgerBuilder <<< runIxBurgerBuilder $ f spec
ipure ends up being a normal
a -> f a, and the
ibind definition is not much other than to apply the transformation function to the inner element, extract the value out of the resulting
IxBurgerBuilder carrying the wrong types, and re-wrap it to have the correct types.
Now that the type machinery is done, we can start writing the most fun part of the application: boilerplate!!! To go from the
Ready state to the
EmptyPlate state, we defined a function like so:
getEmptyPlate :: IxBurgerBuilder Ready EmptyPlate BurgerSpec getEmptyPlate = IxBurgerBuilder mempty
As you can see, the input and output type represent what we wanted to write, and the inner value is our
BurgerSpec from above.
And just because we don't want to die of boilerplate, we'll define one simple method for adding items to our spec:
addIngredient :: forall i o. String -> BurgerSpec -> IxBurgerBuilder i o (BurgerSpec) addIngredient x xs = IxBurgerBuilder $ Ingredient x : xs
This way, the
o are inferred from the usage, but we can easily define functions as simply
addIngredient "Name" with the proper type annotations. And so, the body of the commands bloc looks like so:
-- ADDING THE BUN placeEmptyBun :: BurgerSpec -> IxBurgerBuilder EmptyPlate BottomBunOn BurgerSpec placeEmptyBun = addIngredient "Bottom Bun" -- ADD SOME SAUCES DIRECTLY ON THE BOTTOM BUN addKetchup :: BurgerSpec -> IxBurgerBuilder BottomBunOn BottomBunOn BurgerSpec addKetchup = addIngredient "Ketchup" -- ... -- PUT THE PATTY ON, NO MORE SAUCES addPatty :: BurgerSpec -> IxBurgerBuilder BottomBunOn PattyOn BurgerSpec addPatty = addIngredient "Patty" -- NEXT IS THE CHEESE, OR NONE addCheese :: BurgerSpec -> IxBurgerBuilder PattyOn CheeseOn BurgerSpec addCheese = addIngredient "Cheese" noCheese :: BurgerSpec -> IxBurgerBuilder PattyOn CheeseOn BurgerSpec noCheese = IxBurgerBuilder -- some more definitions later... -- THEN FINISH THE BURGER WITH THE TOP BUN addTopBun :: BurgerSpec -> IxBurgerBuilder TomatoOn TopBunOn BurgerSpec addTopBun = addIngredient "TopBun"
And we're done! Once we call
addTopBun, our state will have been set to
TopBunOn and we will no longer be able to place anything more on the table with our current commands and index types.
By using the indexed bind operator
:>>=, we can write a bunch of these commands for our hamburger spec:
burgerSpec :: IxBurgerBuilder Ready TopBunOn BurgerSpec burgerSpec = getEmptyPlate :>>= placeEmptyBun :>>= addKetchup :>>= addPatty :>>= addCheese :>>= addOnions :>>= noLettuce :>>= addTomato :>>= addTopBun
And so, if we print out this spec after running the hamburger builder, we get the following output:
My burger consists of: TopBun Tomato Onion Cheese Patty Ketchup Bottom Bun
Which looks like a pretty correct burger to me!
Let's see an example of an incorrect spec:
wrongBurgerSpec :: IxBurgerBuilder Ready TopBunOn BurgerSpec wrongBurgerSpec = getEmptyPlate :>>= placeEmptyBun :>>= addKetchup :>>= addCheese -- Can't match PattyOn with BottomBunOn, -- since we haven't put on the patty, -- the most important part of a burger!!! :>>= addOnions :>>= noLettuce :>>= addTomato :>>= addTopBun
If we try to add cheese on top of the bottom bun directly, we will get a type error that the
PattyOn input type we wanted to use didn't match the
BottomBunOn type, so we can't construct a burger this way. Awesome!
Hopefully this has shown that indexed monads are pretty readily usable to solve problems where you want to restrict operations that can be performed before and after a type has been applied.
If nothing else, hopefully this has shown you one way to properly make hamburgers :-DDD
Hyper (https://github.com/owickstrom/hyper) features this kind of approach for working with the status of the response that you can write.
"roskis"/trashcan demo (https://github.com/justinwoo/purescript-roskis) -- for a more perverse example, you might be interested in my demo where I used an indexed monad to keep track of the type of the mutable record I wrote to, preventing more operations when I pull the record out.