ImNotAVirus
Hi everyone,
Published a new library: StdResult!
StdResult is a library designed to standardize function returns.
Highly inspired by Rust’s std::result, this library provides a way of simplifying the management of :ok and :error tuples by providing functions for manipulating them.
The problem:
One problem I come across quite often is the lack of consistency between certain functions. In the same module, some functions will sometimes return :ok while others will return {:ok, result} and others just result. The same goes for errors. It can quickly become complicated to manipulate these results.
That’s where StdResult comes in.
Usage:
Here is a simple example: let’s say we need to retrieve an environment variable, convert it to an integer and check that it’s positive. Our function should return {:ok, value} or {:error, reason}.
Here’s an example of what it might look like with StdResult.
import StdResult
System.fetch_env("PORT")
# This will transform `:error` into a `:error` tuple
|> normalize_result()
# If there is an error, explicit the message
|> or_result(err("PORT env required"))
# If no error, parse the string as an integer
# We could also have used `Integer.parse/1` but for simplicity's sake we won't.
|> map(&String.to_integer/1)
# Test if the number is positive
|> and_then(&(if &1 >= 0, do: ok(&1), else: err("PORT must be a positive number, got: #{&1}")))
# The result will be either:
# - `{:ok, port}`
# - `{:error, "PORT env required"}`
# - `{:error, "PORT must be a positive number, got: <value>"}`
Check out the documentation for more details on existing functions.
Any issues, suggestions or contributions are welcome.
Cheers
Links:
- Hex.pm page: https://hex.pm/packages/std_result
- Documentation: https://hexdocs.pm/std_result
- Source code: https://github.com/ImNotAVirus/std_result
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
cmo
Another solution is to use your editor to inspect the function (hover, keyboard shortcut, etc) to see the spec. This doesn’t introduce cognitive load, you learn the standard library, you don’t pay a performance penalty and are more likely to write idiomatic code.
It is convention that functions/macros ending with a
!raise exceptions.dimitarvp
Not having the time for more thorough feedback but I’d make this shorter i.e.
to_result.Eiji
I don’t like your solution as the reader needs to understand your way of thinking which is not as obvious as you think and in practice your library does not gives developers enough flexibility.
Also
andandornaming is already used inElixirfor working with binaries (or insideEcto.Query, but saidor_whereis written based onSQL’s naming).What you can do is to introduce 2 macros:
tap_on/2andthen_on/2.Another example without early raise:
What we win with those 2 macros?
thennaming is already known, soonwith extramatchpattern as first argument does not require any extra knowledge of saidhexpackage as al it do is following common naming and patterns.then_on_ok/1macro using this oneifs inside anonymous function)handle_error(reason)function, so all error messages would be in just one place without a need to scroll all of them to see a success pipeline steps (useful on large pipelines with many different errors):erroratoms which allows early raise as aboveHere is how simple is to write a custom macros:
With such custom macros we can then rewrite our pipe:
sodapopcan
I echo some sentiments here. There is reason behind different return value types. Possibly not in some libraries and certainly within private projects, but there is certainly good reason for the choice of all return types. For example there is no point in returning a 2-tuple for the happy path if we’re not passing back any other data. ok/error tuples are used as light-weight exceptions and only when the caller can do something about it (talked about here).
Ecto.Repo.get/2returnsnilso it can be used in a scenario where the record might not exist. When you use this function, you are communicating to the reader that there are scenarios where the record won’t exist, but it’s not an “error” and doesn’t need any kind of messaging.Ecto.Repo.create/1returns an ok/error tuple because something can go wrong at the database level which usually happens because the user made an error. FinallyEcto.Repo.get!/2raises and is used when we know for a fact the data exists and if it doesn’t, there is nothing we can do about it other than Let it Crash™ or, if that doesn’t work, stay late on a Friday to fix it.ImNotAVirus
TL;DR: The aim of this library is to manipulate function returns in a more simple/idomatic/“standard” way rather than to standardize the function returns themselves.
It’s all about control-flow (combine/pipe functions with multiple returns types) rather than “all library developpers should always return
:ok/:errortuples”.First of all, thank you all for your answers.
They made me realize something: the purpose of this library is very clear in my mind, but the description I gave is not.
So I’m going to try to rephrase it.
Firstly, and this is my fault, I should never have used the term “standardize function returns”. Here I’m not promoting the fact that all functions should return a
:ok/:errortuple. Of course anempty?/1function, for example, should return a boolean, of course aget!/2function should just return the data (since very often there’s aget/2function in the same module that already returns a:ok/:errortuple).The main use of this library is to manipulate these results in a simpler/“standard” way.
I’ve talked a bit about this in the project README (but not enough I guess), but you can think of StdResult as a new way of doing control-flow using
:ok/:errortuples.Originally, my problem stems from the use of
withwhere the more operations there are to perform, the more complicated it becomes to writeelse. I know that this is a fairly recurrent problem and that many developers have already had this problem. Several posts/libraries exist on the subject and try to solve the problem. One example is this proposal: https://forum.elixirforum.com/t/with-statement-else-index/56914.StdResult is a library that provides an answer to the problem in specific cases.
Perhaps an example would be more telling. Let’s take this piece of code:
Here, if you need more details on the step that failed in the
elseblock, there are 2 solutions:Solution 1: wrap each function call with a tuple
The main problem with this solution is that very quickly the code becomes complicated to reread and too verbose.
Solution 2: wrap functions into another that returns a
:ok/:errortupleEveryone can have an opinion on this solution, but personally sometimes I don’t want to have to scroll or press on my shortcut to see/modify the error I’m going to get, or even have a dozen functions in a module where their only use is to convert a term into a
:ok/:errortuple.The solution I propose is as follows:
I hope that’s clearer. Let me know if it isn’t.
I know, but I couldn’t think of a better name for this macro. If you have, don’t hesitate to suggest
Also, the macros
ok!/1anderr!/1raise during a pattern match, so it’s not that far from convention.I like it! It will be on the next release, thanks !
What do you mean by “the reader needs to understand your way of thinking” ?
What more do you need when you say “in practice your library doesn’t give developers enough flexibility” ?
Why do you need to early raise ? Because you expect a value ?
Then simply use
StdResult.expect/2orStdResult.unwrap/1But basically, for your example, the equivalent of your code would look like this:
As for the rest of your message, in short
then_on_ok/2is anand_then/2andthen_on_error/2isor_else/2.And using functions is a choice, because I don’t like using macros when I can simply use functions.
@sodapopcan I think I’ve answered your comment in this one, but don’t hesitate if you have any questions.
I’ll update the current post and README for the next version.
dimitarvp
Naming is very hard as we all know, I don’t claim mine as better in any way.
Here’s one more idea:
Eiji
Ok, so let’s assume the reader knows only core
ElixirAPI and it’s naming…This original version requires me to see what those functions are actually doing which is against of what you wrote in quote above.
Here I have no idea what “normalization” you mean unless I read your hex package’s docs. This part without a context of the next lines could make me think that
normalize_resultreturns abooleanandor_resultis doing something like& &1 || err("PORT env required").The new version is not better:
to_resultis not inElixircore I would expect that everyto_*functions would return a custom type i.e.struct.inspect_errhave no sense for me.inspectis naming fromIO, but in anonymous function theraiseis called. So … when the result is error we inspect it and otherwise call anonymous function? Wait, that’s conflicting with the message inraise.The macros I have proposed uses existing
Elixircore naming properly. Bothtapandthenexplains what the macro is doing.onis a short ofmatch on.See first quote above and how I can see your naming as said assuming the reader knows only
Elixircore API.Oh, I have missed the
expect. Still it’s yet another function to check in documentation. Unliketapwhich is already known expect is not intuitive.Unless I read your docs I could think that it means:
Alternatively expect could mean that we expect a truthy value for
datavariable and return the passed string otherwise.As a senior developer reading your examples makes sense for me, but as said you are using the existing naming to do things your way instead which for new developers or people who did not read the docs could be unclear.
The macros are for
onthing i.e. for pattern matching.Not really, as in my case those are examples of how a developer could use my macros and in your case it’s your package functions. I wrote those examples to show that said 2 macros is all the developer needs. Their naming is proper (having in mind
Elixircore naming of course). Then if the repository maintainer decides to even shorter the code he could writethen_on_okandthen_on_errorcustom macros like in my last example in that reply message.Because I expect something (environment variable, directory structure, command arguments and so on) outside the app to exist outside the supervisor tree. The cases are:
System.argv/0or task arguments (scripts and tasks)privand so on (scripts and tasks)Ok, so …
Elixircore naming in a different wayAlso I reminded that ok hex package. If I would use a library to deal with results my preference would be
okpackage.dorgan
The above ideas reminded me of the macro I implemented a few months ago: heresy/lib/heresy.ex at main · doorgan/heresy · GitHub
That lets you write:
And later was reminded that it’s similar to the older GitHub - vic/happy_with: Avoid commas on Elixir's with special form. · GitHub
Every now and then the topic resurfaces; having a normalized value structure is valuable
sodapopcan
I think I understand a bit better, though I think it’s just your wording I find confusing. In your problem statement it reads as “every function in the same module should return the same result.” I think I misunderstood thinking this library was for ensuring such return values when it actually unifies existing ones?
I have far less problem with the latter for those who would be interested in this. I personally don’t believe this type of branching logic belongs in pipelines, though. I’m a big believer that Elixir’s very few constructs are all valuable and can themselves be used to signal what’s going on. For me,
withis the tool to use when there are a series of dependent steps. When I see a pipeline, I generally just expect raw data in, raw data out. This is huge for scannability. But of course, I don’t think there is anything inherently wrong here (now that I understand better).@dorgan Isn’t
happy_withmerely alternate but similar syntax forwithwith nothing heretic about it?dorgan
More often than not I find that general purpose macros are not always welcomed because they’re “magic” or “people need to learn them”