Error handling, idiomatic crystal and abstractions

In a recent thread there was some brief, tangential discussion about using a few common FP abstractions within crystal.

I completely agree with @asterite in that just because language X does something, it does not mean that crystal should support it, or an implemention should be made. The language features and semantics available natively in crystal are different to other languages - hence approaches to problems should be based on these features and restraints, not those of another language.

Nilable types (and checks for this) are a perfect example. What’s provided by crystal’s type system and internal operators mostly removes the need for an Option/Maybe type (unless you really need to differentiate between the concept of nothing and nil). This does not however negate the usefulness of other monadic types. Specifically Either.

As a toy project I popped together an either implementation the other day (P.S. @alex-lairan I wish I searched for your lib first). This lets you neatly model some operation that may fail. You can then continue to chain operations which apply as long as you stay on the good path, or just short-circuit otherwise. What’s important is each of these operations may themselves result in an error and the types will continue to represent this. When you are done, calling #value, #value! or #value? provides access to the result so that you don’t leak internal abstractions.

In the interests of making the best us of crystal’s internal features, what are some approaches here that others are using where you need to model errors, but may (for various reasons) want to do this at the type level rather than throwing exceptions?

I really love the monad approach of error handling, mostly because if looks like a rock on the road for me.

When you have one if, it’s ok, but when you have multiples if, this becomes mind breaker and the algorithm starts to look difficult when in fact it’s an easy one.

For example, imagine I want to search for data in the database then look at the data later, I could just do

m_user = Monads::Task.new(-> { User.find!(id) }) # Find may raise exception if no user
sleep 2 # do something that takes time
m_user.to_maybe.fmap do |user|
  # here I can work with my user
end

The monadic approach remove me from all the fiber management and wrap the data to something easy to handle.

If I wanted to do without the monad way, I would have a Channel to the spawn that will return to me the value, and another to return me that an error occurred.

Lucky project uses the Go way to verify if an error occured.


The operation itself has the information is valid or not. If so, the value can be used.

Using futures or some other concurrency synchronization is yet another concept and different from resolving nilable values.
Looking at the part of the code that handles that, I find it much more straightforward to do user.try do |user| instead of m_user.to_maybe.fmap do |user|.
I don’t understand in your example, what happens when User.find! raises. Is the exception supposed to bubble up into the main fiber?

What do you mean by that? In Go errors are usually passed as return value to the calling scope and handled there. I can’t find anything remotely similar to that in the link you provided.

I think Either(A, B) in Crystal is just a union type A | B. That said, if A is an exception then the std doesn’t provide a way to nicely deal with it, that is, to map over B if it’s not A.

That said, the way we represent errors in crystal are exceptions. Then you write regular code dealing with B, and you deal with A using rescue. But that Either abstraction can be useful (though I don’t like that name and the convention that values map over B and that A is usually an error, in Haskell. I prefer Rust’s Result type which is more explicit).

3 Likes

In Go, you usually return code and the value, and it’s this code that is verified.

Here

    SearchData.new(params).submit do |operation, results|
      # `valid?` is defined on `operation` for you!
      if operation.valid?
        html SearchResults::IndexPage, users: results
      else
        html Searches::NewPage, search_data: operation
      end
    end

remind me this way.

It might look remotely similar, but operation is not an error-handling feature but seems to be a regular domain model.