Given the following scenario:
You're an F# developer from Germany.
You fall in love with a Mexican girl ❤️.
You need to learn your way around the culture, the language and, of course, the outstanding food.
In particular, you need to learn all the twisty paths a tortilla can take.
How do you solve this?
There are already nice graphical flow charts for this, but steering the fate of a tortilla interactively should be way more fun, right?
So let me introduce tortillaflow.com.
I decided to implement the flow chart from this reddit thread as a Fable app.
You will notice there's a bit of controversy about it's correctnes but I'll leave that for the product owner to sort out ;)
To model a flow chart in software you basically need to come up with a way to decide which question to ask next and to keep track of the answers. The interaction can be nicely modeled with the Elmish approach. I chose the Feliz flavour for that.
Modeling stuff is one of the major strong points of F#. So here's the model for the tortilla:
type Tortilla =
{ Condition: Condition option
Folding: Folding option
Fried: Fried option
Fixings: Fixings option
SizeAndShape: SizeAndShape option
Dish: Dish option }
The answers will cause updates to the model. As long as a question wasn't asked yet, (for example: "Is it fried?"), the record fields representing the answers stay None
.
And as long as the Tortilla hasn't reached the state of an official tortilla dish, like a taco, the Dish
stays None
.
Discriminated unions are perfect to model the possible answers. Here's what the SizeAndShape
answers look like:
type SizeAndShape =
| SmallTrianglesOvalsOrRectangles
| RolledUp
| Handsized
| Round
Some of the possible fixing combinations (for example Rice
and NoRice
) shouldn't be possible to be represented.
To protect against such combinations when adding a fixing to the tortilla I used a Single Case Discriminated Union with a private case constructor. For more beautiful modeling techniques go read Scott Wlaschin's superb Domain Modeling Made Functional.
type Feature =
| Empty
| Meat
| MeatStrips
| NoMeatStrips
| Cheese
| Rice
| NoRice
| Soup
| SauceOnTop
| NoSauceOnTop
type Fixings =
private
| Features of Feature Set
static member Create = Features Set.empty
module Fixings =
let add (fixings: Fixings) (toAdd: Feature) =
match (toAdd, fixings) with
| (Empty, Features fs) when not (Set.isEmpty fs) -> System.InvalidOperationException() |> raise
| (MeatStrips, Features fs) when Set.contains NoMeatStrips fs -> System.InvalidOperationException() |> raise
| (NoMeatStrips, Features fs) when Set.contains MeatStrips fs -> System.InvalidOperationException() |> raise
| (Rice, Features fs) when Set.contains NoRice fs -> System.InvalidOperationException() |> raise
| (NoRice, Features fs) when Set.contains Rice fs -> System.InvalidOperationException() |> raise
| (SauceOnTop, Features fs) when Set.contains NoSauceOnTop fs -> System.InvalidOperationException() |> raise
| (NoSauceOnTop, Features fs) when Set.contains SauceOnTop fs -> System.InvalidOperationException() |> raise
| (f, Features fs) -> Features(Set.add f fs)
To determine the next question or check if we reached the state of a real tortilla dish is obviously a job for Active Patterns. Nothing special to see here. Go to the Fantomas sources if you want to see a more sophisticated use of them.
The Elmish architecture makes it very easy to have a timeline of all models that were ever created in your application. I still remember a talk of forki at the .NET user group in Cologne some years ago.
He mentioned the possibility to just serialize the model (and by that the state of your application) to JSON, transfer it to the developer and using that to reproduce and debug issues a user might face. The elegance of this still blows my mind.
So to provide a go-back feature to let the user step back in time to a previous state (aka a previous question) is just a matter of collecting the models in a stack and popping them off as the user tracks her way back. Restarting the flow chart is even simpler, just reinstate the initial model.
Speaking of mind blowing or let's say mind reading things, the Spanish translation was largely done by Github Copilot. It's just insane how good this thing is in many, many situations.
Having the code for the web app, it's just a few lines more and you end up with a Tortilla Computation Expression which lets you define your burrito in a little DSL like this:
let t = tortilla {
condition Soft
add Meat
add Rice
notfried
}
And with that, I'll leave you to explore the various tasty paths a tortilla can take.
Buen provecho :)