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.

How do you build a burger?

Never mind all the messy cooking bits, the actual building of a burger has multiple stages in my mind:

Nothing

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

Making our indexed monad

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 i and 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.

Adding commands to build our burger

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 i and 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!

Takeaway (of digital burgers)

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

Links