[HN Gopher] Railway Oriented Programming
       ___________________________________________________________________
        
       Railway Oriented Programming
        
       Author : jchook
       Score  : 289 points
       Date   : 2023-08-18 06:20 UTC (16 hours ago)
        
 (HTM) web link (fsharpforfunandprofit.com)
 (TXT) w3m dump (fsharpforfunandprofit.com)
        
       | bongobingo1 wrote:
       | Does this argument hold: This pushes the error handling away from
       | the call site, and that is a bad thing. Being that the caller
       | knows best how to handle the errors, so it should instead of
       | passing them down.
       | 
       | (Full disclosure, I have seen this talk and read Wlaschins book,
       | but not in a long time, so maybe he did cover this and I forgot.)
       | 
       | Eg given:                   validate         and-then update-db
       | and-then send-email
       | 
       | What about when validate fails? The error is returned to the
       | caller, which is maybe suitable. What about when update-db fails?
       | Should there be a retry? Try another service? Requeue? Tell the
       | user? What about if the send-email fails?
       | 
       | Dont you end up with (except with more unwrapping and sub
       | branches)                  validate        and-then update-db
       | and-then send-email        and-if (Ok, ok)        or-if
       | (ValidationError, notify-user)        or-if (DBError, retry-in-5
       | or-then retry-remote or-then notify-user)        or-if
       | (EmailFailed, requeue-email and ok)
       | 
       | Maybe thats ok?
        
         | piaste wrote:
         | Low-level and high-level error handling can coexist. It's no
         | different than having a specialized try/catch inside a larger
         | try/catch.
         | 
         | You may, for example, choose to handle very fast transient
         | errors inside your `send-email` function. If you manage to
         | connect in under $acceptable-time, return to the happy path*,
         | otherwise return to the caller.
         | 
         | The only hard and fast rule is that any handling requiring
         | human intervention should definitely be wrapped up and passed
         | up.
         | 
         | * but do log a warning, regardless of whether you're just doing
         | classic impure logging, collecting the logs as part of the
         | success value, or using a writer monad.
        
           | ilyt wrote:
           | visually the classical try-catch makes error handling right
           | there more verbose and less readable. I far prefer Rust-like
           | passing result with maybe adding context to the error along
           | the way than try-catch block at every step of long process
           | that have errors that should be handled in-place. Because
           | passing it up is still easy, but adding any context is much
           | less verbose.
           | 
           | > The only hard and fast rule is that any handling requiring
           | human intervention should definitely be wrapped up and passed
           | up.
           | 
           | The "wrapped up" being the important and mostly ignored part.
           | From my experience try-catch error handling usually generates
           | incomprehensible errors where you can only guesstimate what
           | actually happened from stack trace's function names.
           | 
           | To get good error message every layer should have try-catch
           | block that adds context to the error (what operation did it,
           | or what logged user did it etc.), but that's almost never
           | done properly and we get stack trace vomits instead. If you
           | want it to be fixed by _actual humans_ you need to
        
         | mrkeen wrote:
         | Monadic error-handing a win-win way of doing things.
         | 
         | People who dislike Go complain about being forced to check for
         | error conditions too often (likewise with Java's checked
         | exceptions.) Using 'Either', you're not forced into checking;
         | you can check, or you can let the caller handle it.
         | 
         | People who dislike Java's unchecked Exceptions complain that
         | there's no way of knowing what will be thrown, or when. Using
         | 'Either', this is made explicit.
         | 
         | > What about when validate fails? The error is returned to the
         | caller, which is maybe suitable. What about when update-db
         | fails? Should there be a retry? Try another service? Requeue?
         | Tell the user? What about if the send-email fails?
         | 
         | The point is - you don't know! So your approach needs to be
         | well-suited to not knowing, which Either is.
         | 
         | In my current Java codebase at work, there are different ways
         | of 'handling' errors which have built up over the years. A call
         | to a missing 'Limit getUserLimit(User)' might:
         | * throw a NotFound exception       * return null       * return
         | a default
         | 
         | And you can't tell without diving in and reading the code. If
         | it had been written with Either instead:                 * the
         | caller could trust it instead, rather than reading all the code
         | that it calls       * easily convert it to another failure
         | condition - the Either implementation in your language will
         | have built-ins for 'orElse(null)', or 'orElseThrow(...)'
         | 
         | > Should there be a retry?
         | 
         | Either is an expression, which lends itself well to
         | abstraction. This means you can likely write code to retry
         | Eithers in general, as opposed to writing retry code for
         | specifically inside your DbUpdater.
        
         | AndrewPGameDev wrote:
         | I tried implementing this logic in java with a try-catch style:
         | try {                 Validation validation = validate();
         | try {                     Value value = update_db(validation);
         | send_email(value);                 } catch (DBError db) {
         | try {                         sleep(5);
         | Value value = update_db(validation);
         | send_email(value);                     }
         | catch(InterruptedException ie) {                         // do
         | nothing, stupid java-ism                     } catch(DBError
         | db2) {                         try {
         | Value value = update_db_remote(validation);
         | send_email(value);                         } catch (DBError
         | db3) {                             notify_user(db3);
         | } catch (EmailFailedError e) {
         | requeue_email(e);                         }
         | } catch (EmailFailedError e) {
         | requeue_email(e);                     }                 } catch
         | (EmailFailedError e) {                     requeue_email(e);
         | }             } catch(ValidationError v) {
         | notify_user(v);             }
         | 
         | https://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26...
         | 
         | IMHO this is much worse than the railway version you gave, but
         | maybe someone could write a superior version that still uses
         | try-catch.
        
         | DanielBMarkham wrote:
         | Excellent comment. Read my mind.
         | 
         | Yes, low level and high level error handling can coexist, but
         | this style of programming works much better (in my opinion) as
         | a blackboard exercise or a do-it-once-then-done situation.
         | 
         | Love me some DDD, but you gotta have some kind of reasonable
         | maintenance cycle that a moron programmer like myself can
         | manage. That was the beauty of TDD in mutable coding: it scoped
         | down the cognitive load for maintenance.
         | 
         | I can see railway programming carrying a lot of context through
         | a lot of transforms and hell if I'd want to be at the end of
         | the railway getting a big trainload of business and system
         | context I'm unprepared to handle.
         | 
         | DDD and onion architecture for the win. Best of both worlds,
         | and you end up with a railway anyway; it's just outside the
         | micro-apps, not stuck inside them. In my mind, you want railway
         | issues both explicit and a bit cumbersome to code. Most of the
         | time, if done well most railway coding decisions involve
         | business decisions that you should never be using clever coding
         | constructs to avoid in the first place.
         | 
         | ADD: To clarify, monadic programming is great but there's a
         | temptation to use it to avoid necessary business decisions.
         | Sticking ambiguity into a type system can lead you to some
         | difficult or impossible situations later on. I'm not saying
         | never do it. I'm saying most ways I've seen it done are not so
         | good.
        
         | Akronymus wrote:
         | You can wrap the error types into a discriminated union and
         | then check for retry-able errors, and retry if its one of
         | those, otherwise, propagate the error. Or if you don't need to
         | do special handling for any errors, you can just propagate the
         | whole error and let the caller handle it.
         | 
         | ROP definitely isn't useful in all cases, but it has served me
         | quite well in many by simplifying error handling when I don't
         | care at the callee what error occured.
        
         | cjfd wrote:
         | I think it should be pretty obvious that for instance retry
         | logic should be near the place in the code where the first
         | attempt is made. The best case for gathering all errors in a
         | 'railway' is when they can be handled uniformly. E.g., if we
         | want to not retry things but instead report a descriptive error
         | message to the user. Note that also that most error handling
         | logic that should be near the calling place might also fail in
         | more global ways that are then best handled using the
         | railway/exception pattern.
        
         | sasmithjr wrote:
         | It's possible that you could end up with your example as given,
         | but I think you could organize it differently so that it has
         | your handling while also maintaining the same basic top-level
         | pipeline.
         | 
         | Given what you've specified, I might organize it like:
         | notify-user:         if (ValidationError, notify-user-
         | validation-error)         or-if (DBError, notify-user-db-error)
         | try-update-db:         update-db         or-then retry-in-5
         | or-then retry-remote            try-send-email:         send-
         | email         or-then (requeue-email and ok)            do-
         | root-level-pipeline:         validate         and-then try-
         | update-db         and-then try-send-email         or-then
         | notify-user
         | 
         | I think I correctly mapped the and/or/thens to map/bind, so I
         | hope that makes sense.
        
       | rawoke083600 wrote:
       | I love that this comes up every few years, it's really a briljant
       | article and talk.
       | 
       | What is also interesting, I'm usually at a "different place" in
       | my tech-journey, in terms of skill or philosophy every time I re-
       | read.
        
         | el_oni wrote:
         | I really like things like this. There is a python talk called
         | "so you want to be a python expert" and i think i watched it
         | every 6 months for the first few years of my journey and each
         | time i realised i got more and more of it. I'm sure if i were
         | to watch it again there is probably still something now that i
         | could get from it.
        
       | samsquire wrote:
       | I see a lot of correlations between parsing train tracks and BNF
       | and error handling and state machines.
       | 
       | I wrote a bit about it here
       | 
       | https://github.com/samsquire/ideas5#252-happy-path-state-mac...
       | 
       | The problem: how do you re-join the control flow paths to the
       | happy path.
        
       | barrkel wrote:
       | Yes, this is a monad tutorial. It's concrete and specific and
       | doesn't attempt to generalize, but it's explaining an application
       | of monads.
       | 
       | And yes, the approach described, if followed naively and
       | robotically, reinvents a clumsy kind of checked exceptions.
       | 
       | As another comment https://news.ycombinator.com/item?id=37173435
       | points out, after learning and before serious application, it
       | should be leavened with caveats:
       | https://fsharpforfunandprofit.com/posts/against-railway-orie...
        
         | [deleted]
        
       | paddy_m wrote:
       | I implemented a data processing API for customers to write
       | convert and validate their unstructured tabular data in a defined
       | way. For each column a user could specify/override one of 5
       | functions.                   cast: (str|null) -> T
       | compute: T -> T         validate: (T) -> Message[] // doesn't
       | modify the value         serialize: (T) -> str // primarily used
       | so dates can be formatted
       | 
       | Each function was guarded by a try/catch block. This allowed
       | users to write simple functions and not have an edge case blow up
       | all of processing.
       | 
       | It ended up working pretty well. I don't think it was quite
       | railway oriented programming, more a carefully thought out
       | framework.
        
       | RedShift1 wrote:
       | So in Java this would translate into always returning an
       | Optional<T> from your functions?
        
         | the_af wrote:
         | This is Either, not Optional. Optional would lose the error.
        
       | gsuuon wrote:
       | This site has been the best programming education site I've
       | encountered in terms of real, pragmatic concepts taught. My
       | favorite is the concept of making invalid states
       | unrepresentable[1] which I try to apply now regardless of
       | language, though some make it easier than others.
       | 
       | [1] https://fsharpforfunandprofit.com/posts/designing-with-
       | types...
        
       | xupybd wrote:
       | There was an interesting tweet recently on doing this with a
       | computation expression.
       | 
       | https://twitter.com/ijrussell/status/1691752042539237413?s=2...
        
         | FrustratedMonky wrote:
         | Yes.
         | 
         | Think a lot of people think this is just using <Option>'s.
         | 
         | But really in Fsharp, you can build your own 'Monad's called
         | Computation Expressions, and for error handling can pass along
         | different information about the errors.
         | 
         | It doesn't have to be "Only" an <Option>, it can be
         | <MySpecialOption>.
        
       | janislaw wrote:
       | This is exactly how LabVIEW implements error handling.
        
         | elteto wrote:
         | Hah! I was going to post this myself.
         | 
         | When he overlaid the gray boxes over the tracks I thought
         | "that's a VI!".
         | 
         | I'd like to point that LabVIEW is not doing any of the fancy
         | type stuff explained here (Either, Maybe, etc). It's just that
         | VIs (or functions in common parlance) can have multiple inputs
         | and outputs, and LabVIEW's graphical approach lends itself well
         | to this use case. But LabVIEW's type system is fairly
         | primitive.
        
       | iraqmtpizza wrote:
       | [flagged]
        
       | FrustratedMonky wrote:
       | WOW. Great to simply see this being brought up and discussed at
       | all. I spent a year reading through this site and working through
       | examples. One of best F# resources.
       | 
       | Just the 'concepts' discussed on this site has helped me with
       | 'functional' thinking in all languages.
       | 
       | If this is getting attention now (because this is old site). Does
       | this mean F# is gaining traction?
       | 
       | And to the subject, do programmers in other languages use
       | 'railway' style error handling. Like Rust?
        
         | fsloth wrote:
         | "do programmers in other languages use 'railway' style error
         | handling."
         | 
         | I use that in C++. Basically I define a templated return type
         | that can be parametrized using the return type T and the
         | specific enumeration definining the result condition. Although
         | you can do with std::pair<MyValue, std::string> In a pinch,
         | where you return any error statements in the string and exit
         | early if the string is non-empty (when all functions return
         | types like that you can then bubble up the error statement up
         | to top level).
        
         | Archelaos wrote:
         | > Does this mean F# is gaining traction?
         | 
         | I have a strong background in C# for desktop applications (with
         | a database server backend). Over the years my coding style in
         | C# becomes more and more functional (such as using a lot of
         | Linq, immutable classes, return types that potentially include
         | detailed error infos, etc.). What me holds back from F# is that
         | I typically spent most of the time in tailoring the desktop
         | front end. Here F# does not seem to bring any benefit. As far
         | as I know, all available production ready desktop frontends are
         | not native F#, but plain old object-oriented code, mostly in
         | C#, that are bound to F# by just an intermediate layer of glue-
         | code. As soon as I need an even remotely sophisticated user
         | interface customisation, I am back in object-oriented land and
         | should do it best directly in C# again.
         | 
         | So in my particular case, F# needs a native UI framework before
         | it could gain traction for me.
        
           | phosphorco wrote:
           | Our whole frontend is built in elmish which is different from
           | Native, but it can tie directly into FuncUI (Avalonia) from
           | the elmish interface.
        
           | FrustratedMonky wrote:
           | Agree. I think the UI is the biggest block.
           | 
           | I like ELM Architecture, and ELMISH for F#. For F#, all the
           | UI options seem like hobby projects. They are Great. Great
           | concepts.
           | 
           | But, can take some time to wrap head around these concepts if
           | you are trained from birth that a 'button' is an 'object'.
           | 
           | F# really need some of these UI's like ELMISH to be built in,
           | like tooling in VStudio, or part of a release from MS to give
           | them more weight.
        
       | tuukkah wrote:
       | For TLDR, see the slide 151 of 154.
       | 
       | PDF of the slides (here the summary is slide 137 of 141):
       | https://github.com/swlaschin/RailwayOrientedProgramming/blob...
        
       | fuzzythinker wrote:
       | In the JS/typescript world, neverthrow [1] may be the closest
       | library for this. If anyone has good experience with something
       | else, please reply. Not sure if neverthrow is the right choice
       | after replacing all Errors in a 16k LOC engine library with it.
       | It did added more "noise" to the code. Would a custom bespoke
       | code/library add less noise? I'm not sure.
       | 
       | [1] https://github.com/supermacro/neverthrow
        
       | mpweiher wrote:
       | What works even better is using dataflow instead of call/return,
       | as the problem largely goes away by itself.
       | 
       | With call/return, you have to return something, so if you have
       | nothing to return because of an error, you have to return that,
       | or both the error and the normal return value (Go). This pollutes
       | the happy path. With this polymorphic container through which you
       | thread the remainder of the processing, you make the problem a
       | little nicer, but it is still there.
       | 
       | With dataflow, you simply don't send anything to the next filter
       | stage, so your happy path is completely unaffected. You then send
       | the error to some kind of standard error output, which can often
       | be centralised for your application.
       | 
       | Sounds to good to be true, but used it in Wunderlist, for
       | example, and it worked like a charm.
       | 
       | Oh, and the same technique that works for when you don't have a
       | value (errors) works the same when you don't have a value _yet_
       | (async).
        
         | kamray23 wrote:
         | That's got its downsides. Majorly, I feel like it's
         | restrictive. Exception handling, especially asynchronous
         | exception handling, can really disallow recovery from errors.
         | Sure, when you have a straight data stream and you simply want
         | to stop processing at an error, there's no data for the next
         | stage and it never runs. But that greatly limits recovery from
         | errors. If your reaction to sqrt(-1) is to "crash", ending the
         | business part of the program and hopping straight to printing
         | or logging or whatever for the error, it can be very difficult
         | to, say, enlarge the domain. You might have the need to simply
         | give some use-case specific value for negative numbers, and you
         | can pretty simply do that in code, but it'll result in some
         | coupling you don't want as now you have to test for being
         | outside that domain. For sqrt the domain is simple to define,
         | but for a lot of real-world logic, it isn't nearly that simple.
         | You can, in some languages, trap the error. Dataflow machines
         | often are not designed for that though, especially when dealing
         | with copious asynchronicity.
         | 
         | That's where explicit error types, such as Maybe come in. You
         | get to write your happy path as if errors didn't exist, but
         | function composition uses a different set of operators than you
         | usually would. In cases where you do want to recover from an
         | error, you handle both paths then and there, as part of the
         | happy path, and possibly don't even allow for an error beyond
         | that. Most importantly, to get any value which is not an
         | abstract concept of "maybe value" out, you need to handle the
         | existence of both paths gracefully. That can be very nice, and
         | very useful.
         | 
         | That being said, now that you're using explicit error types,
         | you can escape the "happy path"/"error" dichotomy. No longer is
         | there necessarily just a "no value". There can be a "no value,
         | but". Or a "value, but". You can have several errors stack up
         | in a chain of things which could be done in parallel and then
         | collated as a result. You can even entirely give up on the
         | concept of errors, since that's only a very special case. You
         | can use it to encode non-determinism: each unit takes in a
         | single value and processes it to several possible and different
         | values. Combining them with the monadic bind operator, each
         | unit outputting a list of values has those values concatenated
         | at the end to the list of all outputs, then the next function
         | runs for each of those values the set of outputs of them are
         | joined for the next stage and so on. This can be very, very
         | useful for things like traversing graphs. You can, as Haskell
         | programmers often do, use it to haul around a bit of "state" in
         | a technically pure manner (purity is in certain cases very
         | desirable). Perhaps the most infamous use of applicatives and
         | monads in programming is the Haskell IO Monad, which encodes no
         | real paths at all. IO simply is a virus which latches on to
         | everything you do with it, and getting out of IO requires
         | touching the outside world in an impure manner, which in
         | Haskell can "only" be done while evaluating the expression
         | called "main", meaning that the expression called "main"
         | becomes your only point of contact to the outside world and the
         | only way to "unwrap" IO values. Once again, that is for
         | (obsessive) purity reasons. Alternative applicatives even allow
         | for things as simple as an alternative applicative functor
         | based on zipping instead of nondeterminism.
         | 
         | It's a surprisingly varied technique, going far beyond simple
         | railroads, and offers a neat way to write "only" the happy path
         | while staying pure (allowing simple equational static analysis
         | and unit testing without the need for mocks to re-establish
         | purity). It also offers you many other functorial tools which
         | are all linked by a specific composition behaviour. Further,
         | other applicatives provide extra tools when you just need a
         | functor which represents a specific way to combine two values.
         | 
         | Using alternative methods, you often run out of ways to extend
         | existing code, create something completely unreadable yet
         | somehow isomorphic, or you create something readable and
         | extensible, but due to the lack of purity inherent in some
         | exceptional business logic, it becomes very hard to test
         | without extensive mocking. Toeing the line between purity,
         | readability and extensibility can be done in many ways, but
         | functors and especially monads are among the S-tier when it
         | comes to it. That being said, Haskell can get a bit goofy with
         | it, by no means is purity an absolute value to be always
         | chased. I kind of wish more """mainstream""" languages
         | contained better monadic toolboxes for those times when you see
         | an issue and you know you could solve it better than what you
         | have to otherwise do if you only had the tools for it.
        
         | rco8786 wrote:
         | Is this fundamentally different than returning the data or
         | throwing an exception to be caught elsewhere?
        
           | mpweiher wrote:
           | Define "fundamentally". In the end it all comes down to NAND
           | gates... ;-)
           | 
           | But yes, it is very, very different. With an exception, which
           | does achieve a similar effect of stopping further execution
           | right there and then, you need to throw the exception and you
           | need to check it. With dataflow, all you _have_ to do is
           | nothing.
           | 
           | And exceptions are quite a tricky and heavyweight mechanism.
           | What about the intermediate code? Does it need to run despite
           | the exception? Will it just swallow the exception instead of
           | passing it on? You can just pass the final error handler in
           | and set it on the filter and there is no trickiness.
           | 
           | And of course without the exception, there is a lot of code
           | that needs to do error handling despite not really being
           | involved. This becomes really, really noticeable when your
           | async code is handled using callbacks. You just get two sets
           | of callback _everywhere_. Just having the code be synchronous
           | would be better, but with dataflow the whole issue just
           | evaporated.
        
             | gpderetta wrote:
             | In the end it boils down to continuations: your function
             | either either takes a single return continuation that takes
             | result|error variant or it takes two (or more)
             | continuations: one for the happy path, and one for the
             | error path. The former maps well to result objects, the
             | latter is maps better to non local (checked) exceptions,
             | but it easy to see how to transform from one to the other.
        
               | mpweiher wrote:
               | > In the end it boils down to continuations
               | 
               | No it does not.
               | 
               | > your function
               | 
               | Only if you deal exclusively with procedures/functions.
               | 
               | With dataflow, that's not the case. You have two filters,
               | and if the first filter detects an error, it just does
               | nothing (i.e it does not pass the value to the second
               | filter). Done.
               | 
               | No need for exceptions or continuations, or weird return
               | types.
        
           | golergka wrote:
           | Type system doesn't force you to handle exceptions, they're
           | spooky action at a distance.
        
             | naasking wrote:
             | Some do, like checked exceptions.
        
             | chriswarbo wrote:
             | Indeed, exception handlers are essentially the `COME FROM`
             | madness from INTERCAL ;)
        
             | Karellen wrote:
             | Alternatively, they're just reducing boilerplate.
             | 
             | Yes, anything might return an error. Do we need to really
             | write `or error` or whatever everywhere that all we're
             | going to do is pass the error immediately back up the
             | stack?
             | 
             | One way to improve this would be to have some kind of way
             | of declaring the atypical case where you have a function
             | that can _never_ throw an exception... which is a feature
             | of some exception-based programming languages.
        
               | kamray23 wrote:
               | It can be important and reasonable. You might have things
               | which never give errors. You might have things which
               | sometimes do, and you attempt to use them in places which
               | you don't believe to give errors. Here, the great thing
               | about error functors is that the type system comes out
               | and warns you very, very explicitly. Hell, we add that
               | same boilerplate back in a lot of languages with things
               | like "noexcept" and "throws", with the compiler's type
               | checker doing the exact same thing and forcing error
               | handling in a "noexcept" when it calls something which
               | throws. You get an isomorphic system, it works the exact
               | same way, it's effectively an error functor as far as the
               | type checking is concerned.
               | 
               | That being said, this article sells the whole concept of
               | functors incredibly short. You use them to represent
               | certain concepts, such as in this example where the
               | concept being represented is "value, but not everywhere
               | in the domain" or "value or a different type of value".
               | On top of that, these are applicative functors, meaning
               | that they also represent a way of combining two values in
               | a specific manner, and they are monads, meaning that they
               | represent a kind of composition. That means that these
               | same concepts of railway tracks suddenly allow for a lot
               | larger set of tools: you can represent as a functor the
               | concept of having multiple values, you can consider lists
               | to be applicative functors with the combination of two
               | lists being an operation such as concatenation, zipping
               | of sequences, or even cartesian products. You can even go
               | as far as to consider lists monads, in which case their
               | compositional behaviour could be one of non-determinism:
               | each function takes in a single value and evaluates to
               | multiple values - the composition would be running the
               | function over every value in the list (over all possible
               | states), then concatenating the lists that come out
               | (creating a new list of possible states). Treating errors
               | and non-determinism as special cases of a similar
               | behaviour allows for extending many of the methods we use
               | in error handling to this kind of application as well.
               | Not that these two are the only uses either.
               | 
               | It's a much larger toolbox, though going overboard with
               | it will result in unreadable hellish code.
        
               | rco8786 wrote:
               | > Do we need to really write `or error` or whatever
               | everywhere that all we're going to do is pass the error
               | immediately back up the stack?
               | 
               | Right, exactly. Like in Go:
               | 
               | if (err != nil) return err
               | 
               | You find this littered _all over_ every Go codebase. And
               | somehow people seem to not realize that they're doing the
               | exact same thing as throwing an exception (unwinding the
               | stack until something handles the error), just manually
               | and painfully.
        
         | jiehong wrote:
         | Would you have a code example to help clarify this in my mind?
        
       | bandyaboot wrote:
       | > For example, if I want a recipe for making a loaf of bread,
       | saying "just use flour and an oven" is not very helpful.
       | 
       | > Relationship to the Either monad and Kleisli composition
       | 
       | Well, sure the former isn't very helpful, but I at least know
       | what an oven and flour are.
        
         | greydius wrote:
         | Yes, I also was born knowing about ovens and flour. If these
         | other things were important we'd be born knowing them as well.
        
           | bandyaboot wrote:
           | My comment was mostly made in jest. That being said, it's not
           | about being born with that knowledge, but about the reality
           | that knowledge of what an oven and flour are is reasonable
           | assumed knowledge for anyone who has decided to read
           | instructions for cooking something.
        
         | [deleted]
        
         | the_af wrote:
         | You would learn what Either is (which, by the way, is nothing
         | esoteric) just like you learned what a lambda was, or a string
         | type, or a vector.
        
         | waynesonfire wrote:
         | at least you know what you don't know.
        
           | Cthulhu_ wrote:
           | And you know how to find out what you don't know
           | 
           | ...that said, functional programming and the university level
           | words associated with it intimidate me and quickly go over my
           | head. Mainly because I haven't got a practical application
           | for it, and they're more abstract concepts. A railway switch?
           | Sure, I can understand that. A monad? What? Why are you
           | making up words?
           | 
           | (tongue in cheek, I've had a stint of Scala so I can sort of
           | apply some of these things in practice. I just don't have the
           | vocabulary, educational background, or practical
           | applications)
        
             | thewix wrote:
             | This is probably the biggest problem with FP, and I love
             | FP, and use fp-ts which is marred in category theory. It
             | took me a while to understand concepts that are really
             | quite simple but explained very academically.
             | 
             | Remember, [Monada are just monoids in the category of endof
             | unctors](https://stackoverflow.com/questions/3870088/a-mona
             | d-is-just-...)
             | 
             | That being said, I love FP and am a better programmer today
             | because of it.
        
       | metayrnc wrote:
       | Honesty this whole site is a gold mine. Even if you are not
       | interested in using a functional language, if gives you a
       | different perspective which was very helpful for me. I would
       | recommend other posts as well.
        
         | giraffe_lady wrote:
         | The series on building a parser combinator from scratch has
         | been one of the most valuable things I've read and worked
         | through. A lot of concepts and mechanisms from that have been
         | just incredibly useful working with typed languages in a
         | functional style. I still will never know what a monad is tho.
         | 
         | https://fsharpforfunandprofit.com/series/understanding-parse...
        
           | evntdrvn wrote:
           | Y'all might enjoy this book by the same author!
           | https://pragprog.com/titles/swdddf/domain-modeling-made-
           | func...
        
       | jammycakes wrote:
       | The author followed up this post with another one a few years
       | later titled "Against Railway Oriented Programming":
       | 
       | https://fsharpforfunandprofit.com/posts/against-railway-orie...
       | 
       | Railway-oriented programming is an interesting concept and it
       | does have its use cases, but it does need to come with a massive
       | health warning. I've often seen it used in practice to reinvent
       | exception handling badly, and this is something I consider
       | particularly ill advised because exceptions, when understood and
       | used correctly, provide a much cleaner and more effective way of
       | handling error conditions in most cases.
       | 
       | The thing about exceptions is that in most cases, they make the
       | safe option the default. An error condition is an indication that
       | your code can not do what its specification says that it does,
       | and in that case you need to stop what you are doing, because to
       | continue regardless means that your code will be operating under
       | assumptions that are incorrect, potentially corrupting data.
       | Error conditions can happen for a wide variety of reasons, many
       | of which you do not anticipate and can not plan for, and in those
       | cases the only safe option is to clean up if necessary and then
       | propagate the error up to the caller. Exceptions do this
       | automatically for you by default (you need to explicitly override
       | it with a try/catch block) but alternative approaches, such as
       | railway oriented programming, require you to add in a whole lot
       | of extra boilerplate code that is easy to forget and easy to get
       | wrong. If you can't handle the error condition on the way up the
       | call stack, you would then log it at the top level and report a
       | generic error to the user.
       | 
       | Having said that I see two particular use cases for this kind of
       | technique. The first is situations where you need to handle
       | specific, well defined and anticipated errors right at the point
       | at which they occur. Validation is one example that comes to
       | mind; another example is where you are trying to fetch a file or
       | database record that does not exist. The second is situations
       | where exception handling is not available for whatever reason.
       | Asynchronous code using promises (for example with jQuery) are
       | pretty much an exact implementation of railway oriented
       | programming, but since modern JavaScript now has async/await, we
       | can now use exception handling in these scenarios.
        
         | jmull wrote:
         | I think the conventional way exceptions are implemented is
         | pretty bad.
         | 
         | First, a lot of languages make you use an awkward, unnecessary
         | scope to catch an exception. e.g., you want to declare and
         | initialize a variable to the value of a function that can throw
         | (and assign some other value if it does. Well, you've got to
         | split the declaration and initialization, putting the
         | declaration outside the scopes try and catch create. That one's
         | an unforced error -- languages don't have to do that to use
         | exceptions, but for some reason many do. It's pretty weird to
         | have to add homespun utilities for fundamental control flow
         | scenarios.
         | 
         | But the bigger issue is that you really want to handle the
         | error conditions at the lowest level where you have enough
         | context to do so correctly. That's _usually_ pretty low, but
         | exceptions default to  "send it all the way to the top". The
         | default is either invisible or invisible in practice, depending
         | on the language, and wrong, so programs end up riddled with
         | these issues. You tend to end up with these higher-level
         | functions that can throw all kinds of exceptions, many of which
         | are meaningless to the caller. E.g. someone adds a file cache
         | one day and all of a sudden some higher-level HandleRequest
         | function can through a IO exception... because the cache code
         | didn't handle it... because they never even realized it was a
         | possibility. You couldn't design a better mechanism for
         | creating leaky abstractions.
         | 
         | I think anything a function might return needs to be an
         | explicit part of its signature, and a caller needs to handle it
         | explicitly, even if just to indicate, pass it up the line. The
         | langue doesn't need to require a lot of boilerplate to do this.
         | 
         | That's just my experience from having lived through it.
        
           | thuridas wrote:
           | If you catch them at the lowest level you end with N
           | different exceptions that are coupled with implementation.
           | 
           | I only need different exceptions if I am treating them
           | different.
           | 
           | Oh! And you typically catch it next to the failure ( maybe
           | wrapping it in another exception) out in a global exception
           | handler
        
         | sasmithjr wrote:
         | In my mind, the difference between errors-as-values and
         | exceptions is most useful when describing domain-specific
         | errors and other issues that you have to handle in support of
         | the domain/problem space. To me, domain errors make sense as
         | errors-as-values, but your database being unreachable is
         | unrelated to the domain and makes sense as an exception.
         | 
         | > another example is where you are trying to fetch a file or
         | database record that does not exist
         | 
         | I think this depends on whether or not you expect the
         | file/record to exist. Handling a request from a user where the
         | user provided the id used for lookup? The lookup itself is
         | validation of the user input. But if you retrieved a DB record
         | that has a blob name associated with it and your blob storage
         | says that a blob doesn't exist by that name? I find that to be
         | a great situation for an exception.
         | 
         | The errors-or-exception line is fuzzy and going to be dependent
         | on your team and the problems you're solving, but I've found
         | that it's a decent rule of thumb.
        
         | piaste wrote:
         | > Exceptions do this automatically for you by default (you need
         | to explicitly override it with a try/catch block) but
         | alternative approaches, such as railway oriented programming,
         | require you to add in a whole lot of extra boilerplate code
         | that is easy to forget and easy to get wrong.
         | 
         | The unfortunately missing part of exceptions (in mainstream
         | languages) is that they handle this invisibly. Figuring out, at
         | compile time, what sort of exceptions can appear inside a given
         | function is not obvious.
         | 
         | That's the big payoff of ROP: you can look at any function
         | signature and immediately know what sort of errors can come out
         | of it.
         | 
         | Mitigating the downside of ROP (boilerplate) can be done to
         | various extents, depending on the language. Haskell has do-
         | notation. In F#, using the result computation expression [0]
         | can make your code extremely clean:                   type
         | LoginError = InvalidUser | InvalidPwd | Unauthorized of
         | AuthError              let login (username : string) (password
         | : string) : Result<AuthToken, LoginError> =           result {
         | // requireSome unwraps a Some value or gives the specified
         | error if None             let! user = username |> tryGetUser |>
         | Result.requireSome InvalidUser                  // requireTrue
         | gives the specified error if false             do! user |>
         | isPwdValid password |> Result.requireTrue InvalidPwd
         | // Error value is wrapped/transformed (Unauthorized has
         | signature AuthError -> LoginError)             do! user |>
         | authorize |> Result.mapError Unauthorized
         | return user |> createAuthToken           }
         | 
         | Could we do the reverse, i.e. mitigate the downside of
         | exceptions? Is there a linter, code analyzer, or some other
         | compile-time tool that can integrate with a Java IDE and
         | automatically display the uncaught exceptions that might be
         | thrown by a given line of code?
         | 
         | [0] https://demystifyfp.gitbook.io/fstoolkit-
         | errorhandling/fstoo...
        
           | bertylicious wrote:
           | Java has/had a compiler check that forced you to write catch
           | blocks or `throws` annotations in/on functions that call
           | other functions which might throw. The feature is called
           | "checked exceptions" and I believe it has been discarded for
           | its inconvenience by now.
           | 
           | Sometimes it feels like developers are going in circles while
           | trying to find the most optimal way to handle errors.
        
             | brabel wrote:
             | People are now realizing that having the errors a function
             | can cause right in the type system may actually have been a
             | good idea, but when you point out that Result is not the
             | only way and that Java checked Exceptions do the exact same
             | thing (and so does the Zig error handling mechanism which
             | is a third variant of the idea), they come up with all
             | sorts of easily dismissable nonsense to explain why the two
             | are very different.
        
               | oblio wrote:
               | What would you reply to this comment:
               | 
               | https://news.ycombinator.com/item?id=37174698
        
               | layer8 wrote:
               | I'm not the parent, but exception declarations are IMO
               | necessary for a stable API contract. It's exactly the
               | same reason why return types are explicit. The actual
               | issue in Java is that you can't abstract over an
               | arbitrary-length list (sum) of checked-exception types
               | (variadic type parameters) (with the exception of
               | rethrowing from multi-catch clauses).
        
               | brabel wrote:
               | Well, that's kind of true! The fact checked Exceptions
               | are inconvenient doesn't change the fact they are
               | equivalent to returning a Result type (the implementation
               | is obviously different but I think we don't need to
               | mention that).
               | 
               | A future version of Java could totally make it more
               | convenient, and perhaps even make the implementation
               | cheaper such that it would not just nearly the same , but
               | literally the same as in Rust or other similar languages.
        
               | piaste wrote:
               | Does it even need a new language version? If the compiler
               | already spits out the error "hey, your Fart() function
               | should be annotated with 'throws ButtsException'",
               | couldn't an IDE relatively easily be configured to
               | automatically add the " throws " annotations?
        
             | layer8 wrote:
             | Checked exceptions are one of the main reasons I'm sticking
             | with Java, even though Java lacks the ability to abstract
             | over sets of checked exceptions, which does cause some
             | inconvenience. It's unfortunate that no other mainstream
             | languages have been taking that approach.
        
               | 1-more wrote:
               | That sounds awesome! Do you have that flag set on a big
               | codebase? Was it a big hassle to turn it on (like you had
               | to remediate a bunch of code that didn't handle
               | exceptions before you could check it in). Have you seen
               | any big changes since enabling it?
        
               | gbear605 wrote:
               | Checked exceptions in Java are the default state and much
               | of the standard library uses them.
        
               | wizzwizz4 wrote:
               | In languages with a Result type (or Either monad), you
               | basically do have checked exceptions, even though the
               | language _has no exceptions_.
        
               | ivolimmen wrote:
               | Java also has that option. There is an excellent library
               | called vavr. It adds the Try and Either monad to Java.
        
               | layer8 wrote:
               | With result types, you typically don't get automatic
               | exception propagation. I agree that overall it's a
               | spectrum of syntactic convenience, checked exceptions
               | effectively form a sum type together with the regular
               | return type.
        
             | thuridas wrote:
             | After working with railway oriented programming with Arrow
             | in Kotlin I had the same feeling of using checked
             | exceptions.
             | 
             | And please make the Errors generically wrappable in order
             | to avoid losing the traces.
        
           | jammycakes wrote:
           | > The unfortunately missing part of exceptions (in mainstream
           | languages) is that they handle this invisibly. Figuring out,
           | at compile time, what sort of exceptions can appear inside a
           | given function is not obvious.
           | 
           | Figuring out, at compile time, what sort of exceptions appear
           | inside a given function is a futile exercise in many
           | contexts, and railway oriented programming does not fix it.
           | Java tried this with checked exceptions and it fell out of
           | favour because it became too unwieldy to manage properly.
           | 
           | In any significantly complex codebase, the number of possible
           | failure modes can be significant, many of them are ones that
           | you do not anticipate, and of those that you can anticipate,
           | many of them are ones that you cannot meaningfully handle
           | there and then on the spot. In these cases, the only thing
           | that you can reasonably do is propagate the error condition
           | up the call stack, performing any cleanup necessary on the
           | way out.
           | 
           | "Handling this invisibly" is also known as "convention over
           | configuration." In languages that use exceptions, everyone
           | understands that this is what is going on and adjusts their
           | assumptions accordingly.
        
             | Karunamon wrote:
             | Checked exceptions always seemed to me to be an exercise of
             | self-flagellation and enumerating badness; when most of the
             | time there are a handful of specific errors that require
             | special handling, with everything else logged/return
             | error/possibly crash.
        
               | layer8 wrote:
               | The problem is that the callee can't decide for the
               | caller which exceptions will require special handling.
               | And for the caller to be able to make an informed
               | decision about that, the possible exceptions need to be
               | documented. Since this includes exceptions thrown from
               | further down the call stack, checked exceptions are about
               | the only practical way to ensure that all possible
               | failure modes get documented, so that callers are able to
               | properly take them into account in their program logic.
        
               | jammycakes wrote:
               | If you want to (and are able to) document all possible
               | failure modes, then checked exceptions will give you
               | that. As far as I can tell, railway oriented approaches
               | can't.
               | 
               | Unfortunately, you can only do that when the number of
               | possible failure modes is fairly limited. In a complex
               | codebase with lots of different layers, lots of different
               | third party components, and lots of different
               | abstractions and adapters, it can quickly become pretty
               | unwieldy. And then you end up with someone or other
               | deciding to take the easy way out and declaring their
               | method as "throws Exception" which kind of defeats the
               | purpose.
        
               | layer8 wrote:
               | You adjust the reported failure modes to the abstraction
               | level of the respective function, wrapping underlying
               | exceptions if necessary. You don't leak implementation
               | details via the exception types. Callers can still unwrap
               | and inspect the underlying original exceptions if they
               | want, but their types won't typically be part of the
               | function's interface contract, similar to how specific
               | subtypes of the declared exception types are usually not
               | part of the contract.
        
               | catiopatio wrote:
               | No; you simply abstract the underlying subsystem's
               | exceptions in your own types, the same way you do with
               | any other type.
               | 
               | And yes, "railway oriented approaches" can absolutely do
               | this.
        
               | jammycakes wrote:
               | > No; you simply abstract the underlying subsystem's
               | exceptions in your own types, the same way you do with
               | any other type.
               | 
               | That's all very well as long as people actually do that.
               | It doesn't always happen in practice. And even when they
               | do, the abstractions are likely to be leaky ones.
               | 
               | > And yes, "railway oriented approaches" can absolutely
               | do this.
               | 
               | How? Please provide a code sample to demonstrate how you
               | would do so.
        
               | catiopatio wrote:
               | > That's all very well as long as people actually do
               | that. It doesn't always happen in practice. And even when
               | they do, the abstractions are likely to be leaky ones.
               | 
               | They don't have a choice under "railway oriented" API in
               | a typesafe language -- they _must_ translate the
               | subsystem's error types to their own error type.
               | 
               | If the abstraction is leaky, at least it's well-
               | specified.
               | 
               | How is that worse than having no abstraction at all, and
               | leaving callers with no idea what error cases an API
               | might raise?
               | 
               | > How? Please provide a code sample to demonstrate how
               | you would do so.
               | 
               | In what language? What data structure?
               | 
               | If we assume Haskell and Either, then it can be as
               | trivial as:                 first mapError someResult
               | 
               | Scala's Either?
               | someResult.left.map(mapError)
        
             | naasking wrote:
             | > Java tried this with checked exceptions and it fell out
             | of favour because it became too unwieldy to manage
             | properly.
             | 
             | Because they did a half-assed job of it, and required the
             | user to explicitly propagate error signatures. Inference
             | and exception polymorphism are essential.
        
         | fsloth wrote:
         | "The first is situations where you need to handle specific,
         | well defined and anticipated errors right at the point at which
         | they occur"
         | 
         | Barring system level errors can you give an example of an error
         | state that's not like that, that would then rather merit an
         | exception? I would like to understand your point of view, is it
         | due to the nature of the problem, or the constraints of runtime
         | that make exceptions preferable.
         | 
         | In the C++ code I need to write, we can 1. check data for error
         | conditions in the beginning 2. if we fail the error check, let
         | application crash 3. use the found error state to debug and fix
         | the error in the initial checking code.
         | 
         | The data my code needs to process is fairly straightforward -
         | data abiding by some known CAD data format or given geometric
         | topology, so the error conditions are "quite easy" to tackle in
         | the sense that there is an understanding what correct data
         | looks like in the first place.
        
           | xmcqdpt2 wrote:
           | C++/Rust are different because exceptions in those languages
           | are expensive and culturally counter indicated.
           | 
           | For the runtime-hosted languages the author is talking about
           | (JVM, CLR, Python etc.), optionally throwing an exception is
           | much cheaper than constantly creating and unwrapping Result
           | objects. Your example is a perfect case where one would
           | prefer to throw: say you have a parser that parses your file
           | and the parser is expensive because the files are large. You
           | are better off throwing out of your parsing iteration then
           | doing a Result.map in your hot loop. (However you might want
           | to wrap the top level of the parser in a Result and return
           | that.)
        
           | jammycakes wrote:
           | Missing dependencies. External services having gone offline.
           | Timeouts. Foreign key violations. Data corruption. Invalid
           | user input. Incorrect assumptions about how a third party
           | library works. Incorrectly configured firewalls. Bugs in your
           | code. Subtle incompatibilities between libraries, frameworks
           | or protocols. Botched deployments. Hacking attacks. The list
           | is endless.
           | 
           | Probably not so much of an issue if you're dealing with well
           | validated CAD data and most of your processing is in-memory
           | using your own code. But if you're working with enterprise
           | applications talking to each other via microservices written
           | by different teams with different levels of competence,
           | legacy code (sometimes spanning back decades), complex and
           | poorly documented third party libraries and frameworks,
           | design decisions that are more political than technical, and
           | so on and so forth, it can quickly mount up.
        
             | catiopatio wrote:
             | External services having gone offline, timeouts, and
             | invalid user input are expected conditions you should
             | handle locally.
             | 
             | Almost everything else you listed represents a bug in your
             | software that should terminate execution.
             | 
             | I'm more than a little shocked that you think yeeting
             | exceptions up the call stack is appropriate for these
             | cases.
        
         | bertylicious wrote:
         | I think Rust has shown very well how ROP with first-class
         | syntax support pretty much eliminates all boilerplate code.
         | IMHO Rust nailed error handling with the `Result` type/trait.
        
           | scotty79 wrote:
           | It came to my mind too but then I got confused, what if the
           | type of the Result changes along the function call chain and
           | you want to propagate Errors with minimal effort?
           | 
           | Then I saw this stackoverflow question and it seems that ?
           | operator does quite smart thing and is as easy to use as
           | possible.
           | 
           | https://stackoverflow.com/questions/31172451/is-there-a-
           | non-...
        
             | mprovost wrote:
             | The Rust community is converging on the anyhow crate to
             | chain multiple error types without having to enumerate
             | each.
        
               | [deleted]
        
               | benecollyridam wrote:
               | It looks like thiserror (6.6M downloads/month) is more
               | popular even though anyhow (5.7M downloads/month) is
               | listed as #1 https://lib.rs/keywords/error
        
               | dathinab wrote:
               | they are not competing
               | 
               | they handle two different cases
               | 
               | anyhow is for type erased errors, which is mainly used
               | for the kind of errors you mainly propagate upward
               | without handling them in any fine grained way. It's
               | mainly used in applications (instead of libraries). For
               | example in a web server anyhow errors will likely yield
               | Internal Serer errors.
               | 
               | thiserror provides a derive (codegen) to easily create
               | your own error. It's much more often used by libraries,
               | but if an application doesn't want to handle this errors
               | they will likely be converted into anyhow errors. A very
               | common use case is to apply it on an enum which represent
               | "one of many errors" e.g. as a dump example `enum Error {
               | BadArgument(...), ConstraintViolation(...), ... }` and
               | it's no absurd in some cases to have a mixture e.g. an
               | enum variant `Unexpected(anyhow::Error)` which represents
               | various very unpexted errors which likely could be bugs
               | and you might have considered panicing there but decided
               | to propagate them instead to avoid panic related problems
        
               | Farmadupe wrote:
               | Download counts don't mean very much here as I'm fairly
               | sure both crates are common transitive dependencies. Or
               | in other words, millions of programmers aren't
               | individually choosing Anyhow or Thiserror on a monthly
               | basis -- they're just dependencies of other rust crates
               | or apps.
               | 
               | And agreeing with the other reply, nobody jumps up and
               | down with joy when choosing an error handling crate. You
               | pick the right poison for the job and try not to shed a
               | tear for code beauty as you add in error handling.
        
               | dathinab wrote:
               | this is a gross oversimplification
               | 
               | anyhow is the most commonly used crate to have type
               | erased errors(1), nothing more then that but also nothing
               | less
               | 
               | this means when returned form a library a Result<_,
               | anyhow::Error> _is often an anti-pattern_ (often not
               | always!)
               | 
               | but if you write an application it's pretty common to
               | have many many places in the code where you can be sure
               | that no upstream code needs more fine grained error
               | handling (because you workspace is the most upstream
               | code) so using anyhow is a pretty common and convenient
               | choice
               | 
               | Though it's not unlikely for anyhow to fade into being
               | mostly unused in the future with further currently
               | missing rustc/std features, through not anytime soon.
               | 
               | But luckily this doesn't matter, due to how `?` works you
               | can trivially convert errors on the fly no matter which
               | (well kinda, there is an unlucky overlap between orphan
               | rules and From wildcard implementations in the anyhow
               | crate, but we can ignore that for this discussion).
               | 
               | (1): It's basically a form of Box<dyn Error + Send + Sync
               | + 'static> which also has thin pointer optimizations and
               | (can) by default include a stack trace + some convenience
               | methods.
        
               | mprovost wrote:
               | Sure, but the question was specifically looking for the
               | "minimum effort" solution. I almost brought up thiserror
               | but that just makes things more complicated. If you're
               | writing a Rust application and just want to propagate
               | errors, anyhow is currently the most popular way to do
               | that.
        
         | pharmakom wrote:
         | > An error condition is an indication that your code can not do
         | what its specification says that it does
         | 
         | Meanwhile, in real code-bases, exceptions are used for all
         | sorts of expected but less common situations, like
         | ProductNotFoundException
        
       | 1234543y24624 wrote:
       | [dead]
        
       | louthy wrote:
       | > Railway Oriented Programming
       | 
       | This term needs to die.
       | 
       | I have lost count of how many times people have asked me, on my
       | language-ext [1] issues pages, if the library supports "Railway
       | Oriented Programming". The documentation is clear, it is an FP
       | library with many monads, functors, etc.
       | 
       |  _Inventing new terminology for existing concepts just creates
       | even more confusion_.
       | 
       | Creating completely new terminology for existing concepts just
       | because you don't like the words is utterly baffling. Learn them.
       | If you're going to teach monads then by all means use railways as
       | an analogy, but don't call it "Railway Oriented Programming", you
       | are just misleading the reader and not helping them communicate
       | with others in the FP community.
       | 
       | Did anyone know what 'polymorphism' was when they first
       | encountered OOP? No, they learned what it meant. Learn what
       | 'monad' means, learn what 'functor' means, learn what
       | 'applicative' means. You'll be enlightened.
       | 
       | OOP people: not everything needs to be 'oriented' ;-)
       | 
       | [1] https://github.com/louthy/language-ext/
        
         | bregma wrote:
         | Came here to read discussions on EN 50128. Was awfully
         | disappointed.
        
         | fsloth wrote:
         | I think it's quite common pragmatic patterns have multiple
         | names and parallel theoretical frameworks within to understand
         | them.
         | 
         | "A monad" is just a name to the applied pattern. There is
         | nothing called "monad" in C++ for example, but you can write
         | expressions in C++ that are isomorphic with monads. That does
         | not mean that "monad" is the only correct name for the pattern,
         | given people are familiar with different names and theoretical
         | frameworks for the given pattern.
         | 
         | The fact that the pattern is recognized as 'monad' is really
         | usefull of course, since that implies all sorts of other things
         | that may turn out to be usefull. But it does not mean IMO it's
         | wrong to call it by some other popular name.
         | 
         | The analogue in mathematics is rotation in a plane of the unit
         | vector. We can present it as pair (cos(alpha),sin(alpha), a 2x2
         | matrix (m00,m01,m10,11), or even a complex number e^i alpha.
         | 
         | "I'm rotating a vector" ... "no no, you are perfoming a matrix
         | multiplication" ... "don't be silly, it's a complex number"...
        
         | FrustratedMonky wrote:
         | It is a handy metaphor.
         | 
         | Like all handy metaphor's, it helps people conceptualize an
         | idea.
         | 
         | That doesn't mean it is a strict technical term, you can break
         | down all metaphors if you want.
         | 
         | "He is the light of my life."
         | 
         | "NO, he is not a 'Light', he is a human, read a biology book".
         | 
         | LOL. Just realized you are Drax? Drax is posting on HN?
        
           | louthy wrote:
           | Sure it's a metaphor and as I state, there's nothing wrong
           | with using the railway analogy, but _renaming_ 'monads' as
           | 'Railway Oriented Programming' is having the effect that
           | newbies to FP-land think that's what _programming-with-
           | monads_ is _called_.
           | 
           | Then they try to have discussions with others in FP-land and
           | realise they don't have the lexicon to talk about FP because
           | they've been taught some spurious terminology. This is a
           | monad tutorial, it is over 150 slides worth of explanation
           | (probably the largest 'Yet Another Monad Tutorial' I've seen
           | yet), there's no reason to leave the reader thinking they've
           | learned some new concept called 'Railway Oriented
           | Programming'.
        
             | xupybd wrote:
             | Specifically this is the error/result monad. This was the
             | first description that made sense to me as a newbie to all
             | of this. Monad was a scary word. This helped me understand.
        
             | FrustratedMonky wrote:
             | I was being bit harsh.
             | 
             | Upon reflection I think you have a good point.
             | 
             | When learning about functional programming, the metaphors
             | did help. But then later it was hard to recognize them in
             | other languages that used different metaphors for same
             | thing. (maybe all language is metaphor?).
             | 
             | So if F# Computation Expression, was little more expressly
             | outlined as a 'Monad', and how, maybe that would help. It
             | just can't be all at once when learning.
             | 
             | Same for <Option> and error handling. There is the baby-
             | step phase, then growing into details.
             | 
             | So this railway presentation might have been helped by a
             | little cross-connecting the 'simple helpful metaphors',
             | with the 'technically correct words you'd find in a math
             | book or something'.
             | 
             | Like a few slides at beginning or end, with further reading
             | or examples showing 'simple metaphor' = 'over complicated
             | word'.
             | 
             | This does happen in objects.
             | 
             | How many 'typical introduction' books spend a lot of time
             | talking about Ducks and Dogs, and Quacking and Barking,
             | Is-A, Has-A, before it gets into technical terminology?
             | 
             | People still talk about 'Duck Typing'. Not the technical
             | word. Maybe that is bad also.
        
               | louthy wrote:
               | Thank you for that.
               | 
               | One series that I thought did this really well was the
               | Eric Meijer (father of LINQ in C#) lectures on FP
               | fundamentals [1]. It was very much for OO programmers to
               | get up to speed with FP and it was taught using Haskell.
               | 
               | He goes through the series, building up a parser in
               | Haskell, and then at the end (or a reasonable way
               | through) he says "and that's a monad". Never once
               | mentioning it until the watcher had grokked all the
               | concepts up until that point.
               | 
               | That 'big reveal' idea I think is probably the best way
               | to do a monad tutorial, because it forces the tutor to
               | stick to the motivations and the fundamentals.
               | 
               | [1] https://www.youtube.com/watch?v=UIUlFQH4Cvo&list=PLoJ
               | C20gNfC...
        
         | GhostWhisperer wrote:
         | if people cannot learn the concepts when presented with the
         | monad and functor names, but somehow are able to understand
         | them when given different names, it seems the issue is the
         | names are bad
         | 
         | what is creating confusion is this need to sound like a pompous
         | academic by sticking to old terminology that serves no purpose
         | but to make their users sound enlightened and the subject hard
         | to grok
         | 
         | the functional programming community could gain a lot from
         | making it's subject more accessible, not less
        
           | louthy wrote:
           | How is it any different to someone learning OOP?
           | 
           | * Objects
           | 
           | * Class
           | 
           | * Interface
           | 
           | * Polymorphism
           | 
           | * Parametric polymorphism
           | 
           | * Methods
           | 
           | * Method dispatch
           | 
           | * Inheritance
           | 
           | * Singleton
           | 
           | These bear no relationship to the real world usage of these
           | words and some are straight out of mathematics/academia also.
           | 
           | Would you expect FP people landing in OOP land to rename all
           | of these concepts just so FP people can understand OOP
           | languages? Or would you expect them to learn the shared
           | lexicon of OOP-land?
           | 
           | It isn't pompous to have a name for something and then expect
           | people to learn those names if they want to learn the
           | subject. Pomposity would be attaching "oriented programming"
           | to the end of all concepts that you don't understand and
           | telling the world to use your new terminology whilst
           | trampling over the existing community.
           | 
           | Clearly grokking monads is hard. But it's nothing to do with
           | the terminology. This presentation has over 150 slides - so
           | even when it's called 'Railway Oriented Programming' it takes
           | 150 slides worth of exposition to get the point across. I'd
           | argue that the reader/listener would be in the same place if
           | they'd have used railways as an analogy whilst telling the
           | reader that they're learning their first monad.
           | 
           | 'Monad' may well be an awkward word but it's clearly the
           | mental model of the monad that's the problem. I suspect (as
           | someone who's taught how monads work many times) that part of
           | the problem is that once we learn how monads work, we realise
           | they're unbelievably trivially simple. And at that moment we
           | instantly lose the ability to explain it to somebody else
           | because of the _' mental perspective switch'_ that's just
           | happened in our brains.
           | 
           | So we end up with a 1000 'Yet Another Monad' tutorials. This
           | may well be the best one. But it shouldn't mean you get to
           | change the shared lexicon.
        
             | piaste wrote:
             | Hot take: some of those names are bad too (e.g.
             | 'polymorphism' is very vague), and OOP would be easier to
             | learn if there was a beginner's vocabulary.
             | 
             | Yes, "dynamic structural typing" is a coherent and logical
             | name when you're familiar with type theory.
             | 
             | But a learner can understand "duck typing" much faster. The
             | jargon can come later.
             | 
             | > 'Monad' may well be an awkward word but it's clearly the
             | mental model of the monad that's the problem. I suspect (as
             | someone who's taught how monads work many times) that part
             | of the problem is that once we learn how monads work, we
             | realise they're unbelievably trivially simple.
             | 
             | I'm on team "join is way easier to understand than bind".
        
             | GhostWhisperer wrote:
             | none of it matters if you hope more people use and
             | understand functional programming
             | 
             | you overestimate how much people actually understand oop vs
             | just winging it, in particular, inheritance and
             | polymorphism
             | 
             | but back to fp, if monads are, and i'm growing to agree
             | with you this, "unbelievably trivially simple," how can it
             | be that "grokking monads is hard"?
             | 
             | i posit that it is only "hard" because when learning about
             | these concepts, we read shit like "a monad is a monoid in
             | the category of endofunctors" - that's an exaggeration but
             | one that clearly shows how absurd some fp texts sound to
             | outsiders
             | 
             | edit: i get worked up with folks that write about fp the
             | way you argue it must be written. for the few concepts i
             | have been able to grasp i have been thoroughly impressed,
             | but i am being impedded from learning more, or faster
             | because of the hard to parse lingo
        
               | louthy wrote:
               | > none of it matters if you hope more people use and
               | understand functional programming
               | 
               | On a personal level, I couldn't care less tbh. If people
               | want to learn it, that's great, if not that's also fine.
               | There's plenty of room for procedural, OOP, and FP to
               | exist side-by-side. If one domain feels complex to you,
               | don't do it (or put extra effort into learning it), but
               | I'd argue that OOP is much more complicated - it's just
               | that most devs tend to grow up with OOP and so the
               | context switch to FP is more difficult.
               | 
               | > you overestimate how much people actually understand
               | oop vs just winging it, in particular, inheritance and
               | polymorphism
               | 
               | Forget 'polymorphism' for a second and think about
               | 'object'. An object in the real world doesn't have
               | behaviours attached to it. An object in the real world
               | doesn't mutate, in-place, in a discreet portion of time.
               | Objects in the real world have a immutable past, a
               | present state, and multiple possible futures based on
               | interactions with _external events_. Those interactions
               | is literally how we define time. OOP (as it 's commonly
               | practiced) does away with time and has myriad
               | complexities because of it.
               | 
               | The trivial OOP explanation "an object is like a thing in
               | the real world. A rabbit is an object, a triangle is an
               | object, etc." combined with the unbelievable complexity
               | artefacts (in place mutation, hidden state, attached
               | behaviours, etc.) - is much, much worse than the upfront
               | cost of learning about: pure functions, monadic
               | composition, etc. (IMHO of course).
               | 
               | My argument would be that the so called simplicity of OOP
               | terms and the alleged simplicity of learning them
               | actually comes with ton of baggage. The upfront effort
               | with monads or any of the other FP concepts at least is
               | rewarded with code that's more 'honest', robust, and can
               | properly model time.
               | 
               | > i posit that it is only "hard" because when learning
               | about these concepts, we read shit like "a monad is a
               | monoid in the category of endofunctors" - that's an
               | exaggeration but one that clearly shows how absurd some
               | fp texts sound to outsiders
               | 
               | There's probably an element of that. There's definitely
               | two camps in FP. Those that come at it from an academic
               | standpoint and they think about the abstraction and what
               | it means for composition and the like. Then there's the
               | jobbing FP peeps - who actually use it in real world code
               | - they might think in terms of concrete monadic
               | implementations, like List, Option, Either, etc. but also
               | larger more domain specific monads: like a FrontEnd
               | monad, or a DataLayer monad.
               | 
               | To fully grok monads (or at least to get the most out of
               | them) you kinda need to know both and why they can be
               | useful to you. So, maybe there's an element of that.
               | Because a monad can literally encapsulate any behaviour
               | you want it's sometimes quite hard to talk about their
               | possibilities without going into the theory.
               | 
               | But I think there's another aspect. In languages that
               | have first-class support for monads ('do' notation in
               | Haskell, LINQ in C#, Computation Expressions in F#, etc.)
               | they don't actually work like any other paradigm in
               | programming. The idea that something runs 'in between the
               | lines' of your code and the 'thing' that runs _is the
               | flavour of monad you 're in_; is just different to say a
               | visitor pattern, or an adapter, etc. that have explicit
               | invocations.
               | 
               | So yeah, I think it's just one of those things that takes
               | a bit of time to get in your head. But once you do the
               | possibilities are enormous, you give whole sections of
               | code a 'flavour' rather than it being an explicit
               | invocation of a behaviour. It adds a completely new tool
               | to your programming toolkit: one that's unlike any other.
               | 
               | We see this turning up in things like async/await. Most
               | people understand that once you have some awaitable code,
               | everything around it becomes async _' flavoured'_. That's
               | what happens with all monads. An Option monad will make
               | the whole code block optional, a List monad will iterate
               | a list for every line of code - and the result is a List,
               | a domain-specific monad might carry the configuration of
               | the application, or a database connection string, so you
               | don't have to do it manually, etc. The monad is simply
               | the encapsulation of the _flavour_.
               | 
               | The phrase "a monad is a monoid in the category of
               | endofunctors" is more of a joke than anything else from
               | 'A Brief, Incomplete, and Mostly Wrong History of
               | Programming Languages' [1]. It may actually be true, if
               | you want to see how that phrase comes about it's worth
               | watching Bartosz Milewski's Category Theory series [2] ,
               | he gets to it about 10 episodes in, but you need to watch
               | them all to understand it.
               | 
               | However most FP programmers wouldn't know what that
               | phrase means. It's absolutely not required to learn CT to
               | know how monads work, or even what makes the abstraction
               | so powerful. The same with other terms inherited from CT,
               | like 'functor', 'monoid', 'polymorphism' (!!!), etc. The
               | programming language version of these things are not the
               | same as the maths versions - although they're clearly
               | inspired by the maths.
               | 
               | Having said all that, I was surprised at how simple CT
               | was when I started learning it. I am certainly no CT
               | expert, but there's literally 3 or 4 rules to learn and
               | you're done (in terms of what's useful for programmers).
               | There's lots of higher maths stuff that's coming out of
               | it that's mostly irrelevant for programming, but one
               | thing I got from it was a new way to think about
               | structure.
               | 
               | It's like a 10,000 foot view of the schema of an
               | application where you stop thinking about the data and
               | you start thinking about the relationships between types.
               | This is a powerful tool when trying to get a handle on
               | the complexity of a system.
               | 
               | You can't write code in CT, but, just like with monads,
               | knowing it and knowing some of the theory behind it,
               | gives you some programming _superpowers_ (I know that
               | sounds grandiose, I just couldn 't think of a better
               | description).
               | 
               | And the end of the day _everything_ in FP is about
               | function composition. Monads are functions, functors are
               | functions, monoids are functions. The name attached just
               | describes the _shape_ of the functions needed for the
               | composition to work.
               | 
               | Understanding why those shapes are useful is learning
               | your craft.
               | 
               | [1] https://james-iry.blogspot.com/2009/05/brief-
               | incomplete-and-...
               | 
               | [2] https://www.youtube.com/watch?v=I8LbkfSSR58&list=PLbg
               | aMIhjbm...
        
               | GhostWhisperer wrote:
               | i'm not reading any of that
        
             | thewix wrote:
             | When I was trying to learn monads no one ever gave me an
             | example of mapping a list-returning function to a list.
             | Everyone went on about 'effects', monad laws, etc. For some
             | of us it's best to work backwards from something concrete
             | and then show why the laws are important.
             | 
             | I love how simple most things are in FP compared to OO. I
             | also hate how poorly FP concepts are explained.
             | 
             | I'm not saying you're wrong about people learning
             | terminology. However, you definitely get more strange looks
             | from FP terms than OO terms. Class, interface and object
             | are common terms. When I mention a word like 'monad',
             | 'monoid', 'magma', or 'functor' people look at me like I'm
             | nuts. It's not logical. A new word is a new word. It's just
             | FP words sound almost alien and trigger extra confusion in
             | people.
        
       | bruce343434 wrote:
       | So basically, monadic error handling, that is, using
       | `Optional<T>` and `Error<T>` and just `map`ping everything (which
       | is a nop on the nil and error variants)
        
         | jgilias wrote:
         | Which is how things actually work in non-FP languages too. In
         | those you're _always_ within Exception<T> and MaybeNull<T>
         | monads. It's just implicit and we tend to just cross our
         | fingers and hope it's all well.
        
           | mrkeen wrote:
           | To drive the point home a little a little harder:
           | 
           | I don't use Haskell because it has a representation for
           | values which might exist (all languages have that), I use it
           | because it has a representation for values which do exist.
        
             | tome wrote:
             | A little harder still:
             | 
             | It does _not_ have a representation for values which do
             | _not_ exist!
             | 
             | (see also: "make invalid states unrepresentable")
        
       | teddyh wrote:
       | Not to be confused with Railroad Syntax Diagrams:
       | <https://en.wikipedia.org/w/index.php?title=Syntax_diagram&ol...>
        
         | dgivney wrote:
         | Or Rail, the programming language
         | 
         | https://esolangs.org/wiki/Rail
        
       | el_oni wrote:
       | Elixir has a nice take on this with the `with` keyword/macro
       | with {:ok, file_handle} <- File.open(filename),
       | {:ok, contents} <- IO.read(file_handle),                   {:ok,
       | parsed} <- MyModule.parse(contents)              do
       | {:ok, parsed}              end
       | 
       | what this does is run the functions in order top to bottom, and
       | if the return value from each function doesn't match with what is
       | on the left, it returns early with the thing that didn't match,
       | otherwise it continues.
       | 
       | This means you don't need to write each function to take a tuple
       | of {:ok, value} and another clause to take {:error, reason}, you
       | can just write your functions to take the value they care about
       | and let pattern matching in the with block to take care of error
       | propagation.
       | 
       | so if File.open returns {:error, reason} then IO.read never
       | executes and the result of the with is {:error, reason}
       | 
       | It essentially means you can program the happy path and let the
       | caller match on the sad paths (if they want to)
        
         | bjourne wrote:
         | What is the advantage of that over exceptions? And what if
         | IO.read fails? Who closes the file handle?
        
           | el_oni wrote:
           | The file handle is the PID of the process that opened the
           | file, it monitors the process that asked for the file to be
           | opened. and if that process goes down the file will be
           | closed.
           | 
           | You can also add an else in the with so it looks like
           | with {:ok, fh} <- File.open(filename),              {:ok,
           | contents} <- IO.read(fh)         do              contents
           | else              {:error, reason} -> File.close(fh)
           | {:error, reason} # this will return after the file has been
           | closed         end
           | 
           | or maybe you would prefer to open the file, and pass that
           | into the with and either way close the file. I just used IO
           | as an example because they return nice {:ok, x} or {:error,
           | reason} tuples, but this works with any pattern.
           | 
           | The benefits over an exception is as the caller i can pattern
           | match on the result of this. so i could have this is in a
           | function in a case                   result = case
           | with_example(filename) do             {:ok, result} -> result
           | {:error, :some_reason} -> # this is recoverable, do something
           | else                  #log the issue
           | recover!(filename) # bangs in functions indicate they can
           | fail and raise an error             {:error, :another_reason}
           | -> # this is unrecoverable                   # log the issue
           | raise "unrecoverable error"             _ -> # any other case
           | we don't know about                   raise "unexpected
           | issue"
           | 
           | or i might not care and want it to crash if it doesn't match
           | 
           | {:ok, parsed} = with_example(filename) # will raise a match
           | error if {:error, reason is returned
        
             | [deleted]
        
           | ahoka wrote:
           | Unless you are writing oldschool Java, then exceptions are
           | not typechecked.
        
           | sodapopcan wrote:
           | Exceptions in Elixir are reserved for situations that are
           | truly exceptional.
           | 
           | If IO might fail as a business concern, then we use File.read
           | and pattern match on the return type where we can explicitly
           | handle the error case.
           | 
           | Otherwise, if we know a file will always be there and IO is
           | failing for reasons out of our control, that is truly
           | exceptional so we can use File.read! which will throw an
           | exception on failure. In Elixir we generally don't handle
           | this, we just crash and a supervisor brings the process back
           | up.
        
           | el_oni wrote:
           | The file handle is the PID of the process that opened the
           | file, it monitors the process that asked for the file to be
           | opened. and if that process goes down the file will be
           | closed.
           | 
           | You can also add an else in the with so it looks like
           | with {:ok, fh} <- File.open(filename),              {:ok,
           | contents} <- IO.read(fh)         do              contents
           | else              {:error, reason} -> File.close(fh)
           | {:error, reason} # this will return after the file has been
           | closed         end
           | 
           | or maybe you would prefer to open the file, and pass that
           | into the with and either way close the file. I just used IO
           | as an example because they return nice {:ok, x} or {:error,
           | reason} tuples, but this works with any pattern.
        
             | dahfizz wrote:
             | > The benefits over an exception is as the caller i can
             | pattern match on the result of this. so i could have this
             | is in a function in a case
             | 
             | Your example looks just like exceptions to me, just with
             | different keywords                   try:
             | return with_example(fname)         except RecoverableError:
             | recover(fname)         except Exception as e:
             | raise e
        
               | el_oni wrote:
               | In the examples yes, because it's just a simple binary ok
               | or error case
               | 
               | but if you returned a list you can pattern match on an
               | empty list, a single element list or a list that's
               | longer. which you wouldn't do with exceptions in another
               | language. that's the nice part about `with` that you can
               | stop once you stop matching the pattern and return
               | whatever you currently have, which in the list example
               | may be a perfectly valid thing to return.
               | with [value] <- Module.some_list_function(arg), # can
               | return an empty list too              [head | tail] =
               | list <- Module.another_func(value), # can return a single
               | element list              longer_list <-
               | Module.takes_multi_element_list(list)         do
               | longer_list         end
               | 
               | There are loads of other examples for uses of this, or
               | you can just write in an additional clause for your
               | function that handles the other case and pipe it down the
               | line.
               | 
               | It's about what makes sense for your domain.
               | 
               | I may not be explaining this well so apologies for any
               | confusion
        
           | bedobi wrote:
           | Exception based error handling is so bad and unsafe that
           | adopting functional error handling with Either, Try etc as
           | implemented by functional addon libraries for many languages,
           | while not yet common, in time it will become the new default
           | even in OO languages. (just like it's been the default in
           | functional languages for decades)
           | 
           | Functional error handling types are much simpler, safer and
           | more powerful.
           | 
           | Simpler because they don't rely on dedicated syntax- they're
           | just regular objects no different to any other object.
           | 
           | Safer because unlike exceptions, they force callers to handle
           | all potential outcomes, but no more. (no risk of ignoring
           | errors and no risk of catching a higher level of error than
           | desired, ubiquitous bugs in exception based error handling)
           | 
           | Powerful because they support map, flatmap, applicative etc,
           | making it easy to eg chain multiple computations together in
           | desired ways, which is unwieldy and bug prone when using
           | exceptions.
           | 
           | > What is wrong about dedicated syntax
           | 
           | It adds complexity to the language! It could be that, when
           | learning Java, Kotlin and any other language, we learn that
           | methods return what they say they do... and that's that. No
           | weird dedicated syntax and magic, special treatment for
           | returning anything other than the happy path, and the HUGE
           | complexity that comes with it, eg the dedicated syntax itself
           | and how it behaves, differences between checked and unchecked
           | exceptions, hierarchies of exceptions etc etc.
           | 
           | > Exceptions are easier
           | 
           | But that's the point, they're not.
           | 
           | Exceptions based error handling is unnecessary, hugely
           | complex, doesn't compose at all, obfuscates or straight up
           | hides what can go wrong with any given call, so leads to
           | countless trivially preventable bugs... I could go on. And
           | after decades of use, there's still no consensus about what
           | exceptions should be or how they should be used. Exceptions
           | are a failed experiment and I have no doubt that in ten
           | years, Java, Kotlin and many other languages will acknowledge
           | as much and move away from it the same way Joda Time
           | outcompeted and replaced the horrible Java date and time
           | library.
        
             | jaen wrote:
             | Option and Result types, as implemented today in mainstream
             | languages (ie. mostly anemically), are not the answer to
             | exceptions being a mess.
             | 
             | Exceptions have a lot of additional functionality in larger
             | ecosystems such as:
             | 
             | - Backtraces ie. showing the exact path of the error from
             | its source to whereever it was handled, in a zero-cost way.
             | This is by far the most important aspect of exceptions, as
             | it enables automatically analysing and aggregating them in
             | large systems, to eg. attribute blame from changes in error
             | metrics to individual commits.
             | 
             | - Nested exceptions ie. converting from one error system to
             | another without losing information. Extensible with
             | arbitrary metadata.
             | 
             | - An open and extensible error type hierarchy. Again,
             | necessary in large scale systems to differentiate between
             | eg. the cause (caller fault, callee fault aka HTTP 400/500
             | divide), retryable or permanently fatal, loggable etc.
             | exceptions while also maintaining API/ABI backward/forward
             | compatibility.
             | 
             | (for some of these, eg. Rust has crates for a Result-y
             | equivalent, but a community consensus does not exist,
             | yet...)
             | 
             | General-purpose exceptions simply are complicated, and any
             | system trying to "re-invent" them will eventually run into
             | the same problems. Over-simplifying error handling just
             | results in less maintainable, debuggable and reliable
             | systems.
        
               | kagakuninja wrote:
               | This isn't a binary choice. In Scala, you can use
               | Throwable or Exception as your error type with Either:
               | Either[Throwable, Option[Foobar]]
               | 
               | The type Try[T] is essentially Either[Throwable, T]
               | 
               | Either[Throwable, T], Try, as well as IO from Cats Effect
               | give you the stack traces that you expect from
               | conventional Java style, with the superior option of
               | programming in the monadic / "railway" style. Try also
               | interfaces nicely with Java libraries: val result:
               | Try[Foobar] = Try(javaFunction).
        
               | bedobi wrote:
               | Don't agree with a single thing, especially not with the
               | characterization that functional error handling is some
               | kind of attempt at reinventing exceptions. But yeah, it's
               | clear my and your camp will never agree lol. Fortunately
               | for you, so far, your camp has mostly won, at least in
               | the "object oriented" languages. But I think that's
               | rapidly changing.
        
               | jaen wrote:
               | I am not in any sort of "camp", in fact I prefer using a
               | mostly functional style. The above comment was based on
               | experience working in large (~100M LoC) code bases.
               | 
               | As the comment clearly indicates, it is about
               | anemic/"naive" functional error handling not being the
               | counterpoint to general-purpose exceptions, not
               | functional error handling vs. exceptions in general.
               | 
               | I do mostly prefer error handling being explicitly marked
               | at every call site (ie. the functional style), but note
               | that this is not always meaningfully possible in very
               | large systems (at least beyond the notion of "I do not
               | know exactly what errors are possible here, just
               | propagate whatever happens" which is equivalent to
               | regular exception handling)
               | 
               | And, as I already mentioned in the original, Rust does
               | have functional solutions to some of these problems, and
               | as other comments indicate, eg. Scala has them as well
               | (probably even theoretically better since it can be a
               | strict superset of the existing zero-cost exception model
               | in the JVM).
        
               | x-shadowban wrote:
               | The backtrace argument is good, but I wonder how valuable
               | traces would be in a world that never experienced reads-
               | of-nothing (npe, reading from undefined, reading out of
               | bounds array, etc). Presumably this would be because of
               | 100% use of ADTs, or maybe some other mechanism; but,
               | even Haskell throws exceptions out of `IO a` so such a
               | world might never be realized.
        
             | hot_gril wrote:
             | I'm happy with exceptions in high-level code, where like
             | every line is possibly erroring out cause of a bad RPC, DB
             | query, or even just bad integer math (div by 0 etc). If I'm
             | writing a web backend or something, I don't _want_ to
             | manually handle every single error case. I just want to
             | send back HTTP 4xx (or whatever equivalent in other
             | protocols) when I can catch the problem and 5xx when I don
             | 't expect it. Probably most people in this situation agree.
             | 
             | This is one of my beefs with Golang. The `err != nil` stuff
             | gets exhausting when it's every other line. I get it if
             | you're writing lower-level systems like you might in Rust
             | or C, but Golang is often used for high-level stuff. Java,
             | JS, etc did the right thing for their target use case. JS
             | actually went more _towards_ exceptions when they added
             | async syntax, and if anything I think Golang is going to be
             | the one to cave eventually (like they did with generics).
             | 
             | And my day job used to use Erlang/OTP. It was a neat
             | language with lots of great design decisions, but overall
             | tedious for the type of feature work we were doing. It
             | makes a lot more sense for things like ejabberd.
        
               | bedobi wrote:
               | > I don't want to manually handle every single error case
               | 
               | I get this all the time from people who are used to
               | exceptions, and it's based on a lack of understanding. If
               | you get an Either<Error, User> from the db, you don't
               | need to handle the errors in every method call in the
               | service layer. You can just call map and that function
               | will be applied in the happy case, no need to handle the
               | Error case. Then, at the resource layer, you can simply
               | userService             .getUser(userId)
               | .fold({ error -> HttpResponse.400(error) },
               | { user -> HttpResponse.200(user) })
               | 
               | As for Go for HTTP REST APIs, agreed that it simply isn't
               | a good tool for that. In fact, I don't think Go is a good
               | tool for anything - for pretty much every use case, there
               | are objectively superior languages, lol. But that's a
               | whole nother can of worms.
        
               | hot_gril wrote:
               | In a language without exceptions, anywhere you call
               | something that can return an error, you have to
               | explicitly do something with it, even if you're just
               | returning it early (which you usually are). That's what I
               | meant by handling. Otherwise you're using exceptions,
               | where early-returns are implied unless you catch. In your
               | example, probably getUser is having to handle errors too,
               | albeit with Erlang's rather slick syntax.
               | 
               | I've been on both sides of this, preferring explicit
               | handling when I was writing systems code and preferring
               | exceptions when I was writing web backends, so I
               | understand how to use each. Exceptions are popular in
               | high-level languages because that's actually what people
               | want in those use cases.
               | 
               | I also dislike Golang for plenty of other reasons.
               | "Master of none" language like you implied, and late to
               | the party with nothing special to bring other than
               | pointless syntax differences.
        
               | [deleted]
        
               | bedobi wrote:
               | > In a language without exceptions, anywhere you call
               | something that can return an error, you have to
               | explicitly do something with it, even if you're just
               | returning it early (which you usually are)
               | 
               | No? You just have to indicate it may exist, which is
               | desirable. When you get user by user id from the db,
               | there may be no such user, or there could be a db
               | connection issue. The function should indicate that and
               | compiler should check that you're handling it, not
               | pretend nothing can ever go wrong. (which is effectively
               | the case when you're hiding the unhappy path with
               | exceptions) And there is no such thing as "returning
               | early". In functional programming there is usually a
               | single return statement with a chain IO or Eithers or
               | whatever chained together and they decide themselves
               | where they short circuit, not one return statement for if
               | there's an error and another for a happy path.
               | 
               | In my example, Either<Error, User> might be what's
               | returned natively by a functional db library that doesn't
               | throw exceptions
               | 
               | but in languages without exceptions, like Haskell, it'd
               | look like this
               | 
               | queryItemByID :: IConnection conn => Int -> conn -> IO
               | [[SqlValue]] queryItemByID itemID conn = do stmt <-
               | prepare conn query execute stmt [toSql itemID] rows <-
               | fetchAllRows stmt return rows where query = unlines $ [
               | "SELECT fieldName, value FROM itemDataValues, itemData,
               | fields WHERE", "itemDataValues.valueID =
               | itemData.valueID", "and itemData.itemID = ?", "and
               | fields.fieldID = itemData.fieldID" ]
               | 
               | queryItemByID :: IConnection conn => Int -> conn -> IO
               | [[SqlValue]]
               | 
               | > queryItemByID 5569 conn >>= mapM_ print [SqlByteString
               | "title",SqlByteString "From Stochastic Calculus to
               | Mathematical Finance-Kabanov.pdf"]
               | 
               | No exceptions anywhere, and you don't have to do anything
               | with any exceptions. You could just send this IO back up
               | to the http layer of your app and turn it into a 400 or
               | whatever on any error if you don't want to disambiguate
               | between them or handle them.
        
               | hot_gril wrote:
               | > You just have to indicate it may exist, which is
               | desirable. When you get user by user id from the db,
               | there may be no such user, or there could be a db
               | connection issue. The function should indicate that and
               | compiler should check that you're handling it, not
               | pretend nothing can ever go wrong. (which is effectively
               | the case when you're hiding the unhappy path with
               | exceptions)
               | 
               | You don't pretend nothing can go wrong. Somewhere up the
               | call stack, you're handling the exception. If nowhere
               | else, whatever webserver lib will send 500 if you didn't
               | catch something.
               | 
               | To give a concrete example with my daily work, we use C++
               | without exceptions for high-level code for some reason.
               | Because practically every func we write returns an error,
               | the result looks like this:
               | RETURN_IF_ERROR(DoThis());       ASSIGN_OR_RETURN(const
               | foo, DoThat());       RETURN_IF_ERROR(DoThis());
               | RETURN_IF_ERROR(DoThis());
               | 
               | in every func down the entire call stack. And it was
               | worse before we finally overrode the people opposed to
               | macros:                 absl::Status status = DoThis();
               | if (!status.ok()) {         return status;       }
               | absl::StatusOr<Val> val = DoThat();       if (!val.ok())
               | {         return val.status();       }       ...
               | 
               | What's the value in putting this macro on every single
               | line, to remind me that everything can fail? I know it
               | can fail. This is like Bart Simpson with the chalkboard.
               | Functional programming languages could have exceptions
               | too if they wanted. It's just syntactical sugar around
               | errors.
        
               | bedobi wrote:
               | I mean if your opinion is "somewhere up the call stack,
               | you're handling that exception" and "if you don't, the
               | framework will 500" means you're not pretending nothing
               | can go wrong, then we will never agree, because I
               | absolutely would characterize both of those as pretending
               | nothing can go wrong, at the very least in context at the
               | function call site.
               | 
               | As for your code example, again, that is not analogous to
               | the functional style. The functional style is you have an
               | Either object no different to any other object. You do
               | not if on the object, you do not early return the object
               | in the sad case etc etc. Just throw all notion of all
               | that out the window. You just pass it around without
               | doing anything, or, if you only wanna do something in the
               | happy case, you send in what to do using map, and then
               | keep passing it around. There is zero requirement to
               | handle the error like you seem to imply. Only at the
               | outermost layer do you handle the error, and only if you
               | wish to do so. (if you don't, just fold it into a 400 or
               | 500 or whatever)
        
               | hot_gril wrote:
               | > I absolutely would characterize both of those as
               | pretending nothing can go wrong, at the very least in
               | context at the function call site
               | 
               | Agreed that within the function call site I'm pretending
               | nothing can go wrong, but not within the entire program.
               | I don't see what's unsafe about that. We're just now
               | implying that anything can return an error instead of
               | restating that fact every time, and instead you have to
               | be explicit about catching it.
               | 
               | About the functional programming... you're saying there's
               | no if-else or early return in functional programming, but
               | Erlang, a functional programming language, has if-else
               | and exceptions ("throwables") that look like Java:
               | https://learnyousomeerlang.com/errors-and-exceptions.
               | Under the hood it's one logical chain like you said, but
               | we're using a lot of syntactic sugar on top.
               | 
               | > if you only wanna do something in the happy case, you
               | send in what to do using map, and then keep passing it
               | around
               | 
               | That's the kind of if-else early-return I mean, and it's
               | probably what you want to do if you aren't using
               | exceptions. My C++ code also (explicitly) passes the
               | error around, albeit not in the functional way. In like
               | 99% of the cases you are just passing the error up.
        
             | 1-more wrote:
             | > Exceptions are easier
             | 
             | I'd add that they're easier because I don't know of any
             | compiler that makes you handle every type of exception. I
             | know a grip of compilers (and to be fair compilers with
             | options and language extensions set) where you cannot not
             | handle the "that didn't work, what now?" cases: the Left of
             | the Either Left Right, Nothing of Maybe Value, Error of
             | Result Error Value, None of Option etc etc etc.
             | 
             | Languages that force you to handle everything that can
             | happen when you do a thing are an accessibility tool for me
             | and my fellow smooth brains. If I can't see it in my buffer
             | I'm not going to remember to check it.
        
             | seanmcdirmid wrote:
             | Exceptions give you a nice stack trace, and you can
             | configure your debugger to trap when the exceptional case
             | occurs, rather than having to reverse engineer what
             | happened when you see the functional error value. This can
             | all be fixed, but right now for all implementations of
             | function error handing via maybe and such) are very painful
             | to debug.
        
         | ljm wrote:
         | This is influenced by Haskell (where) and lisp (let) AFAIK.
         | 
         | You're setting the preconditions for the function, and with
         | elixir you get an extra 'else' block to help.
        
         | kazinator wrote:
         | > _returns early with the thing that didn 't match_
         | 
         | What if the things are all of a different type? This is
         | dynamically typed? You have to look at the type of the thing to
         | find out where it went wrong, and guess which failing
         | expression it came from?
         | 
         | It's like halfway to reinventing exception handling.
         | with pat1 expr1, pat2, expr2 ....        do          happy case
         | // all matched       catch   // various unhappy patterns
         | matched against mismatching expr          pat1 do ... end
         | pat2 do ... end          ...       end
         | 
         | Just call it "else" or something instead of "catch" and then it
         | doesn't look like exception handling.
        
           | el_oni wrote:
           | Well, this isn't the only way to do things. There are
           | exceptions in elixir and you can catch them if you want. But
           | most functions that can fail have two versions
           | 
           | File.read() returns {:OK, contents} or {:error, reason}
           | 
           | So you can pattern match on the result.
           | 
           | File.read!() returns contents or raises an exception.
           | 
           | The first one allows you to use errors as values and handle
           | the problem at the source. The latter assumes its going to be
           | successful and either makes you catch the exception or, more
           | likely for elixir, let the process crash.
           | 
           | The with statement is a good fit for certain domains where
           | otherwise you might have a bunch of nested cases.
           | 
           | If you are worried about it returning a different type you
           | can wrap it in the else block                   with {:ok,
           | bar} <- func(foo)         do             bar         else
           | value -> {:failed, value}
           | 
           | end
           | 
           | So you can make your failed cases more homogeneous to pattern
           | match on. You can even pattern match on the different failed
           | cases if you want to ensure more homogeneity.
           | 
           | If using with doesn't make sense for the domain there are
           | other constructs in the language that will
        
       | beders wrote:
       | Using this style is infectious like any other monad, so all your
       | business will look like this. Stop reading if you are ok with
       | this.
       | 
       | That said, let's talk about validation errors vs. exceptions.
       | 
       | Exceptions are - as the name implies - unexpected errors that
       | unwind the stack to whoever catches them. Along the way any
       | necessary cleanup is handled automatically.
       | 
       | Exceptions are for cases where the error occurring is _outside_
       | the scope of the business function. I /O errors come to mind, OOM
       | etc.
       | 
       | Now if you want to VALIDATE your data, you can do this without
       | resorting to exception handlers or monads: You turn the outcome
       | to your functional pipeline into data. Have validation errors?
       | collect them in a set. Look at them after the pipeline completes.
       | Chances are you don't want to fail on the first one.
       | 
       | Need to do side-effects in a pipeline? Don't. Instead describe
       | the effect with data, run them later.
       | 
       | Need to back out early out of the pipeline? Split the pipeline at
       | that location, handle the result, stuff it into another pipeline.
       | 
       | Or use (shudder) a chain of interceptors that can decide if the
       | pipeline should be continued along its happy path. If your
       | language supports it, use pattern matching to make that decision.
       | Note that the decision making to skip parts of the pipeline or
       | the rest of it, is done outside your business function (which
       | improves the chances of them being reusable because they will
       | only be concerned with a pure data transformation and don't need
       | to know about machinations like Maybe/Either or some such to
       | signal things to the caller).
        
       | sillysaurusx wrote:
       | Hi HN! I know this is an inappropriate comment but I hope you
       | have a good day today. Just feeling grateful to the universe.
       | Hoping that anyone feeling down will catch a break, and everyone
       | feeling excited will enjoy your weekend. Much love to all of you.
       | (And if you ever need someone to vent to about random stuff, DMs
       | are always open -- please remember to prioritize yourselves above
       | other life considerations once in awhile. It's not selfish.)
       | 
       | Anyway, back to your regularly scheduled programming. Radio
       | announcer voice: _you're listening to smooth jazz^W^Whacker
       | news..._
        
         | [deleted]
        
           | [deleted]
        
         | zogrodea wrote:
         | Thank you! Hope you have a good day too!
        
         | samsquire wrote:
         | This is the kind of uplifted positivity of light and goodness
         | that I like to see on the web.
         | 
         | I think society, television, films and books focus on the
         | opposite of utopia and negative things and dark things. But
         | they ignore all the blessings and positive things. LOVE,
         | gratitude and kindness and light, and faith are what matter and
         | what we should be focused on.
         | 
         | Why would you embrace something that is darkness when you could
         | embrace goodness and light?
         | 
         | Your attention should be on good things 100% of the time.
         | Reacting to a bad situation or something negative, in a good,
         | positive way. Not a negative way.
        
         | xyzal wrote:
         | Somehow, this comment lifted my spirits. Thank you!
        
           | revskill wrote:
           | Sorry it doesn't help the poor souls who's debugging
           | useEffect issues.
        
         | adhvaryu wrote:
         | Very gloomy weather in the United Kingdom today. Thanks for
         | this!
        
       | dang wrote:
       | Related:
       | 
       |  _Railway Oriented Programming_ -
       | https://news.ycombinator.com/item?id=31404643 - May 2022 (1
       | comment)
       | 
       |  _Railway-Oriented Programming (2015)_ -
       | https://news.ycombinator.com/item?id=17337155 - June 2018 (160
       | comments)
       | 
       |  _Railway oriented programming_ -
       | https://news.ycombinator.com/item?id=11955917 - June 2016 (57
       | comments)
       | 
       |  _Railway Oriented Programming_ -
       | https://news.ycombinator.com/item?id=9166943 - March 2015 (6
       | comments)
       | 
       |  _Railway-oriented programming_ -
       | https://news.ycombinator.com/item?id=7887134 - June 2014 (52
       | comments)
       | 
       | Also:
       | 
       |  _What is railway oriented programming? (2020)_ -
       | https://news.ycombinator.com/item?id=34245639 - Jan 2023 (17
       | comments)
       | 
       |  _Railway Oriented Programming in Elixir (2015)_ -
       | https://news.ycombinator.com/item?id=11958578 - June 2016 (1
       | comment)
        
       | ctenb wrote:
       | A few years ago, I took some time to do a write-up about this
       | programming style, specifically for C#:
       | https://chtenb.dev/?page=rop-cs-1
        
         | dzonga wrote:
         | really helpful posts. however, I wanted to save them as pdf's
         | but your website's layout is broken. when you select print - it
         | only wants to show wants on the screen not everythign on the
         | page.
        
           | ctenb wrote:
           | Thanks, I haven't considered printing as a usecase indeed, I
           | could make that work at some point with some css tweaks I
           | reckon. But until then, the raw html can be found here: https
           | ://github.com/chtenb/chtenb.github.io/tree/master/docs/...
           | They can probably be converted to pdf just fine, or you could
           | just use the html
        
       ___________________________________________________________________
       (page generated 2023-08-18 23:02 UTC)