What's a sandwich? by Mark Seemann
Ultimately, it's more about programming than food.
The Sandwich was named after John Montagu, 4th Earl of Sandwich because of his fondness for this kind of food. As popular story has it, he found it practical because it enabled him to eat without greasing the cards he often played.
A few years ago, a corner of the internet erupted in good-natured discussion about exactly what constitutes a sandwich. For instance, is the Danish smørrebrød a sandwich? It comes in two incarnations: Højtbelagt, the luxury version which is only consumable with knife and fork and the more modest, everyday håndmad (literally hand food), which, while open-faced, can usually be consumed without cutlery.
If we consider the 4th Earl of Sandwich's motivation as a yardstick, then the depicted højtbelagte smørrebrød is hardly a sandwich, while I believe a case can be made that a håndmad is:
Obviously, you need a different grip on a håndmad than on a sandwich. The bread (rugbrød) is much denser than wheat bread, and structurally more rigid. You eat it with your thumb and index finger on each side, and remaining fingers supporting it from below. The bottom line is this: A single piece of bread with something on top can also solve the original problem.
What if we go in the other direction? How about a combo consisting of bread, meat, bread, meat, and bread? I believe that I've seen burgers like that. Can you eat that with one hand? I think that this depends more on how greasy and overfilled it is, than on the structure.
What if you had five layers of meat and six layers of bread? This is unlikely to work with traditional Western leavened bread which, being a foam, will lose structural integrity when cut too thin. Imagining other kinds of bread, though, and thin slices of meat (or other 'content'), I don't see why it couldn't work.
FP sandwiches #
As regular readers may have picked up over the years, I do like food, but this is, after all, a programming blog.
A few years ago I presented a functional-programming design pattern named Impureim sandwich. It argues that it's often beneficial to structure a code base according to the functional core, imperative shell architecture.
The idea, in a nutshell, is that at every entry point (Main
method, message handler, Controller action, etcetera) you first perform all impure actions necessary to collect input data for a pure function, then you call that pure function (which may be composed by many smaller functions), and finally you perform one or more impure actions based on the function's return value. That's the impure-pure-impure sandwich.
My experience with this pattern is that it's surprisingly often possible to apply it. Not always, but more often than you think.
Sometimes, however, it demands a looser interpretation of the word sandwich.
Even the examples from the article aren't standard sandwiches, once you dissect them. Consider, first, the Haskell example, here recoloured:
tryAcceptComposition :: Reservation -> IO (Maybe Int) tryAcceptComposition reservation = runMaybeT $ liftIO (DB.readReservations connectionString $ date reservation) >>= MaybeT . return . flip (tryAccept 10) reservation >>= liftIO . DB.createReservation connectionString
The date
function is a pure accessor that retrieves the date and time of the reservation
. In C#, it's typically a read-only property:
public async Task<IActionResult> Post(Reservation reservation) { return await Repository.ReadReservations(reservation.Date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
Perhaps you don't think of a C# property as a function. After all, it's just an idiomatic grouping of language keywords:
public DateTimeOffset Date { get; }
Besides, a function takes input and returns output. What's the input in this case?
Keep in mind that a C# read-only property like this is only syntactic sugar for a getter method. In Java it would have been a method called getDate()
. From Function isomorphisms we know that an instance method is isomorphic to a function that takes the object as input:
public static DateTimeOffset GetDate(Reservation reservation)
In other words, the Date
property is an operation that takes the object itself as input and returns DateTimeOffset
as output. The operation has no side effects, and will always return the same output for the same input. In other words, it's a pure function, and that's the reason I've now coloured it green in the above code examples.
The layering indicated by the examples may, however, be deceiving. The green colour of reservation.Date
is adjacent to the green colour of the Select
expression below it. You might interpret this as though the pure middle part of the sandwich partially expands to the upper impure phase.
That's not the case. The reservation.Date
expression executes before Repository.ReadReservations
, and only then does the pure Select
expression execute. Perhaps this, then, is a more honest depiction of the sandwich:
public async Task<IActionResult> Post(Reservation reservation) { var date = reservation.Date; return await Repository.ReadReservations(date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
The corresponding 'sandwich diagram' looks like this:
If you want to interpret the word sandwich narrowly, this is no longer a sandwich, since there's 'content' on top. That's the reason I started this article discussing Danish smørrebrød, also sometimes called open-faced sandwiches. Granted, I've never seen a håndmad with two slices of bread with meat both between and on top. On the other hand, I don't think that having a smidgen of 'content' on top is a showstopper.
Initial and eventual purity #
Why is this important? Whether or not reservation.Date
is a little light of purity in the otherwise impure first slice of the sandwich actually doesn't concern me that much. After all, my concern is mostly cognitive load, and there's hardly much gained by extracting the reservation.Date
expression to a separate line, as I did above.
The reason this interests me is that in many cases, the first step you may take is to validate input, and validation is a composed set of pure functions. While pure, and a solved problem, validation may be a sufficiently significant step that it warrants explicit acknowledgement. It's not just a property getter, but complex enough that bugs could hide there.
Even if you follow the functional core, imperative shell architecture, you'll often find that the first step is pure validation.
Likewise, once you've performed impure actions in the second impure phase, you can easily have a final thin pure translation slice. In fact, the above C# example contains an example of just that:
public IActionResult Ok(int value) { return new OkActionResult(value); } public IActionResult InternalServerError(string msg) { return new InternalServerErrorActionResult(msg); }
These are two tiny pure functions used as the final translation in the sandwich:
public async Task<IActionResult> Post(Reservation reservation) { var date = reservation.Date; return await Repository.ReadReservations(date) .Select(rs => maîtreD.TryAccept(rs, reservation)) .SelectMany(m => m.Traverse(Repository.Create)) .Match(InternalServerError("Table unavailable"), Ok); }
On the other hand, I didn't want to paint the Match
operation green, since it's essentially a continuation of a Task, and if we consider task asynchronous programming as an IO surrogate, we should, at least, regard it with scepticism. While it might be pure, it probably isn't.
Still, we may be left with an inverted 'sandwich' that looks like this:
Can we still claim that this is a sandwich?
At the metaphor's limits #
This latest development seems to strain the sandwich metaphor. Can we maintain it, or does it fall apart?
What seems clear to me, at least, is that this ought to be the limit of how much we can stretch the allegory. If we add more tiers we get a Dagwood sandwich which is clearly a gimmick of little practicality.
But again, I'm appealing to a dubious metaphor, so instead, let's analyse what's going on.
In practice, it seems that you can rarely avoid the initial (pure) validation step. Why not? Couldn't you move validation to the functional core and do the impure steps without validation?
The short answer is no, because validation done right is actually parsing. At the entry point, you don't even know if the input makes sense.
A more realistic example is warranted, so I now turn to the example code base from my book Code That Fits in Your Head. One blog post shows how to implement applicative validation for posting a reservation.
A typical HTTP POST
may include a JSON document like this:
{ "id": "bf4e84130dac451b9c94049da8ea8c17", "at": "2024-11-07T20:30", "email": "snomob@example.com", "name": "Snow Moe Beal", "quantity": 1 }
In order to handle even such a simple request, the system has to perform a set of impure actions. One of them is to query its data store for existing reservations. After all, the restaurant may not have any remaining tables for that day.
Which day, you ask? I'm glad you asked. The data access API comes with this method:
Task<IReadOnlyCollection<Reservation>> ReadReservations(
int restaurantId, DateTime min, DateTime max);
You can supply min
and max
values to indicate the range of dates you need. How do you determine that range? You need the desired date of the reservation. In the above example it's 20:30 on November 7 2024. We're in luck, the data is there, and understandable.
Notice, however, that due to limitations of wire formats such as JSON, the date is a string. The value might be anything. If it's sufficiently malformed, you can't even perform the impure action of querying the database, because you don't know what to query it about.
If keeping the sandwich metaphor untarnished, you might decide to push the parsing responsibility to an impure action, but why make something impure that has a well-known pure solution?
A similar argument applies when performing a final, pure translation step in the other direction.
So it seems that we're stuck with implementations that don't quite fit the ideal of the sandwich metaphor. Is that enough to abandon the metaphor, or should we keep it?
The layers in layered application architecture aren't really layers, and neither are vertical slices really slices. All models are wrong, but some are useful. This is the case here, I believe. You should still keep the Impureim sandwich in mind when structuring code: Keep impure actions at the application boundary - in the 'Controllers', if you will; have only two phases of impurity - the initial and the ultimate; and maximise use of pure functions for everything else. Keep most of the pure execution between the two impure phases, but realistically, you're going to need a pure validation phase in front, and a slim translation layer at the end.
Conclusion #
Despite the prevalence of food imagery, this article about functional programming architecture has eluded any mention of burritos. Instead, it examines the tension between an ideal, the Impureim sandwich, with real-world implementation details. When you have to deal with concerns such as input validation or translation to egress data, it's practical to add one or two more thin slices of purity.
In functional architecture you want to maximise the proportion of pure functions. Adding more pure code is hardly a problem.
The opposite is not the case. We shouldn't be cavalier about adding more impure slices to the sandwich. Thus, the adjusted definition of the Impureim sandwich seems to be that it may have at most two impure phases, but from one to three pure slices.
Comments
Hello again...
In one of your excellent talks (here), you ended up refactoring maitreD kata using the
function. Since this step is crucial for "sandwich" to work, any post detailing it's implementation would be nice.Thanks
qfilip, thank you for writing. That particular talk fortunately comes with a set of companion articles:
The latter of the two comes with a link to a GitHub repository with all the sample code, including the
Traverse
implementation.That said, a more formal description of traversals has long been on my to-do list, as you can infer from this (currently inactive) table of contents.