6 Comments
User's avatar
Alexander Zubkov's avatar

I found "do notation" from dry-monads the best. It uses exceptions under the hood to control flow, every method may accept any amount of arguments and terminate the chain on failure, returned values may be passed to next methods which you need. We had a custom monad implementation on a previous project, but there were many problems which dry-monads solves, otherwise you will have to write that yourself or deal with a partial implementation. So we preferred to add a dependency over our code.

Julik's avatar

If your procedure does not run in a very tight loop, Exceptions for flow control in Ruby are fine - because they replace the Either. If you rescue exceptions according to their class (using matching of some description) you get a very good reproduction of an Either, much better than the approach with tuples of `[:ok, :result] and `[:error, :message]`. One of the unpleasant aspects of those tuples is that nothing prevents you from having a `[:ok, :error_message]` or a `[:error, :result]` creep in (say hello to the Go error handling). So I would do it with exceptions and a reduce - if your pipeline needs to be composable. If it isn't, maybe a straight-ahead `input = do_thing(input)` per line would be even simpler.

```ruby

calls = [

-> (input) { do_thing(input) },

-> (input) { do_another_thing(input) },

-> (input) { do_yet_another_thing(input) },

]

result = calls.inject("hello!") do |input_from_previous, callable|

callable.(input_from_previous)

end

```

Julik's avatar

If your procedure does not run in a very tight loop, Exceptions for flow control in Ruby are fine - because they replace the Either. If you rescue exceptions according to their class (using matching of some description) you get a very good reproduction of an Either, much better than the approach with tuples of `[:ok, :result] and `[:error, :message]`. One of the unpleasant aspects of those tuples is that nothing prevents you from having a `[:ok, :error_message]` or a `[:error, :result]` creep in (say hello to the Go error handling). So I would do it with exceptions and a reduce:

```ruby

calls = [

-> (input) { do_thing(input) },

-> (input) { do_another_thing(input) },

-> (input) { do_yet_another_thing(input) },

]

result = calls.inject("hello!") do |input_from_previous, callable|

callable.(input_from_previous)

end

```

Pragdave's avatar

That's a perfectly valid idea. I have always been nervous when using exceptions for control flow, if for no other reason tan their nonlocal nature makes it harder to reason about the code. I agree that in this case, if they are kept local to this one module, and always trigger an end of processing, they'd be OK.

As for the [ ok, :error_message ] issue, there's always Ruby's pattern matching...

Julik's avatar

There is, but I haven't really taken on to Erlang-style destructuring. It is amazing for deconstructing a network packet or a struct of fixed offsets, but using it as a dispatch mechanism... didn't stick with me.

Pragdave's avatar

Ah, there's come a moment when you'll suddenly find yourself writing more case statements with patterns than if statements. Not only does it linearize what could be a chain of `elsif`s, but it also lets you extract values that you need. Highly recommended.