What is the closest scheme Crystal offers which is similar to using Result / Options in Rust?

What is the closest scheme Crystal offers which is similar to using Resut / Options in Rust ?

Union types. This is an extract of a pending blog I have on the matter. The example below is about having a function that parses a string and returns an AST or an exception. That is, it doesn’t raise the exception, it returns it.


With union types it’s easy to provide information about the issues found in the input provided to our parsing function:

class ParseException < Exception
  getter issue : String
  getter line : Int32
  getter col : Int32
  
  def initialize(@issue, @line, @col)
  end
end

alias ParseResult = Ast | ParseException

def parse(input : String) : ParseResult
   if input == "hello"  # just a silly example
     Hello.new          # Hello is an Ast
   else
     ParseException.new "Should start saluting with 'hello'", 0, 0
   end
end

Now, when using parse, we must consider the exception. This is easy to do with a case statement:

case result = parse "this is an example"
in Ast
  puts "yay!"
in ParseException
  STDERR.puts "#{result.issue} at #{result.line}:#{result.col}"
end

Give me the gun, I want to shoot at my foot

Having to case at each returned object might sound like a bit too much. No need to suffer: we can mimic the same idea as with Nil and extend the Object class with a pure! method to assume an object is the pure value and not an exception:

class Object
  def pure! : self
    self
  end
end

class Exception
  def pure! : NoReturn
    raise self
  end
end

With this extension we can now assume a call to parse won’t fail (or the exception will explode in our face!):

parse("hello").pure!.to_s + " pure world"

EDIT: An Option is nothing but a nilable type, as in MyType?. Object#not_nil! plays the role of pure! above.

5 Likes

This is the thing that catch my eye when starting to know Crystal. The ability to embed so nicely such types is :star_struck:

Here it is done for exceptions, but of course you can do a similar thing for your own hierarchy.

1 Like

On the other hand, a language which uses "try / catch"constructs expects you to write the code which throws (not merely returns) exceptions.

I wouldn’t say expects. It allows raising exceptions, but doesn’t demand it.
Libraries can use either way of error reporting.

The stdlib largely raises, but there are also many methods that don’t and just return nil on an error condition. That’s a very crude error reporting as there is no actual description of the error, but in these cases it’s decently clear from the methods’ function what went wrong.

If there is a way to ignore errors, the danger is always that a piece of code will fail silently at some point in the future, isn’t it?

Not sure what you’re referring to. What do you mean with ignore errors?

Say, you want to open a file, for instance and, say, in case the operation fails the result is just an integer returned, which is supposed to be non-null in case of success and conversely.

No mechanism forces you to check for that integer, you can ignore it, if you wish, as you can in C or in Go.

Say, these lines of codes are in a library.

Now, say, another person is using that library. That person is not aware of your deliberate decision to ignore the return code. If the expected file is not found, then the app will crash without pointing clearly to the cause of the error, I suppose.

Right, you can use any tool for wrong-doing, no doubt of that. I think raising an exception or returning it are two alternatives for different cases. You can even combine them: raise when something really went wrong, and return it when you can expect it (like when parsing user input).

2 Likes

Sounds reasonable.

1 Like

Hi,

To manage the Exception a bit like Rust / Monad, I did this GitHub - Nicolab/crystal-result: Rust-like error handling for Crystal (`Ok` / `Err`)

Thank you.

It sounds interesting. I’ll have a closer look.