Cool; seems like a reasonable workflow. I figured people must be handling these situations somehow with existing tools. But for my taste, I think the ok_then flow is a little cleaner:
user_id
|> User.get_user() # Result.maybe(User.t(), any()) = {:ok, User.t()} | :none | {:error, any()}
|> Result.map(&User.modify_user(&1, user_anything)) # Result.maybe(User.t(), any())
|> Result.error_consume(&Logger.error/1) # If :error, log and replace with :none
|> Result.unwrap_or(&User.default/1) # Can provide value or function for lazy evaluation
Well, for example: I built this library largely out of necessity in refactoring an existing codebase. It’s definitely easier to retrofit ok_then into existing code that returns :ok/:error, because for the happy codepath the data format doesn’t actually change. If I introduce an additional wrapper for :some/:none, then all the code that touches the refactored functions would need to be adjusted to handle the new data format (unwrapping {:some, value}), which makes it tougher to make incremental changes.
The good news, though, is that it’s really easy to get ok_then to work with :some/:none, because all the generic tools are readily available (and documented):
@spec some_unwrap!(Result.result_input()) :: any()
def some_unwrap!(result) do
Result.tagged_unwrap!(result, :some)
end
{:some, "hello"} |> some_unwrap!() # "hello"
:none |> some_unwrap!() # **(ArgumentError) Value is not tagged some: :none.
@spec default_some(input, (() -> out) | out) :: input | {:some, out}
when input: Result.result_input(), out: any()
def default_some(result, func_or_value) do
Result.default_as(result, :some, func_or_value)
end
{:some, "hello"} |> Result.default_some("default") # {:some, "hello"}
:none |> Result.default_some("default") # {:some, "default"}
There’s a Result.ok_or(t, e) type defined that excludes :none, if you want to use that in typespecs, and you can build your own functions pretty easily to simplify the double wrapping and unwrapping, if you like:
I think that conflict highlights the utility of other languages having split apart Option/Maybe from Result. The former is for avoiding nil problems the latter for consistent error handling. So maybe there needs to be a separate module for the Option/Maybe portion with some conversion functions to allow turning them into Results and vice versa.
That could be interesting, yes. There certainly would need to be more buy-in for a pattern like this, but I think we could provide support with an additional Option module. The significant advantage I wanted to introduce with ok_then was making it hard to produce {:ok, nil}, and a solid mechanism for falling back to default values. For this purpose, I do think {:ok, value} | :none works just as well as {:some, value} | :none.
The existing Result module also has the benefit that if code later begins returning {:error, reason} values too, the downstream code won’t break, since Result.unwrap_or_else/2 will return the “else” for any tag that isn’t :ok - you might just want to add Result.error_consume/2 somewhere to log the error. In Rust, the type would change from Option<T> to Result<Option<T>>, and the compiler would provide help in adjusting downstream code. We don’t get that in Elixir.
But yeah, for people who want to reserve Result types for :ok/:error, and want a separate type for :some/:none, it should be quite easy to add additional support. The Result module itself probably could be used as-is, since in theory it’ll only be used to wrap Options, which should never be nil. I’m imagining something like this?
And then I guess we’d want methods such as Option.default, Option.or_else etc…, pretty much identical to existing Result functions, but with :some as the happy tag instead of :ok.
I just came across a library function that returns a very non-standard list() | {:error, integer(), any()}, but was pleasantly surprised to realise that ok_then can already handle this situation quite nicely:
Version 1.1.0 has been released, with the following notable additions:
Result.tap(result, func_or_value) and related functions
This function behaves like Kernel.tap/2, but calls the function only if the result was tagged as expected:
The function is not called if the tag doesn’t match.
Naturally, error_tap/2 and tagged_tap/3 are available too.
Result.default!(result, value) and family
If you know a default value should never be nil, a bang-variant of the “default” functions is now available that will raise if the value is nil, instead of returning :none:
iex> :none |> Result.default!("hello")
{:ok, "hello"}
iex> :none |> Result.default!(nil)
** (ArgumentError) Value is nil.
Result.filter/2 now accepts 0-arity functions and truthy values
Previously this function accepted only a 1-arity function, but there are situations where the value is irrelevant, or relies on an external value, and this is now possible: