What is railway oriented programming? (2020)
blog.logrocket.com
blog.logrocket.com
For me it seems like a method that may work with the right language sugar, but without a sugar made specific for this you will have to pollute all functions with extra error handling to handle the possible red-rail-input. At this point it should be better to just handle those errors when they happen, and quit the happy path there, instead of following along the "happy path" just passing errors to all the following functions.
The core idea is that instead of a Func that takes in a Foo and returns a Bar, you can take in a Maybe<Foo> and return a Maybe<Bar>.
Then you don't need error handling in the outer composition, once everything is of the form F = Maybe<T1> -> Maybe<T2>, etc, then you can fully compose everything at the outer layer into a F = Maybe<T1> -> Maybe<Tn>.
So then you just check for Ok() at the very end and unwrap and handle any errors there.
The use of promises and async await allow the unhandled rejections to bubble up, but they've put "Success" else "Failure" in their pseudocode which could be interpreted as a return (wouldn't bubble up without extra code in the callers) or a throw (would bubble up) - not filling in that part leaves too much room for interpretation as does using async await everywhere unless you are already somewhat familiar with what they're driving at.
My understanding of what's trying to be conveyed would be better explained by using something like the Result type in Rust paired with the ? operator (failures are returned early/bubble up but successes go through). They should have slapped together a result type in Typescript to get the point across.
Similarly in go you have multiple returns and the common `if err != nil { return err }` a billion times which I think is the same concept here, just much more verbose. How does that differ from the initial code in tfa? I'd say it's because the caller returns the callee's failure rather than constructing its own thing or continuing with logic.
Then there's also some monad discussion that I'm not fully qualified to give.
Note that it took me a bit of getting used to the idea due to my background with python. Its type system does require a lot of extra code to implement this pattern.
https://fsharpforfunandprofit.com/rop/
It’s a great site to read every once in a while to revise.
Rust bakes this into the language with the Result type. You signal railway oriented programming by returning a Result, which contains either the thing you want or an error. If you try to get the thing, you must deal with the possibility of an error in some way. There is no half-way.
Conceptually, the "two track system" doesn't make much sense. It models the error handling as a linear routine, with monotonic jumps from the happy path to the error path. But is error handling really linear in the general case? If it's not what is the use of modelling it as a track? And are the jumps really monotonic as illustrated? If they are not, all the elegance attributed to this model goes away.
Some related points:
- Sequential composition (putting one piece of track after another) is captured by the Monad interface. Monad is more general than MonadFail, since it includes things which don't fit into the 'two track' analogy.
- Concurrent composition (running separate tracks side-by-side) is captured by the Applicative interface. Applicative is more general than Monad, since it includes things which can't be sequenced.
Where possible, we should try to use the most-general interface we can: this makes our code usable in more scenarios, and also prevents us calling inappropriate methods. In fact, this article's example of validating multiple fields not a good use-case for 'railway oriented programming', since it will abort after detecting one failure!
A better approach is something like the Validated type from Scala's cats library https://www.javadoc.io/doc/org.typelevel/cats-docs_2.13/late...
Validated can represent success/failure, but it does not provide the Monad interface; hence we cannot sequence one check after another. The only way to combine multiple Validated results is via its Applicative interface, which performs all the checks concurrently: if they're all successful, the result is successful; if any fails, the result contains all of the failures.
In the article's example, if we have an invalid email address and a missing first name, we'll only be told about the email address. In the Applicative approach, we'll be told about both problems.
(Note that we can also define an ApplicativeFail interface; but I don't think the railway metaphor makes sense in that case)