giddie
Ok_then - The Swiss Army Knife for tagged tuple pipelines
GitHub: GitHub - flexibility-org/ok_then: The Swiss Army Knife for tagged tuple pipelines
HexDocs: OK then... — ok_then v0.1.0
I just couldn’t quite find what I was looking for when it came to handling tagged tuples. In particular, I really wanted to find an elegant solution to the problem of missing return values that are not errors. Because sometimes, a missing return value is an error, and other times it’s not. Rust handles this very elegantly, using separate Result (Ok / Error) and Option (Some / None) types, often in tandem.
So I decided I had to write my own solution for handling tagged tuples. And I’m quite pleased with the result. Of course I have to give credit to those who have gone before, such as the authors of the “OK” and “Croma” packages.
The most controversial feature will be the addition of :none alongside :ok and :error, though this becomes mostly transparent in pipelines.
There are a few more functions I’d like to add, in particular to the Result.Enum module. And I’d also like a monadic “with”-like macro, similar to that offered by the “OK” package.
Most Liked
dimitarvp
What does a new convention of :ok / :none achieve that hasn’t been achieved by the :ok / :error convention?
If a function does not return data then it can just return :ok (not a tuple).
When you say semantics and consistency, I am not seeing it. It’s OK to have a personal preference but I don’t see the value.
As for nil, well, I think we all know that’s a huge and very expensive mistake in computer science in general. But as others pointed out, nil is also an atom so replacing it with another one doesn’t help much. Not sure I am seeing the argument for the stack traces as well.
Maybe I just don’t get it though.
dorgan
There are some functions that return none, like erlang:dist_ctrl_get_data, erlang:system_info, but it seems in those cases it means “set to none” or “not available” rather than “not found”
giddie
Yes, absolutely. The ok_then library will also accept any bare atom (:some, :success, :ok, …). It interprets these internally as {:ok, {}}, {:some, {}}, ..., so mapping functions will receive {} as the wrapped value, and that’s what unwrap!() would return too. It’s just that :none is returned by Result.from/1 and Result.map/2 if they receive nil as a value to wrap, and there are shorthand functions to handle :none, such as default/2 and none_then/2.
So if you want, you could fall back to a bare :ok quite easily like this:
Result.from(nil) # :none
|> Result.default({}) # :ok
# or
Result.from(nil) # :none
|> Result.none_then(:ok) # :ok
As another Rust guy I can see where you’re coming from. But my view is that beyond avoiding
nilat all costs, the rest is kind of achievable with fairly vanilla Elixir. But I get the need: you want chainable / pipeable value, which is fair.
I’d be interested in some testcases, actually. Could I tempt you to come up with some examples of vanilla Elixir that we could compare to an equivalent using the ok_then library?
stevensonmt
This addresses a real desire for me having gotten used to Option/Result types with Rust and Elm. The discussion in this thread has shown how much thought you put into the API design. Thanks for putting this out!
giddie
I think that conflict highlights the utility of other languages having split apart Option/Maybe from Result. The former is for avoiding
nilproblems 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?
"hello"
|> Option.from() # {:some, "hello"}
|> Result.from() # {:ok, {:some, "hello"}}
|> Result.unwrap!() # {:some, "hello"}
|> Option.unwrap_or_else("default") # "hello"
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.







