Skip to content

Instantly share code, notes, and snippets.

@dawedawe
Last active July 24, 2023 08:31
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dawedawe/1412f1748a316e128cdefc0d22277fdc to your computer and use it in GitHub Desktop.
Save dawedawe/1412f1748a316e128cdefc0d22277fdc to your computer and use it in GitHub Desktop.
F# Advent Calender 2022

Tortilla Flow

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment