Closed sporto closed 4 years ago
Gleam doesn't currently have traits so ?
would be a lot more restrictive than in Rust (no into trait).
It would be good to also investigate OCaml's rebindable syntax which is used to achieve this and more.
If you add an implicit modules system then you have your trait-like things (though even more significantly powerful if first-class functors are added), then you'd be able to have something desugar as you want.
However, ?
fits in languages like Rust fine, because early returns, however I wouldn't recommend it for something like Gleam, which doesn't. OCaml already has a construct for this.
OCaml lets you add an unbound amount of +
's or *
's at the end of a let
or and
, so you can do this:
let open Result in
let+ one = get_one(...) in
let+ two = get_two(...) in
...
And this just desugars to:
let open Result in
(let+) get_one(...) @@ fn one ->
(let+) get_two(...) @@ fn two ->
...
The let+
in this case is defined in the Result
module like:
let (let+) res monad =
match res with
| Err _ = e -> e
| Some s -> monad s
But in general the let* <match> = <expr> in <body>
gets mapped to (let*) <expr> @@ fn <match> -> <body>
This ability let's you open a module into the local space as normal, then use them like let
's. On the forums I built a whole Elixir-style for
comprehension with them and described these in more detail especially how they work in regards to the whole patterns.
In General, for consistency, the OCaml community uses +
's as meaning a monad call (where it takes something, operates on the innerds, and returns that same type), and where *
's mean an applicative call. It is considered that they may open up other operators to be used on them as well, but already the OCaml community is making great use of these with not only monad/applicative style handling for things like Result handling, but also for comprehension systems, parsers, etc... etc... It makes a lot of code a lot more readable, and it handles the ?
case just fine.
If any inlining is performed at all then the function call can get entirely optimized out (as can be done in OCaml), but function calls are very cheap on the BEAM (since it's optimized for precisely this), though still not as cheap as an inline case.
Some syntax ideas for something like OCaml's rebindable syntax:
import gleam/result.{then}
pub fn main() -> Result(Thing, Err) {
let(then) value = may_fail()
let(then) next = also_may_fail(value)
Ok(next)
}
pub fn qualified_main() -> Result(Thing, Err) {
let(result.then) value = may_fail()
let(result.then) next = also_may_fail(value)
Ok(next)
}
import gleam/result.{then}
pub fn main() -> Result(Thing, Err) {
let let+ = then
let+ value = may_fail()
let+ next = also_may_fail(value)
Ok(next)
}
pub fn qualified_main() -> Result(Thing, Err) {
let let+ = result.then
let+ value = may_fail()
let+ next = also_may_fail(value)
Ok(next)
}
What other approaches do languages take here?
Do seems comparable to OCaml's rebindable let syntax, the others seem less expressive.
Something that generalizes exceptions pretty well, in a typed setting, is algebraic effects. Monads are painful to use if they're not done in a pure setting like Haskell (and even then, transformers are still a pain); they don't compose well with list.map
or other monads (like error handling and option handling when also using futures or whatnot). let+
is just sugar on top of monads, you still get a ton of continuations and you still have the problems of composing let+
from list with result or option.
See koka (and slides), the plans for OCaml, etc. Also a good talk about some work done on the jvm that emphasizes the importance of using native control flow instead of monads.
My opinion is you don't need something that generalises, or that you can discover the generalisation in later versions.
If result is going to get special status (as the comment on this issue suggests to me, https://github.com/gleam-lang/gleam/issues/531) Then you could have something that worked just for results.
I like the rust approach, and even without traits It would be useful.
The only thing I don't like about the rust approach is ?
very much suggests boolean to me. I would have chose !
instead.
I also think a syntax like below would work,
let value != try_get_value()
To me that reads as "let value not equal to try_get_value".
If we specialise for Result I think I would prefer this syntax:
try value = try_get_value()
I quite like the idea of having a syntax specific to Result from an approachability point of view- it is much easier to explain something concrete and it is harder to misuse than a syntax where the exact behaviour can be supplied by the user.
What use cases would we be losing out on if we made it specific to Result?
I would be interested in how frequently people use ? with types other than Result in Rust. By default it is implemented for Result, Option (which are Results in Gleam) and Poll
@tomwhatmore @gamebox @QuinnWilton @chouzar @kyle-sammons Do you have any thoughts on the above?
I've let it be known before that I think a comprehension is as readable and more powerful than one off syntax like this. It can be specialized to the built-in monad-likes we have (List and Result), and can be generalized with whatever polymorphism device we develop later(traits, first class modules, etc).
I'm a big fan of ?
in Rust and would very much like such a thing to exist in Gleam. As to the specifics...
?
with anything other than the default types in Rust and still find it extremely useful.let value != try_get_value()
reads as not equal to me also..expect()
for example) but ?
doesn't seem bad to me. I would be fine with !
as well, but at the end rather than by the =
.let value = try get_value()
over there) and would work quite similarly to how you'd use it in a Swift function that is marked with throws. I quite like it, but this may be because I have written a lot of swift.This seems like an important point that didn't get any discussion:
However, ? fits in languages like Rust fine, because early returns, however I wouldn't recommend it for something like Gleam, which doesn't.
Is there a specific rationale for not having early returns in Gleam? Would supporting them have any other ramifications?
Is there a specific rationale for not having early returns in Gleam? Would supporting them have any other ramifications?
I think that early returns are a lot more useful in statement based languages than expression based languages as they don't pllay well with higher order functions. I can't say I've ever wanted them in Erlang/Elixir/Haskell etc, so I don't feel a reason to add them to Gleam at present.
Chiming a bit late here, but I agree with most things that @tomwhatmore said. I also prefer the syntax of try value = may_fail_func()
as it seems much more clear-cut and readable. That said, as a Haskell fan I'm also always a fan of the do
syntax, but that may be overkill for this specific use-case.
If we were to have a try
binding syntax would it make more sense to have to work on patterns rather than to have it be specialised to Result?
// result based version
try form = read_form(request)
// expands to
case read_form(request) {
Ok(form) -> ...
Error(e) -> Error(e)
}
// pattern based version
try Ok(form) = read_form(request)
// expands to the same, but doesn't wrap `Ok` around the pattern for the user
case read_form(request) {
Ok(form) -> ...
Error(e) -> Error(e)
}
This would allow it to be used with types other than Result, and would match assert
and let
more, though it would be slightly more verbose.
I would lean to result based version, because other matches might not make as much sense, also you need a consistent type of the failure case for the early return. The expanded code needs to return in the error case.
// result based version
try form = read_form(request)
// expands to
case read_form(request) {
Ok(form) -> ...
Error(e) -> return Error(e)
}
I guess you example might actually already include this if you assume the ...
on the Ok branch includes all the lines of code after try
in the function.
Yes, all the lines after the try statement go in place of ...
as there is no return statement in Gleam.
I'm trying out the Result specialised try
here https://github.com/gleam-lang/stdlib/commit/4787386533f909a46d9f9854af846a2d398d2586
Just like
?
in Rust, this make the code a lot easier to understand when you need to deal with multiple results / options.From
To