Result Monad (Discussion)

Hey,
I whipped up a Result Monad in Crystal and I’d like to hear your opinion on whether it can be done better or shouldn’t be done at all… or can be done better in Crystal in a different way.

I’ve been a Ruby Dev for a long time, but I also used Elixir, Erlang and Rust. A trend in the ruby community is atm to peak over to the functional guys and implement some of the concepts.
dry-monads is one example: https://dry-rb.org/gems/dry-monads/1.3/

A couple of days ago I wanted to improve the readability of my code and googled “pipe operator in crystal lang”, which leads to: https://github.com/crystal-lang/crystal/issues/1388. Reading through the comments it looked like the community is welcoming it but core devs are not (correct me if wrong)
Then I googled “crytal lang monads” which led to: https://crystal-lang.org/2016/09/09/a-story-of-compromises-and-types.html (Someone commented that other languages solve this via monads)

So it looks like Crystal is not even thinking about embracing some of the functional trends. (Again correct me if wrong)

Inspired by http://nywkap.com/programming/either-monads-ruby.html and https://gist.github.com/lpil/2b87eebf479c43eda7e4bea21587a7fb here is a Result Monad in Crystal:

enum Either
  Ok
  Err
end

struct Result(T)
  getter value : T
  getter type : Either

  def initialize(@value, @type); end

  def then(&block)
    raise @value if @type.err?
    yield @value
  end

  def then(ok : Proc, err : Proc)
    return ok.call(@value) if @type.ok?
    err.call(@value)
  end

  def self.ok(val : T)
    Result(T).new(val, Either::Ok)
  end

  def self.err(val : T)
    Result(T).new(val, Either::Err)
  end

  def ok? : Bool
    @type.ok?
  end

  def err? : Bool
    @type.err?
  end
end

and this is a fictional example on how it can be used:

# assuming all methods return a Result(T)
create_from_string(str).then(
  ok: ->(v : Foo){ validate_x(v) },
  err: ->(e : Exception){ control_what_happens_on_error(e) }
).then { |v|
  validate_y(v)
}.then { |v| 
  save(v)
}.then(
  ok: ->(v : Foo) { http_client_post(v) },
  err: ->(v : Exception) { maybe_even_revert_something_from_before(v) }
)
1 Like

Hi, this look great for simple example.

When you will have more monadic functions, you may have a lot of if in your code.

I would do class inheritance.

Look at https://github.com/alex-lairan/monads/ it’s what we have done with @moba1

I wonder why I haven’t found your repo googling for “crystal lang monads”. Thanks!

Hi!

I personally discourage from introducing any “monad” type as is in the standard library because it doesn’t play nice with the way the language was thought to use.

Compare this:

create_from_string(str).then(
  ok: ->(v : Foo){ validate_x(v) },
  err: ->(e : Exception){ control_what_happens_on_error(e) }
)

To this:

begin
  foo = create_from_string(str)
  validate_x(foo)
rescue e
  control_what_happens_on_error(e)
end

In my opinion there’s a lot of noise in the first snippet.

You should also benchmark Result vs. regular language constructs to see how well the behave. My guess is that using those lambdas together with closures, together with creating those Result objects will make things much slower, and also compile slower.

Finally, monads already exist, or they don’t, in Crystal. The concept of monad is tied to Haskell. It means having the >>= and >> operators, return, and the do notation to simplify the syntax. So saying “these are monads in Crystal” doesn’t feel right. We don’t have those operators, it doesn’t seem the Result type here have those, and there’s no “do notation”.

7 Likes

@asterite I’m currently working on “do notation” using inspired by https://dry-rb.org/gems/dry-monads/1.3/do-notation/

Monads aren’t just a way to remove exceptions, they helps to simplify the code.

Also, monads aren’t just a “Haskell thing”, a lot of languages (Crystal included) are adding monadic logic (hi #try)

Personnaly I don’t like Null, Nil, None, etc.
It’s not a correct way to describe nothing (for me) because it represents too many cases.

3 Likes

Monads in Haskell allow the programmer to control the order of computations in a language that is purely functional. In Haskell they are necessary. Do-notation is required to make the underlying syntax palatable. There’s a lot of good in functional programming (testable functions without side-effects, flexible function composition, even currying…) but I’m not sure monads (or functors or applicatives) are explicitly necessary to realize those benefits (or to be functional).

I’m not objecting to the occasional utility of monad-like implementations. I’m just reacting to OP’s comment that “…it looks like Crystal is not even thinking about embracing some of the functional trends.”

This looks a lot like how Promises work: https://github.com/spider-gazelle/promise

The language will definitely benefit of a way to return a meaningful, non-critical errors. For now, there are four options:

  • Nil, which doesn’t add any information
  • yield, neither
  • raising an Exception, which adds the question of possible errors a method can return, and have a performance impact.
  • Union of some sort of types (or a monad-like type), which does the job but not very standard.

It would be great to know which “normal”, non-critical errors methods can return, to avoid unhandled exceptions or catching all unconditionally (which is not a very good practice) (related issue: https://github.com/crystal-lang/crystal/issues/7154).
This will make the language safer.

If you want to return errors in a meaningfull way, you could just define a enum of errors (or even a more complex type) and return that.
Your function/method would then have the return type union of whatever | errorType .

Am I missing something? This seems much simpler and cleaner than adding extra syntax.

1 Like

This is an important distinction. There’s a lot to love about FP in general but trying to bolt FP concepts onto Crystal because of how well they work in Haskell is like putting mustard on Jello because it tastes great on a hamburger.

Does mustard taste good on Jello? I dunno, but I’ll say confidently that it won’t taste like it does on a burger.

5 Likes

Returning an Enum is fairly limited:

  • does not suppport error message
  • cannot hold a child error

That’s why I usually return an Union, SomeType | Error when needed, or I yield the Error, which is sometimes more convenient.

So it is the same as Go lang is using by returning error code and value. For Go Lang it works quite well.

With a lot of if inside the code.

The trouble with if is that it break the flow.

With monads, you look at your code like a river stream.
You bring everything to the end and you look at the result.

I disagree. I get the logic much faster from if flow than from “river” flow.
I am well fed with that “river” flow in Promises in NodeJS.
It just changed when async/await was added.

It is possible with Go to know possible errors a method can return? I don’t think so.
That’s the main advantage I see for proper error types with structures like monads, unions, personal preferences apart.

The thing is, in most cases when you get an error you just want to log it or report it, and continue with your regular workflow.

An error is something unexpected. As such, having to deal with it at every step of a computation is an unnecessary burden.

Now, returning a union type meaning “one of these many things can happen” is valid, as long as you want to consider different logic flows in your applications. But errors and exceptions are usually not expected, many times they are bugs, and you usually want to report them, continue with the app, and fix them at a later time.

1 Like

That’s probably the reason why in Go you can handle an error, but the error doesn’t have a lot of info. That’s also the reason why begin/rescue and try/catch exist: to not having to care about errors at every step of the computation.

The reason this is not like this in functional languages it’s because the only way you can do things there is by composing functions. It’s not because it’s a great way to program (in my opinion).

Same with monads and everything functional: they are workarounds to what you can do in those languages. They are based on the idea “what can we do with just functions?”.

5 Likes

Agreed. FP even has exceptions, they just adapted them for their paradigm.

1 Like