Dmitrii Kovanikov writes posts about enterprise software development and functional programming, both entertaining and serious. I never used a functional programming language (such as Haskell or OCaml) at work, but several ideas from functional programming have become popular in mainstream general-purpose programming languages such as Swift (which I mostly write). Often even a partially functional approach produces simpler code that’s easy to understand and quick to write and maintain. I’m always looking for good ideas I can adopt to write better code, whatever their source.
Dmitrii recently posted a tweet (now also reposted to Bluesky) titled “Functional programming self-affirmations” that listed five items:
- Parse, don’t validate
- Make illegal states unrepresentable
- Errors as values
- Functional core, imperative shell
- Smart constructor
These got me interested, but as usual, the problem with such distilled mantras is that you need a lot of context to understand and use them. What does it mean to have a “smart constructor”? In the comments Dmitrii himself and other people added links to suggested reading. Let’s go over each item and uncover what it means.
1. Parse, don’t validate
The text that introduced this notion is this blog post with the same name written by Alexis King. It also covers the next notion so we’ll come back to it later. It gives a much better explanation than my second-hand version could ever do.
For me the two standout ideas were:
- While validation establishes correctness, parsing enriches the data and adds knowledge we can build upon, and does that early on the way from less-structured to more-structured data. In my own work I describe it as “building a solid foundation” meaning that you can confidently build higher-level abstractions when the lower-level abstractions are leak-proof, with correctness likely enforced by the compiler.
- The notion of shotgun parsing (a mix of input validation and parsing that tries to capture all the “bad” cases — an antipattern, because it doesn’t work) defined in the paper “The Seven Turrets of Babel: A Taxonomy of LangSec Errors and How to Expunge Them”. It is exactly the same situation as writing tests: the tests cannot prove the absence of bugs, only that certain things are correct. To come closer to proven correctness, illegal states must be forced out of the program, usually with robust type definitions.
Which neatly leads us to the next point…
2. Make illegal states unrepresentable
This is an idea that I see talked about fairly often even in “regular” commercial programming. However, in my experience, not many people are willing to go all the way to achieve it. The post I linked above talks about this notion in its second half. I define it to mean “your logic and types (where applicable) are designed in such a way as to make it physically impossible to create a bad state”.
There are specific examples both in the blog post above and also in this F# for Fun and Profit “Designing with types” post that make it clear. The latter makes an excellent point why people do not often go all the way — it often requires flipping assumptions about a system, and also not wanting to bother defining a type (for example) for a container that can hold either one or three items:
At this point, you might be saying that we have made things unnecessarily complicated.
People often see this upfront work as complicating the code (“Why can’t I just validate in the initializer and be done?”) and don’t do it. Down the line, someone else doesn’t know about the (unenforced) requirements, and that’s when bugs are introduced. If this idea is implemented well, then the API design or the compiler will make it impossible to make a mistake, because you cannot use the code in a way that represents a bad state. It may be tedious but it’s so worth it.
3. Errors as values
This notion is well-described in “Errors as Values: Free Yourself From Unexpected Runtime Exceptions”. The “values” in the phrase refers to returning an error value from a function instead of raising an exception. Usually, the error value is well-defined, and exceptions are unexpected.
This idea is now fairly mainstream and I see that people try to avoid using exceptions. Many languages encourage and support you to return something like a Result
(which, for example in Swift, is a sum type of Success
and Error
) or, like Go, a tuple of values: a successful result and an optional error. This practice came to replace setting a global error variable or returning magic “error” values © and throwing exceptions (C++, Java, etc.).
To me, this simply makes more sense: isn’t it objectively better to get a finite and predictable error value from a function than an unspecified exception that may or may not happen that you still have to guard against?
4. Functional core, imperative shell
I was not familiar with this notion at all, but after reading this blog post by Javier Casas it now makes sense. Functional programming is, ideally, a realm of pure functions without side effects that are easy to reason about and easily testable in isolation.
To integrate this code into real systems in the real world you need to set up an environment, procure and provide input values, maybe talk to the OS (which is often stateful) and communicate output values. That’s where the dichotomy comes from. You push as much code as possible that deals with logic into a “functional core”, and only the messy interfacing routines end up in an “imperative shell”. This way the meat of the program can get easily tested without any scaffolding, and the test environment and possibly mocks only need to be provided to the shell.
5. Smart constructor
The page “Smart constructors” on the Haskell Wiki explains this notion well. It neatly dovetails with “making illegal states unrepresentable” by providing compile- or at least runtime checks when making new values. If the type system cannot enforce a constraint, you can prevent “bad” values from being constructed at runtime.
The page raises another point, that smart constructors can do optimizations because they control internal representation. The example on the page is compacting data, but it can also be normalization, making structures homogenous or converting value formats.
The benefit is, again, creating a solid foundation that you can build on. You know the data you operate is clean, tidy and valid. This means you can do fewer checks later in the pipeline which improves both performance (less code is executed) and clarity (there are fewer noisy checks).
Conclusion
Do these ideas belong only in functional programming? While they are practiced more there, and functional programming languages generally have strong type systems that help implement these constraints, we “regular” developers can use most of these concepts when writing our mostly imperative code to make it simpler. Clearly, if your functions have few or no side effects, the underlying data is clean, and you can’t accidentally create “bad” objects, you’ll have an easier time.