Maybe I'm not understanding this, but you're saying that you should only ever return success or failure from functions but then you're also throwing exceptions? how do these patterns reconcile with this railroad programming style in the video?
THe introduction talks about throwing, but the rest of the text is about returning Success and Failure values. It's probably just a poor choice of terminology in the intro.
I don't understand what that paragraph is on about. I use this approach extensively and the whole point is that you don't need exceptions anymore. LogRocket is a service that kind of counts on errors being thrown so maybe there's something there haha
The spirit of the idea is to be explicit about what is the behavior you can expect from the code. You can’t foresee every exception, but you can at least make an effort to handle those that are likely to happen. It allows the calling code to handle the exception in a way that it doesn’t kill the program unless necessary. You can recover from some exceptions and should if possible.
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.
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.
The main point is that your function can only return either a success or a failure. Failure should be handled using the throw statement so as to throw an exception, while success is what leads to another function, which can be of any type.
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.
- 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!
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)
The code examples don't make any sense, the functions may return a error but they are not being checked for in the next function. Using thir own metaphor, their functions may deviate to the red rail, but then the next function just assume everything is always coming from a green rail, they don't codify the real possibility that the input is from the red rail.
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.
I got the impression that the author of the article didn't actually understand the idea they were writing about. It reads like blogspam whose real purpose is not to convey understanding but to contain advertisements for, in this case, LogRocket.
Yes, this article (the OP article that is) is horribly confusing and doesn't follow the ROP architecture at all.
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.
This looks just like goto-based error handling in C (e.g. [1]). But that is an invention of necessity, lacking better error handling mechanisms.
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.
Maybe I'm not understanding this, but you're saying that you should only ever return success or failure from functions but then you're also throwing exceptions? how do these patterns reconcile with this railroad programming style in the video?
THe introduction talks about throwing, but the rest of the text is about returning Success and Failure values. It's probably just a poor choice of terminology in the intro.
It’s about adapting methods that throw out of your control. For example, I/O or timeouts in JS (presentation is language agnostic).
I don't understand what that paragraph is on about. I use this approach extensively and the whole point is that you don't need exceptions anymore. LogRocket is a service that kind of counts on errors being thrown so maybe there's something there haha
The spirit of the idea is to be explicit about what is the behavior you can expect from the code. You can’t foresee every exception, but you can at least make an effort to handle those that are likely to happen. It allows the calling code to handle the exception in a way that it doesn’t kill the program unless necessary. You can recover from some exceptions and should if possible.
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.
Yes, extremely confusing article imo.
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.
The idea is that instead of checking error after every call you just throw exception and handle it above but article is just plainly bad.
Link to ROP author webpage:
https://fsharpforfunandprofit.com/rop/
It’s a great site to read every once in a while to revise.
i must say i found this pretty incomprehensible - the railoroad track illustrations i found particularly hard to understand.
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.
This analogy/metaphor is usually represented in code via an interface called MonadFail (e.g. in Haskell https://hackage.haskell.org/package/base-4.17.0.0/docs/Contr... and Scala https://www.javadoc.io/doc/org.typelevel/cats-docs_2.13/late... )
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)
The code examples don't make any sense, the functions may return a error but they are not being checked for in the next function. Using thir own metaphor, their functions may deviate to the red rail, but then the next function just assume everything is always coming from a green rail, they don't codify the real possibility that the input is from the red rail.
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.
I got the impression that the author of the article didn't actually understand the idea they were writing about. It reads like blogspam whose real purpose is not to convey understanding but to contain advertisements for, in this case, LogRocket.
Agreed - the original content this is cribbing from[0] is really good. Worse CSS, though :)
[0] https://fsharpforfunandprofit.com/rop
Yes, this article (the OP article that is) is horribly confusing and doesn't follow the ROP architecture at all.
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.
Yeah it makes loads of sense. Syntactic sugar required to make the medicine go down, of course :)
This looks just like goto-based error handling in C (e.g. [1]). But that is an invention of necessity, lacking better error handling mechanisms.
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.
[1] https://www.xml.com/ldd/chapter/book/ch02.html#buierr