Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Using IxMonad to enforce good hamburger building in Purescript

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:

placeEmptyBun :: BurgerSpec -> IxBurgerBuilder EmptyPlate BottomBunOn BurgerSpec
placeEmptyBun = addIngredient "Bottom Bun"

addKetchup :: BurgerSpec -> IxBurgerBuilder BottomBunOn BottomBunOn BurgerSpec
addKetchup = addIngredient "Ketchup"

-- ...

addPatty :: BurgerSpec -> IxBurgerBuilder BottomBunOn PattyOn BurgerSpec
addPatty = addIngredient "Patty"

addCheese :: BurgerSpec -> IxBurgerBuilder PattyOn CheeseOn BurgerSpec
addCheese = addIngredient "Cheese"

noCheese :: BurgerSpec -> IxBurgerBuilder PattyOn CheeseOn BurgerSpec
noCheese = IxBurgerBuilder

-- some more definitions later...

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:
  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


Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
Help us understand the problem. What are the problem?