Hacker News new | past | comments | ask | show | jobs | submit login
Railway oriented programming (fsharpforfunandprofit.com)
148 points by douche on June 22, 2016 | hide | past | favorite | 57 comments



I really think that's an useful tutorial for all languages with exceptions ( that's an exceptional event ) and normal control flow ( application error, validation, retry ).

Like that code become really simple, but more expressive than returning an int error code each function. A function can return an error with messages or message+data or message+retrycount etc.

With F# (can be done in other languages obv) it's really easy to have different success/failure return types ( amazing type inference ) without writing too much code or having issues with maintenance ( the compiler does his job for us ).

Like that, it's easy to create simple pure function, easier to test and reason about.


>for all languages with exceptions ( that's an exceptional event )

Unless, like me, you expect the unexpected.

  // if we're still here, guess it worked!

  return retval;
  }


FYI, this "railway" approach is basically the way Promises work.


The gyrations needed for error handling in functional programming are painful. As Rust libraries become more functional, Rust is getting more of those problems.

The problems created by not having exceptions are tougher than the problems created by having exceptions.


I personally find this "railway" approach (which is essentially a glorified Either) much less painful than dealing with exceptions.

Did you really find the example imperative error handling code more elegant than the railway-style function composition?


Compared to exceptions, the Either approach to error handling has some big problems:

1) Good luck annotating all potential cases of divide by zero or IO error, and all callers of those.

2) Everyone must pay the runtime cost of wrapping and unwrapping on every operation, even if nothing fails.

3) The type of a function that can fail in ways A and B isn't the same as the type of a function that can fail in ways B and A.

4) Good luck getting a stack trace.

I think unchecked exceptions are better for most applications, where errors are rare and have many different types.


4) Good luck getting a stack trace.

That's a great point. At the end of the day, Either and (checked) exceptions are equivalent from a semantic point of view, but having exceptions being an explicit part of the language means much better support for tooling and debugging.


Nothing prevents the system to add a stack trace on the Error branch of an Either. It's just that some evaluation strategies [lazy] that are usually associated with functionally programming [Haskell] provide unusual reduction traces. An eagerly evaluated expression would add the usual stack traces.


It is not just stack traces. If an exception is not handled, the debugger will break at the frame that trowing the exception (at leat on sane implementations that do two phase unwind), while with either (and error codes) all state useful for debugging will have been destroyed.


1) This is kind of invariant failures (logic errors that should never happens) and handled by panics in the same way as C++ exceptions. 2) In most cases Result<T, E>/Optional is returned from a heavyweight operation (I/O, dictionary lookup, etc) compared to an easily predicted error checking branch. 3) Probably this is problems of a type system or design, not an error handling strategy. 4) Good luck of getting a stack trace of re-thrown (propogated) exception in C++.

Exceptions are not too bad, but they are very often misused.


> Exceptions are not too bad, but they are very often misused.

This! The worst is when people start using them to control the flow of business logic. I hate that.


What actually is so bad about doing this?

I'm a data scientist, not the software engineer, so forgive me if this is something you all learned in CS 101.


You know how GOTO is real bad? Well, using exceptions for flow control* is basically a thinly disguised GOTO.

Also, constructing an exception object, with the full stacktrace etc., is a pretty heavy operation and can cause big performance issues. But it's mostly the first thing.

* Which is an important distinction. Flow control exception are recognisable because you intentionally throw an exception and expect it to be caught by a particular catcher to process it in a particular way - much like a GOTO, and with all its fragility. In the proper usage of exceptions, when you throw an exception you don't care which line of code catches it and when you catch an exception you don't care which line of code threw it.


Believe it or not, I was just working on a script this weekend that was becoming a nightmare to debug. And when I realized that I had been lazily using exceptions as control flow, this really hit home.


Just wait until the same people get hold of monads to implement their business logic.


IO errors, division by zero and other truly exceptional situations are well suited for exceptions.

Validation cases like the examples on the article are not exceptional and the failure cases must be handled properly. In this case it makes sense to use an Either -like solution where the compiler will warn if there are unhandled cases.

Both styles of error handling are useful in some cases and both have their issues.


3) and 4) are essentially subclasses of 1) and to solve 1) one needs a typesystem like in Koka or PureScript where code is polymorphic regarding effects so introducing new one without breaking existing code is possible. PureScript uses that to precisely model JS exceptions.

As for 2) the exceptions are not zero-costs even with modern implementations and profile-driven optimizing compiler makes the runtime cost of wrapping/unwrapping low.


C++ exception can be "zero cost" in the non exceptional path (for example http://llvm.org/docs/ExceptionHandling.html#itanium-abi-zero...)


Presence of exceptions still prevents some optimizations that the compiler can do otherwise like omitting frame pointer etc.


Did you even read the article? For the points 1 and 4 F# has exceptions that are handled seamlessly with this technique using a simple function in the chain, as clearly showed in the article. For point 2 you are just using a different operator from the normal composition one. I don't understand your point 3.


I've read the article and don't completely understand your comment.

For point 1, if you want to use the railway approach instead of exceptions, you must annotate all functions in the chain.

For point 2, I was talking about the runtime cost.

For point 3, Result<Result<Foo,A>,B> isn't the same type as Result<Result<Foo,B>,A>. People in the Haskell world acknowledge that it's a big problem ("monads don't compose"), hence all the research on monad transformers and extensible effects etc.

For point 4, if your function is the lowest in the call stack and decides to report an error using the railway approach, you don't get a stack trace.


For 1 and 4, is not for runtime exceptions (div by 0, out of memory, etc) that you need to bubble up to the top, is for "logic"/checked exceptions where you want to use code to deal with the failure case. For 3, that totally defeats the purpose, the whole point is to have a single error type (it can capture the details of the error anyway) then you can have

  Foo -> Result<Foo1,Error>
  Foo1 -> Result<Foo2,Error>
  Foo2 -> Result<Foo3,Error>
And use bind to compose them easily.

I think the confusing part is the word "exception" as it mixes concepts.


Erlang style "fail noisily and let the supervisor handle it" is quite a solid way to deal with "exception handling bloat" in any paradigm, functional or otherwise... The hard part is finding a way to create effective layers of supervision.


An exception is a noisy failure, and the supervisor is whatever try/catch that it bubles up to. Still, Erlang style has it's own complexities. It is in essence a network of micro services, and creating that overlay network and manuvering in it isn't simple.


Yep.. with python assert statements + supervisorctl has been a very good setup for scaling a low latency, clean chat filtering app.


F# has exceptions. It's a tool that has it use at times, but they are in essence a goto


Every single control flow tool you've ever used is in essence a goto. Every conditional, every loop, every function call. Many modern function calls are in some respects _worse_ than gotos ever were, you don't even know which code you're going to jump to.

It's not goto's that were so painful, it was unstructured gotos. Calling exceptions gotos is not a reasonable criticism. They have some very well-defined structure, and additional benefits like separating your happy-path code from your error-handling code, making both simpler.


It's important that there are two very different approaches to control flow manipulation. They differ in when the target of a jump is bound.

- Static binding of location to jump to. This is what goto, callcc, while, for etc. do.

- Dynamic binding of location to jump to. This is what exceptions do.

You can show that they are really different, in the sense that (in the absence of powerful language features), they cannot simulate each other, see [1]. Once you realise that exceptions and gotos are quite different thinking about error handling becomes more clearer.

[1] J. G. Riecke, H. Thielecke, Typed Exceptions and Continuations Cannot Macro-Express Each Other.


I accidently hit downvote on your post, and I cannot undo it. I hope some kind soul will upvote you to counter. Your post has solid information for the topic at hand.


That's a really cool result, thanks for pointing it out!


The comparison to goto was not meant as criticism, but because both disrupt the control flow in a way that has no structure. Conditionals, loops, and calls do have structure, they are the flow.

When a language support contructions such that normal control flow event are either "success + result", or "failure + error", it's much easier to follow, for me, and experience has taught me that it is far less error prone. Exceptions have a use for things that are unexpected, but happened anyway, such as OutOfMemoryException.


In essence exceptions are an implicit variant (discriminated union) extension of the return type of a function.

So a function returning T can actually return T or one of the set of Exception types.


No, I think there's an essential difference here.

The return type of a function is always handled by its direct caller and only by its direct caller.

Exceptions are fundamentally different because they aren't necessarily handled by the caller, but by the nearest appropriate exception handler, wherever it is, regardless of scope.

That is both their strength when they are used appropriately, and their weakness when they are not.


You're right in that the implicit bit extends all the way up the call stack. They aren't the same in practice, just in theory.

That implicit parallel 'track' is useful in doing the Foo => Result<Foo, Error> for you in the form of Result<Foo, Exception> but bad in not warning of unmatched values and hiding the fact. Like many a tool it can be abused by those who know just enough to use it but not enough to know why.

This is why Java originally forced you to make thrown exceptions explicit. This railway approach is one good way of dealing with error conditions explicitly.


Exceptions as discriminated unions in C# http://tomasp.net/blog/2015/csharp-pattern-matching/



Haskell has exceptions and monads. Either can be used for error handling; monads are, admittedly, unusual, but the error-handing type monads (as opposed to side-effect-handling type monads) are easy to understand in terms of wrapping values (or the potential for a lack of value) such that the type system can be used to both enforce a certain level of care and enable a certain insensitivity to errors.

For example, the Maybe monad is the simplest way to handle a function which might not return a usable value: A value wrapped in a Maybe monad has a type of either (Just a) (with a being any type at all) or Nothing. Here's a trivial example, straight from the ghci REPL:

    > let mdiv x y = if y == 0 then Nothing else (Just (x / y))
    > let foo x = (Just (x + 5))
    > let bar y = (Just (y / 2))
    > mdiv 5 6 >>= foo >== bar
    Just 2.9166666666666665
    > mdiv 5 0 >>= foo >>= bar
    Nothing
See? Complete insensitivity to errors until the last possible moment. Much less noisy than the C equivalent would be, although in this specific example I suppose the Inf semantics of the IEEE floating point standard would be equivalent.


Pardon the unwise bit of word play http://www.calcentral.com/~monadrailway/Monad/Welcome.html

To be combined with Amtrak logo obviously.

ps: Dan Pinoni had an implementation running on ascii http://blog.sigfpe.com/2006/08/you-could-have-invented-monad...


Ironically, it was accidentally trying to reinvent this pattern in Elm that simultaneously made me give up on Elm and finally understand what good monads are.

This is a really cool tutorial, I look forward to sinking my teeth into it.


There's one thing I don't understand in the following signature provided for bind :

  val bind : ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>
The author says that it takes one parameter, a function and returns another function, but then why are there 3 args in the signature ?

I would have expected something like this :

  val bind : ('a -> Result<'b,'c>) -> (Result<'a,'c> -> Result<'b,'c>)
for something that takes a function and returns another function adapted to the input, or something like this :

  val bind : ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>
for a function that takes a function G and an input I and applies G to I if possible ad returns the eventual result (curried, Haskell-style).

What am I missing about F# ?


I don't know much about F#, but in a language with currying, such distinction does not exist, given f(x, y) == f(x)(y)


My puzzlement came from the fact that :

  f(in -> out)(in') -> out 
  ==
  f((in -> out), in') -> out 
and

  f(in -> out) -> (in' -> out)
are similar. But thinking some more about it, I don't see a good reason for which this would be a problem. This simply means that any function defined to take [] elements is also able to take [:x] and return a function taking [x:]...

I hadn't thought about this particular thing until now, but this kind of halfway-through computation is interesting.


You are on the right track (no pun intended). Your second and third function signatures are actually the same thing in F# (and any other language with curried functions by default). Similar functions such as "map" and "filter" can be thought of the same way.

A big benefit of currying by default is that all functions can be treated as one parameter functions -- a very powerful tool for function composition.

Also, providing only some of the parameters (e.g. the first) and leaving some to be provided later is the technique known as "partial application" -- another key tool in functional programming.

If you think these things look interesting, you should for sure look into F#/OCaml/Haskell/Elm/etc.


Every programming i do or language i use where there are linked lists i already have to think of trains :)


How is a binary "good vs. bad" result going to work? Don't you think it is necessary to have many many "bad" conditions--i.e. errortypes?

You're just going to have to hide that away and that just drives this paradigm moot.


(2013)


It's a monad tutorial that's too embarrassed to admit it.


No it's not. This is a tutorial on error handling that happens to be monadic. The emphasis is on handling errors, not talking about monads.


It can be more than one thing at a time.


Emphasis mine:

> It's a monad tutorial that's too embarrassed to admit it.

This is not a correct interpretation of the blog post. This is not a monad tutorial, this is a tutorial on error handling that happens to be monadic.


> This is not a monad tutorial, this is a tutorial on error handling that happens to be monadic.

And is, for that very reason, a great tutorial on monads, even if it's not a monad tutorial.


Highly relevant:

https://fsharpforfunandprofit.com/about/#banned

Seriously! As I make clear on the home page, this site is not targeted at mathematicians or Haskell programmers. It is targeted at the vast numbers of C#, VB and Python programmers who are coming to functional programming for the first time. F# is a fantastically accessible language for the average enterprise programmer, but mathematical jargon puts a lot of people off ("a monad is a monoid in the category of endofunctors, what's the problem?"), and I think it is much better to explain F# with concepts from within its native environment, rather than using terminology that originated elsewhere and is often not applicable. For example, treating F# computation expressions like Haskell monads can often make things more confusing. Doing this also helps to bypass the "how can you get anything done in language X? It doesn't even have y." debate and focus on what F# can do well.


Yes. On the opposite end of the spectrum is any Reddit answer or blog post by Edward Kmett but I for some reason I love reading his answers even if I have no idea what he is talking about! However his audience would be advanced Haskellers or researchers.


> It's a monad tutorial...

this would be closer to Haskell arrows than monads no ?


This is one of the tutorials that made monads click for me (the other was the actual explanation at Learn You a Haskell). It's the Either monad, plain and simple.


And here I was thinking it's about a practice of returning & accepting a tuple of ( primary-result xor dead-already-cuz ) and morphing the (primary) result type as the "f * g * h of x" pipeline cascades.


This is a tutorial on how to get a nose bleed.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: