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/
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:
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.
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”.
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.”
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.
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.
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.
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?”.