On the surface, it makes complete sense, the non-locality of exceptions make them hard to reason about for the same reasons that GOTO is hard to reason about, and representing failure modes by values completely eliminates this non-locality. And in purely functional languages, that's the end of the story. But in imperative languages, we can do something like this:
def my_effectful_function():
if the_thing_is_bad:
# do the failure thing
raise Exception
# or
return Failure()
return Success()
and a client of this function might do something like this: def client_function():
...
my_effectful_function()
...
and completely ignore the failure case. Now, ignoring the failure is possible with both the exception and the failure value, but in the case of the failure value, it's much more likely to go unnoticed. The exception version is much more in line with the "let it crash" philosophy of Erlang and Elixir, and I'm not sure if the benefits of locality outweigh those of the "let it crash" philosophy.Have any imperative folks here successfully used the "errors as values" idea?
[1] https://doc.rust-lang.org/book/ch09-02-recoverable-errors-wi...
Unfortunately, the whole "exceptions vs. expected" issue is fundamentally unsolvable for as long as we stick to working on common single source of truth plaintext code. Explicit and implicit error handling are both useful in different circumstances - it's entirely a presentation issue (i.e. how code looks to you); our current paradigm forces us to precommit to one or the other, which just plain sucks.
--
[0] - See how it works in C++, where you can't easily hide the mechanism.
Plus, if you want to bubble the error, you must intentionally use the ? operator, so you are forced to acknowledge the function you called actually may raise an error (as opposed to calling an API you're unfamiliar with and forgetting to check whether it can raise an error, and the compiler not having your back)
That as an alternative to throwing the error. This way you get the benefit of being able to follow the flow of control from each called function back to each caller, as opposed to control jumping around wildly because of thrown errors.
In a statically typed imperative language that would need support for sum-types, to be able to return either an error or a non-error-value. Then you would be less likely to ignore the errors by accident because you would always see the return value is maybe an error.
Isn't this also basically how Haskell does it, handling side-effectful values as high up as as possible which in Haskell means moving them into the runtime system above all user-code?
FP-first/only languages tend to push you in these directions because it makes programming with them easier.
In languages where FP is optional, it takes discipline and sometimes charisma to follow these affirmations/patterns/principles.. but they’re worth it IMO.
How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?
How can you do "Errors as values" at a large scale without do-notation / monads?
How can you do "Functional core, imperative shell" without the ability to create mini DSLs and interpreters in your language?
Java (along with many other object-oriented languages) lets you create objects that are effectively immutable by declaring all fields private and not providing any property setters or other methods that would mutate the state.
Errors as values is one of the headline features of both Go and Rust, neither of which has do notation and monads.
Functional core, imperative shell is something I first learned in C#, but I don't think it was _really_ at significantly more of a disadvantage than most other languages. The only ones that let you really enforce "functional core, imperative shell" with strong language support are the pure functional languages. But people working in, say, Ocaml or Scala somehow still survive. And they do it using the same technique that people working in Java would: code review and discipline.
None of this is to say that language-level support to make it easier to stick to these principles is not valuable. But treating it as if it were a lost cause when you don't have those features in the language is the epitome of making the perfect the enemy of the good.
I think you're confusing "make illegal states unrepresentable" with "parse, don't verify"? If your type cannot represent any invalid states, there's no way you can reach them through mutation.
By either making the data const, or encapsulating mutable state with private fields and public methods
> How can you do "Errors as values"
Go, Rust, Odin, Zig, and many more are imperative languages that do exactly this
> How can you do "Functional core, imperative shell"
Write a function that takes the old state and returns the new state
The Result monad can be implemented in any static language with generics (just have to write two functions) and in a dynamic language this is easy (but return will have to be like T.return as there is no implict inference).
I didn't get the relation between FCore/IShell and DSLs, the main requirement for FCore is a good immutable library. Macros help DSLs though that is orthogonal.
But really, my main point is that OOP vs FP is red herring as 3/4 aspects which characterize OOP can be potentially done in both OOP and FP, with different syntax. We shouldn't conflate the first 3 with the 4th aspect - mutability.
An OOP language with better extension mechanism for classes +immutable data structure libraries and a FP language with first class modules would converge. (ref: Racket page below and comment on Reason/OCaml down the page).
See Racket page on inter-implementability of lambda, class, on the unit(ie. a first-class module) page here (https://docs.racket-lang.org/guide/unit_versus_module.html). Racket has first class 'class' expressions. So, a mixin is a regular function.
You don't need monads for this. You just need the ability to encode your error in some term like `Either e a` and ability to eliminate those terms. In Rust for example that is the `Result` type and you use pattern matching to eliminate those terms.
Is immutability exclusive to functional programming?
Is the ability to use data/values exclusive to functional programming?
Are monads exclusive to functional programming?
For discussions like this, how do we separate "it was done first in functional programming but can also be done in procedural programming" with "it cannot be followed outside of functional programming"?
> How can you "Make illegal states unrepresentable" with mutable state and sequences of mutations that cannot be enforced with the type system?
Firstly, don't use mutable state, write immutable types. Secondly, write constructors that reject poorly formed data structures. Thirdly, for existing libraries with types that are mutable, create a wrapper for the library with functions that return an IO/effect monad.
> How can you do "Errors as values" at a large scale without do-notation / monads?
Luckily, from a C# PoV, we have LINQ, which is equivalent to do-notation. I agree that manual management of monadic flow would be hard without something akin to do-notation or LINQ.
You can get quite far with fluent methods, but a general monadic-bind is quite hard to chain if you want to carry all of the extracted values through to subsequent expressions (lots of nesting), so yeah, it would not be ideal in those languages. It should be stated that plenty of functional languages also don't have do-notation equivalents though.
> How can you do "Functional core, imperative shell" without the ability to create mini DSLs and interpreters in your language?
I've never really liked the "Functional core, imperative shell" thing. I think it's an admission that you're going to give up trying to be functional when it gets difficult (i.e. interacting with the real world). It is entirely possible to be functional all the way through a code-base.
In terms of DSLs: I'm not sure I know any language that can't implement a DSL and interpreter. Most people don't realise that the Gang of Four Interpreter pattern is isomorphic to free-monads, so most imperative languages have the ability to do the equivalent of free-monads.
As the GP states, it takes discipline to stick to the constraints that a language like Haskell imposes by default. Not sure about the charisma part!
I have found that having access to a world-class compiler, tooling, and large ecosystem to be more valuable to getting shit done than the exact language you choose. So, bringing the benefits of the pure-FP world into the place where I can get shit done is better than switching to, say Haskell, where it's harder to get shit done due to ecosystem limitations.
There's also the get out of jail free card, which allows me to do some gnarly high-performance code in an imperative way. And, as long as I wrap it up in a function that acts in a referentially transparent way, then I can still compose it with the rest of my pure code without concern. I just need to be a bit more careful when I do that and make sure it's for the right reasons (i.e. later stage optimisations). That's less easy to do in FP languages.
Again, it's about discipline.
If you want to see how this can look in a mainstream, imperative-first, language. I have a few samples in the repo, the one I like to share when helping OO-peeps learn about monads and monad-transformers is this game of 21/Pontoon [2]. I suspect most people won't have seen C# look like this!
[1] https://github.com/louthy/language-ext/
[2] https://github.com/louthy/language-ext/blob/main/Samples/Car...
Let's say this was my program:
void Main()
{
PureFunction().Run();
ImpureFunction();
}
If those functions represent (by some odd coincidence) half of your code-base each (half pure, half impure). Then you still benefit from the pure functional programming half.You can always start small and build up something that becomes progressively more stable: no code base is too imperative to benefit from some pure code. Every block of pure code, even if surrounded by impure code, is one block you don't have to worry so much about. Is it fundamentalist programming? Of course not. But slowly building out from there pays you back each time you expand the scope of the pure code.
You won't have solved all of the worlds ills, but you've made part of the world's ills better. Any pure function in an impure code-base is, by-definition: more robust, easier to compose, cacheable, parallelisable, etc. these are real benefits, doesn't matter how small you start.
So, the more fundamentalist position of "once one part of your code is impure, it all is" doesn't say anything useful. And I'm always surprised when Erik pulls that argument out, because he's usually extremely pragmatic.
Especially when dealing with a fast changing domain, having to support different versions of data shapes across long time periods: dynamic data definitions are more economic and will still provide sufficient runtime protection.
"Errors as values" - what is an error? I see this pattern misused often, because not enough thought was put into the definition of an error.
"Disk is Full" vs. "data input violates some business rule" are two very - very - different things and should be treated differently. Exceptions are the right abstraction in the first case. It's not in the second case.
"Functional core, imperative shell" - 100% agreement here.
This has not been my experience. The speed increase in development not having to worry about the unrepresentable cases have been very valuable. In addition as requirements change migrating old data hasn't been a huge concern. For code changes refactoring the types helps address new cases as well.
If you're writing a CMS/wiki software, it's gonna be pretty straightforward to do.
If you're working with transactions, trades, contracts etc, it's not.
I don't think that's it. I think what's needed is an accumulator method for error interfaces so that you can accumulate new errors into existing errors.
> 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?
Whether an error is returned as a value or thrown is orthogonal to whether it is finite and predictable. Java has checked exceptions. In Swift you also can specify the exceptions that a function may throw. How is it any less predictable than return values?
Semantically, a thrown exception is simply a return value with debug information that gets automatically returned by the caller unless specified otherwise. It is simply a very handy way to reduce boilerplate. Isn't it objectively better to not write the same thing over and over again?
One way to achieve "Make illegal states unrepresentable" is by using "refined" types, a.k.a. highly constrained types. There is a "refined" library in both Haskell and Scala and the "iron" library for Scala 3.
This is a nice ideal to shoot for, but strict adherence as advocated in the article is a short path to algorithmic explosions and unusable interfaces on real life systems.
For example, if you have two options that are mutually incompatible, this principle says you don't make them booleans, but instead a strict enum type populated with only legal combinations of the options. A great idea until you have 20 options to account for and your enum is now 2^16 entries long. Then your company opens a branch in a different country with a different regulatory framework and the options list grows to 50 and you code no longer fits on a hard drive.
If you truly do have 2^16 valid and distinct behaviors, it is not possible for humans to correctly write 2^16 different code paths anyway.
More than likely, the majority of the combinations of your 29 Boolean flags are invalid. You should strive to have that checked by the program, and not only as a note in external documentation.
No one is saying int should be turned into an enum.
Imagine a simple shipping system for example. The package might be routed via truck, boat, plane, pipeline, foot, etc... Probably even a combination of those options. The package might be low, medium, or high priority, although high priority packages are not allowed to be delivered by boat. The package might be small, large, or liquid, but liquids can't be delivered by foot. There are 195 different countries with even more regulatory regimes to consider, some of which may have different customs requirements based on mode of transport. Now imagine a problem that is actually complicated.
The idea of encoding all of this business logic into the datatype is a road to madness IMHO. Especially if the rules can change on you and require you to rework your datatypes to match. On the small scale this makes a lot of sense and is good advice, but strict adherence is impractical.
Your latter example needs context. In what situation have you had an enum with 2^16 states? In any case, if you generally have a reasonable number of booleans, with some states being logically invalid, then you'll need error checking on those anyway.
Leaving them as booleans gives you the ability to monkey-patch things later in an ad-hoc manner, which is useful when you're still in a prototyping phase and the requirements aren't clear (and you could argue that this is almost always the case in many problem domains; I think that would be valid criticism.) But if you've already modeled the domain and you want any assurance of correctness, then I don't see why you wouldn't enforce the constraints at the type level. Your criticism seems to me the usual trade-off between strong static typing and dynamic and/or weak monkey-typing.
data OptA = ATrue OptB | AFalse
data OptB = BTrue | BFalse
There are three valid combinations but no type has three alternatives. Nobody in their right mind would write out an enum with 2^16 cases. If you do, you are misusing enums and it's time to consider other tools in your language.No, no one would continue up to 2^16 and the code would get unmanageable long before that. But it's illustration of the problems starting out dealing with the invalid states of two variables using an enum because what happens when more and more variables arrive? Sure, the standard answer is "just refactor" but my experience is no client or boss wants to hear "adding this small state is going require a lot of change" and a trickle of binary conditions is a very common occurrence as is code expanding to handle these (and become excessively complicated).
Chances are, you can use sum types (discriminated unions) to factor things nicely if you think about them.
Maybe you have a good chance of combining these binary conditions in a good way. But I mention you've substituted a hard problem instance (factoring binary conditions) for an easy problem instance (checking binary conditions). Functional programming has a weird dual personality where on the one hand you hear "A functional programmer is always a smarty and solve hard problems as a matter of course" but also you hear "functional programming would be the dominant paradigm if only ... we taught people young so they wouldn't have their bad procedural habits"
Y and Z are mutually exclusive.
X can only be set if W is set.
If Y is set then X must be set.
Going down this road you end up encoding your business logic into your datatypes. Which is good to a degree, but makes things messy when new options are added or requirements change. Imagine a new option U is introduced that is only valid when W is unset and Z is set but allows X to be set. Your interface becomes very hard to manage with even a small amount of complexity added to the options.
But that is just how things are currently. The world may change. And it's important to strive for maintainable code that can accommodate such change easily.
Expanding company operations to work in a different country is an example of this (our) "world changing". States that never occur together here, may occur together there. Or in the future more generally.
So, making illegal states non-representable is about avoiding errors in respect to the system specification. But it does not take into account the fact that in the real world specifications typically evolve and change even during development, and perhaps more so later.
How do you represent this in a way that makes it impossible, even through programmer error elsewhere in the program, to have the flags in an invalid state?
You can create en enum that looks like:
enum program_state =
(
W_X_Y_NZ,
W_NX_NY_Z,
NW_NX_NY_Z,
... and so on
);Suppose you have an Order object that needs to track where some larger process is in relation to three subtasks. We could imagine say that the Inventory department needs to set a physical object aside, then the Billing department needs to successfully charge for it, then the Shipping department needs to retrieve that physical object and ship it.
You start from a description of this as "one Order contains three Subtasks" where a Subtask contains the Go-style (Optional[Result], Optional[Error]) type. This architecture almost fits into a relational database, except that foreign key constraints are a bit funky if you shove everything into one nullable Result column. But let's just have the Result be some random JSON in the Subtasks table and let our relational purists weep.
Then you read this advice and you start to see that this allows for a lot of illegal states: things could contain both a result AND an error, or neither. You eventually decide that neither, is an allowed state. These are two boolean flags representing only 3 legal states and so they need to be factored into an enum: the enum is "Pending | Success[Result] | Failure[Error]".
Well, except the problem is a bit more nuanced because the pending-states also need to be consistent among the different subtasks: there is a dependency graph among them. So you should actually have an enum that says:
Inventory_Pending
Inventory_Failure[Error]
Inventory_OK_Billing_Pending[InventoryData]
Inventory_OK_Billing_Failure[InventoryData, Error]
Inventory_OK_Billing_OK_Shipping_Pending[InventoryData, BillingData]
Inventory_OK_Billing_OK_Shipping_Failure[InventoryData, BillingData, Error]
Inventory_OK_Billing_OK_Shipping_OK[InventoryData, BillingData, ShippingData]
See, you would have had 3x3x3 = 27 valid states before for the Order but we have reduced to only the 7 legal states. Yay!But now consider e.g. the following mutation. On Failure cases the executives at our company mandate that we never return a failed Order to a Pending status, rather we must always create a separate Order. This Order might skip inventory and/or billing and those need to be represented separately, as Inventory Skipped[OrderID] or InventoryAndBillingSkipped[OrderID]. So now our list of states following the "no unrepresentable state" logic, should really be:
[... the 7 above, plus ...]
Inventory_Skipped_Billing_Pending[OrderID]
Inventory_Skipped_Billing_Failure[OrderID, Error]
Inventory_Skipped_Billing_OK_Shipping_Pending[OrderID, BillingData]
Inventory_Skipped_Billing_OK_Shipping_Failure[OrderID, BillingData, Error]
Inventory_Skipped_Billing_OK_Shipping_OK[OrderID, BillingData, ShippingData]
Inventory_And_Billing_Skipped_Shipping_Pending[OrderID]
Inventory_And_Billing_Skipped_Shipping_Failure[OrderID, Error]
Inventory_And_Billing_Skipped_Shipping_OK[OrderID, ShippingData]
Now someone else wants to add remediation actions, but only to remediate the exact error in the failure state, so _Failure is going to mean "no remediation taken" but we need to add some _Remediation with a boolean saying whether that process has completed or not. So we add: Inventory_Remediation[Error, Bool, Array[RemediationEvent]]
Inventory_OK_Billing_Remediation[InventoryData, Error, Bool, Array[RemediationEvent]]
Inventory_OK_Billing_OK_Shipping_Remediation[InventoryData, BillingData, Error, Bool, Array[RemediationEvent]]
Inventory_Skipped_Billing_Remediation[OrderID, Error, Bool, Array[RemediationEvent]]
Inventory_Skipped_Billing_OK_Shipping_Remediation[OrderID, BillingData, Error, Bool, Array[RemediationEvent]]
Inventory_And_Billing_Skipped_Shipping_Remediation[OrderID, Error, Bool, Array[RemediationEvent]]
We're only up to 21 total states so far which is still probably manageable? But these changes do demonstrate exponential growth, which is a technical term that means that the growth of each step is some small fraction of the total growth that has happened up until that point. Because everything depends on Inventory (it's at the root of the tree), when we add a new state that the Inventory can be in (Skipped) we have to add enum cases for all of the other states, and we pay a cost proportional to the size of the tree. Similarly when everything can have an error (at the leaves of the tree), when we add a new uniform requirement for errors we have to add new leaves all throughout the tree and we pay a cost proportional to the size of the tree. (Another thing to notice about the Remediation state is that it is a Pending state for another Subtask that could have been added to the original Order whenever something moves into Failure mode.)You get something powerful by reducing the 256ish-or-whatever states into the 21 legal states; you have a compile-time assurance that no bugs in your code have created weird states that can propagate their weirdnesses throughout the system. But you also have to maintain the 21 legal states all at once, instead of maintaining 4 subtasks each having one of 4 statuses.
Parent currently downvoted without comments. Seems like a sad way to respond.
I really hope HN enforces a rule that a downvote must be accompanied by a reply.
linked from the article:
https://www.javiercasas.com/articles/functional-programming-...
State Skeptic: Yes, But! How do you compose the 'pure core + impure shell' pieces?
FPN: Obviously, you compose the pure pieces separately. Your app can be built using libraries built from libraries.... And, then build the imperative shell separately.
My take is that the above solution is not so easy. (atleast to me!) (and not easy for both FP and non-FP systems).
Take an example like GUI components. Ideally, you should be able to compose several components into a single component (culminating in the app) and not have a custom implementation of a giant state store which is kept in something like Redux and you define the views and modifiers using this store.
Say, you have a bunch of UI components each given as a view computed as a function from a value and possible UI events which can either modify the value, remain unhandled or configurable as either. Ex: dialog box which handles text events but leaves the 'OK' submission to the container.
There are atleast two different kinds of composability (cue quote in SICP Ch1 by Locke) - aggregation and abstraction. Ex: Having a sequence of text inputs in the document(aggregation) and then abstracting to a list of distances between cities. This abstraction puts constraints on values of the parts, both individually(positive number) and across parts(triangle inequality). There is also extension/enrichment, the dual of abstraction.
This larger abstracted component itself is now a view dependent on a value and more abstract events. But, composing recursively leads to state being held in multiple layers and computations repeated across layers. This is somewhat ameliorated by sharing of immutable parts and react like reconciliation. But, you have to express your top->down functions incrementally which is not trivial.
> Ideally, you should be able compose them several of them into a single app and not have a custom implementation of a giant state
If you are suggesting that components store their state, I'm not sure about "ideal" there. That works well for small GUI applications. In GUI applications of modest size, you do want a separate, well-organized and non-redundant data layer you can make sense of, at least from my experience. Qt, specifically, allows you to do both things.
These four concepts are forced/complected into a 'class' construct, but they need not be.
In particular, FP only varies on 4, but languages like ML,Clojure do 1,2,3 even better than OOP languages. Modules for encapsulation, Dispatch on first or even all arguments for polymorphism and first class modules, ML style, for extensibility.
Aside: There was a recent post (https://osa1.net/posts/2024-10-09-oop-good.html) (by someone who worked on GHC no less), favorably comparing how OOP does extensibility to Haskell typeclasses, which are not first class, but modules in ML languages can do what he wants and in a much more flexible way than inheritance!
There is also the dynamic aspect of orginal OOP - message passing instead of method invocation, but this is about dynamic vs static rather than OOP vs FP.
What OOP languages have managed to do which static FP hasn't done yet is the amazing live inspectable environments which lead to iterable development like we see in Smalltalk. The challenge is to do this in a more FP way while being modular.
Note that there is no option really - the app wont be reimplementing how a key is handled in a text box. But composability means that the same principle should hold not just for OS/browser components but also for higher level components (A custom map or a tree-view where there are restrictions on types and number of nodes - these should also have default handling and delegation to upper levels.)
The global store choice makes it harder to have component libraries. But, the composable alternative has its problems too - redundancy and communication which skips layers (which requires 'context' in React).
True, which is why re-frame has a dependency graph and subscriptions that avoid re-computation, i.e. the data dependencies are outside any view tree.
If data changes, only active nodes (ones that have been subscribed to) will re-compute. If nothing changed in a node, any dependent nodes will not re-compute.
It's a beauty.
Say a city stats(location, weather) component is held inside a region component which in turn is in charge of a product route generating component (which also contains a separate 'list of products' component).
You can't update the city coordinates safely from the top as the region component enforces that the cities are within a maximum distance from each other. The intermediate constraint would have to be lifted to the higher level and checked.
Edit: There is also a more basic problem. When your app has multiple types of data(product, city), the top level store effectively becomes a database(https://www.hytradboi.com/2022/your-frontend-needs-a-databas...). This means that for every update, you have to figure out which views change, and more specifically, which rows in a view change. This isn't trivial unless you do wholesale updates (which is slow), as effects in a database can be non-local. Your views are queries and Queries on streaming data is hard.
The whole update logic could become a core part of your system modelling which creates an NxM problem (store update, registered view -> does view update?). This function requires factoring into local functions for efficient implementation which is basically the data dependency graph.
I don't see the value of learning this stuff one random blog post at a time.
There are many books and established blogs with long series of material to give you an education.
A significant share of the current dev community wasn't there when these principles were first described. Sometimes, bringing up old topics once again helps seeing that many people had never heard of them yet.
None of these "affirmations" are common in any code base I work with, unfortunately.
Edit: I think I misunderstood your point. You were asking for a similar kind of list as TFA from the other commenter which may not necessarily be the *same* 5 ideas
Leaving the rest here in case it helps someone:
1. Parse, don’t validate
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
2. Make illegal states unrepresentable
https://fsharpforfunandprofit.com/posts/designing-with-types...
3. Errors as values
https://jessewarden.com/2021/04/errors-as-values.html
4. Functional core, imperative shell
https://www.javiercasas.com/articles/functional-programming-...
5. Smart constructor
Ah yes, because using constructors to ensure that new objects are in a valid state is virtually unheard of in object-oriented programming.