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

type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New
GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. https://github.com/woylie/doggo Features Unstyled Phoenix components....
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
ahamez
Hi everyone, I’ve been working on this protobuf library for 3 years. We use it in the company I work for, EasyMile, to communicate with ...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
kip
I’ll shortly be launching Text, a nascent text analysis library. Current functionality In this early version (not ready for prime time) ...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
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
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews