Rust has a very nice semantics for Option<T>
and Result<T,E>
These two have similar uses but have a clear distinction about expectations. Option is used when the returned value can be Some<T>
or None
whereas Result<T,E>
is used when the returned value can be an Ok(T)
or an Err(E)
. In Erlang and Elixir we usually see a return value of {:ok, result}
or an {:error, error}
, this makes sense when there are 2 possible outcomes success and a failure. However, in some cases we expect value to be present or absent, in those cases a tuple containing {:some, result}
or a an atom :none
seems more expressive. However there is also the possiblity of using result
and nil
if the result is not present. Using {:some, result}
/ :none
will make the receiver handle both the scenarios and seems like a good way to write code. However, I haven’t seen a lot of elixir code which does this or a variation of this. I’d love to hear how you guys write these type of functions.
I agree, but I don’t know that this needs to be an either/or kind of decision. What about:
{:ok, result}
:none
{:error, error}
The with/do/else pattern I feel encourages more readable response atoms. I wound up with this recently:
with(
client_ip = get_client_ip(request_ip, params),
{:ok, location} <- Geoip.lookup_ip(client_ip),
true <- should_search?(location, parse_exclusion_zone(params)),
query = build_search_query(%SearchQuery{}, location, params),
{:ok, results} <- NHSearch.search(query),
Analytics.track(results, client_ip, "api", "search_results")
) do
json conn, results
else
{:error, :location_not_found} ->
Logger.error("Unable to find the location for: #{get_client_ip(request_ip, params)}")
# Need to make this an error message of some sort to explain what's going on.
json conn, []
:user_in_exclusion_zone ->
Logger.info("User is too close to the exclusion box")
# Need to make this an error message of some sort to explain what's going on.
json conn, []
other ->
Logger.error(inspect(other))
json conn, []
end
should_search could have just returned false, but with the with() pattern, I instead returned :user_in_exclusion_zone to make the code less brittle and easier to read.
The Erlang ecosystem (and thus much of Elixir’s) uses:
{:ok, result}
/ :error
– or –
{:ok, result}
/ {:error, reason}
– or –
:ok
/ {:error, reason}
– or –
:ok
/ :error
Depending on what information needs to be returned. Consequently I’m a fan of the Exceptional Elixir Package as it wraps all of the above, and exceptions, and other things all into a single interface that is wonderfully easy to pipe around. ^.^
I’ve also seen code where people use [result]
if there is a result, and []
if there is none.
Lists are of course a more general way of Rust’s Option<T>
or Haskell’s Maybe
type, as besides no result and one result, they might also contain two, three, etc. results.
This is exactly what I’ve done in a library I’m working on. It certainly works well and expresses all the scenarios, but :none
isn’t very idiomatic, is it?
What about this alternative?
{:ok, {result}}
{:ok, {}}
{:error, error}
I also question whether nesting result
in a tuple this way or passing around {}
are idiomatic.
The way to indicate a successful status for something that isn’t expected to return an actual result would normally be simply :ok
. If one or more results are expected, but can be empty, then {:ok, []}
/ {:ok, [some, results]}
.
If it’s something like initializing a connection or whatnot, I’d expect {:ok, the_thing}
or {:error, :because_reasons}
.
What if exactly zero or one results are expected and errors are possible? Using a result list works, but doesn’t convey there can be no more than one result.
Maybe using a list even when you don’t anticipate ever needing to return multiple results is wise because it:
- encourages callers to handle the possibility and
- gives you the option to return more results in the future
Hmm… well, not sure actually - do you have an example? I’ve mostly never been in that situation
Unless you mean things like eg. looking up a key in a KV store; then I’d expect one or more of a couple of different interfaces to be available:
- give me a value, or give me death! (eg; if key is found, return value… else, raise exception)
- give me a value, or an error (eg;
{:error, :not_found}
) - give me a value, or some default (that you specify, but a reasonable preselected one is
nil
)
I’d use the option that suits my particular situation.
A lookup function to a KV store was exactly what I was about to suggest.
Having the caller provide a default with a fallback to nil
is pretty good, but not perfect. If the KV store can have any arbitrary term as a value you don’t know in advance whether nil
or any other value is guaranteed to not be returned as a positive result. If I call get(kv_store, k)
and get nil
does that mean the KV explicitly maps k
to nil
or there is no key k
yet? To know for sure I’d have to call get(kv_store, k, :another_default)
and check whether I still get nil
or :another_default
.
For that reason, I think it’s best to wrap the positive result somehow. A list is one option, but again it doesn’t convey that there can’t be multiple values associated with a single key.
You wouldn’t get nil
in that case - unless it was the value of that key But I’d only ever use the call w/ a default value if I needed a value, and wanted it to be eg. 0, or something else, if the key wasn’t registered.
If I just needed to act differently in case of a missing key, I’d simply use the part of the API that returns {:ok, value}
/ {:error, :not_found}
(or… {:ok, value}
/ :error
like Map
does).
Thinking about it, for this type of situation I’d call the API broken if it didn’t at least have those two options (default value that I can specify for missing keys or ok / error responses).
Ah, you beat me. Here’s the edit I posted around the same time:
Reading your post again, I realize the not found case was classified as an error using {:error, :not_found}. That’s also a good solution, but it’s a fairly common case for a key to not have a value. So far I’ve preferred saving :error and {:error, reason} for truly problematic situations.
Hehe, yep… saw that afterwards
But to clarify… the way I see it, a proper API would ideally implement all three of these options, letting me choose which one to use depending on what I need.
As for reserving :error
for more serious problems… that’s why you have a reason along with it If you start venturing outside of these conventions, you do risk making life just a little bit harder for those who are experienced with the existing Elixir standard library, or Erlang for that matter.
Of course new ways of doing things are worth exploring, but I think you’d need to prove some fairly substantial benefit for the ecosystem at large to follow suite.
This whole time I’ve been thinking about Map.get/3
, but when you mentioned Map
returning :error
I took a closer look and noticed fetch
. Providing a variety of options makes a lot of sense.
Completely agreed. I’ve dabbled with Elixir/Erlang on and off over the years, but I’m still learning what an idiomatic API looks like. I enjoy exploring options, but ultimately I want to learn what’s idiomatic and why. Thanks for your help.
I’m happy we’ve got a forum where we can hash these things out. Thanks @minhajuddin for asking about this–I was wondering the same thing. And thanks to everyone else for chiming in :). This is a great community.
Or you can use something like Exceptional that I linked above, which can normalize all of those representations into a singular, easily-pipeable version.
Yep… Have yet to try it myself, but that’s more a client concern though, if I understand it correctly?
Eyup, it lets you handle errors from any api whatever they may do. It also encourages you to return an API of the form of result
| %YourException{}
, do note that is returning your result straight if successful or returning, not raising an exception on failure, which is a bit of an oddity in the Elixir world, but Elixir’s exception structures contain everything needed to know about errors so it is a wonderfully useful way to return an error information, and by returning it and not throwing it then you do not incur the large performance cost from raising exceptions.
Seems like it might be a bit awkward to match on if you’re not using Exceptional… Especially if structs could be returned during normal operations too
In the Elixir built-in Exception(s?) module it has helpers for testing if something is an exception.
Looking at lot of the messages in this thread, it’s clear that depending on the types of data being returned, there isn’t really a single right answer for return formats. It depends on the type of data you’re returning, and your problem domain. If I am searching for a product on amazon and nothing matches, then {:ok, []} makes sense. If the search failed because the database was down, then {:error, :database_down} would make more sense.
A lot of functions will also return their value, which implicitly means :ok. This makes your code easier to pipe around.
I’m playing with Exceptional. I do like the idea of using exception structs as return values, because structs aren’t positional the way tuples are. Definitely that feels like a better answer. However, I haven’t found a case where I feel it improves on the with/do/else syntax. I tried to recreate my earlier example with it, and it came out the same, aside from my ‘else’ block matching maps instead of tuples. So far, I’m just not sure it’s worth breaking with idiomatic elixir for that.
@OvermindDL1 - I’m not a classically trained functional programmer. Am I missing something?
I think the important thing is that irrespective of exactly which alternative is chosen that it is easy to pattern match on. So when you do want to test the alternatives you can just use case
case some_function(1) do
{:ok,val} -> do_yes(val)
{:error,err} -> do_no(err)
end
This is possible in most cases but what is also very practical that you can just match against the success case else have the system generate an exception if no match:
{:ok,val} = some_function(1)
This case is often forgotten which can complicate its usage.