giddie

giddie

GitHub: GitHub - flexibility-org/ok_then: The Swiss Army Knife for tagged tuple pipelines · GitHub
HexDocs: OK then... — ok_then v1.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.

Showing Posts 1 to 10

wmnnd

wmnnd

Seems like a neat idea! You’ve already mentioned that you think adding :none might be controversial - why do you think it’s still useful?
After all, nil is also simply an atom (is_atom(nil) == true and nil == :nil) :slight_smile:

dorgan

dorgan

Erlang uses :none in it’s standard lib, so not it’s not unheard of, but since nil is already the convention in elixir, I have the same question.

The exceptional library does that, a comparison between your solution and that one would be nice :slight_smile:

ondrej-tucek

ondrej-tucek

You haven’t found Result library? And course for

there is no standardised pattern to represent optional values , other than value | nil

there is ExMaybe..

hauleth

hauleth

I always have found undefined as a default value meaning “not found”.

dorgan

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

giddie OP

There are two main reasons:

Semantics: There is no particular convention to determine the meaning of nil - when I see nil in an error message, the cause (generally a missing nil-check) could be anywhere in that stack trace. However, by wrapping nil into :none, I know that when I see :none in a stack trace, I’m forgetting to pattern-match a tagged tuple, usually right at the top of the stack trace. Technically, the difference is very small - as you say, both nil and :none are atoms - but in practice the name difference is helpful.

Another way to look at this is by typing: :none is a valid value for the type Result.maybe(any()), whereas nil is the absence of any value at all. It could have been :no_result or something similarly more explicit to mark the “type” of the tuple, but I decided it was more pragmatic to keep the tag name short, and “none” is also easier to mirror in function names than the latter.

Consistency: To make functions more generic, results can be normalized into two-element tuples: {atom(), any()}. This happens internally, but you can also see this in functions such as or_else. For atom results such as :ok, this becomes {:ok, {}}. For :none this becomes {:none, {}}. Although nil is an atom, I think it’s a little less clear: {nil, {}}. It just looks a bit too much like a mistake or oversight to me - as if the tag itself is missing or undefined, which is not the intended meaning.

giddie

giddie OP

I don’t think it addresses optional values (e.g. accidentally mapping a wrapped value to nil)? I may have missed it. I did examine that package briefly, but the functionality seems to be aimed mainly at handling exceptions in pipelines, which is also a really nice goal, but different. I felt I first wanted something focused on a really clean and consistent API for regular tagged tuples. So far I’ve managed to mostly avoid exceptions in pipelines by catching them early, near the cause, but I’d definitely try exceptional if that became impractical.

dimitarvp

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.

giddie

giddie OP

Yes; I did check out that library. Again, it doesn’t really handle optional values, and results are expected to only be tagged :ok or :error, whereas ok_then accepts any tagged tuple (though :ok, :error, and :none have more convenient functions). I also felt like the API wasn’t quite as full or consistent as I’d have liked.

As far as I can tell, this package adopts the value | nil paradigm, though it does offer some semantics that make it easier to chain multiple nil-checks together in a pipeline. It doesn’t scratch my itch, because I feel that tagged tuples are semantically clearer and also safer.

giddie

giddie OP

You mean it could return :ok | {:ok, value}? Yes, you could certainly do that. Personally I find it less clear, because it’s not immediately clear that :ok is a result that could have held a value. It could be made explicit with {:ok, {}}, for instance, and in fact ok_then interprets :ok in that way:

def example(value_r) do
  value_r
  |> Result.map(fn
    {} -> "default"
    value -> value
  end)
  |> Result.unwrap!()
end

{:ok, "hello"} |> example()   # "hello"
:ok |> example()              # "default"
:error |> example()           # **(ArgumentError)

So imagine that instead of tagged tuples, we use structs:

%Result{is_empty: false, is_ok: true, value: "hello"}
%Result{is_empty: true, is_ok: true, value: nil}
%Result{is_empty: false, is_ok: false, value: "reason"}

In every case, what is returned is a Result, which should be unwrapped before it’s used directly. The spec for a function returning this would use Result.t() (or similar). In every case, it’s a Result that is returned. Returning nil would clearly mean that you’re not returning a Result.

What I’m suggesting is that semantically, :ok | :error | :none is exactly the same. The type is Result.maybe(). Returning nil would mean that we’re not returning a Result.maybe(). This helps because if I see an error such as :none.hello/0 is undefined, then I know what I’m dealing with is a Result.maybe() pretty much exactly where the exception was raised that has not been unwrapped - it’s being treated as an unwrapped value. Unwrapping the result with unwrap_or/2 will force you to consider a default value. If I see an error such as nil.hello/0 is undefined, or - more likely - an obscure Ecto exception caused by a nil leaking into a query - I need to work up the stack to find out where I should have checked for that nil, because there’s nothing to “unwrap” here - it’s simply a value that isn’t there.

Where Next? Top

Trending in Announcing Top

restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 12366 136
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
ausimian
Castle is a package I put together to assist with building Elixir releases capable of being live-upgraded via the ‘official’ release hand...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews