ImNotAVirus

ImNotAVirus

std_result - A way to standardize function returns

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:

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

To reduce the amount of function splits and these micro-functions, I use a helper function called validate, which transforms a boolean into :ok | {:error, reason}:

def validate(true, _reason), do: :ok
def validate(false, reason), do: {:error, reason}

And now you can write

with :ok <- validate(User.exists?(updated_user.id), :not_found), ...

It doesn’t solve all the problems you mention, but it can often help avoiding micro-functions and keep the logic in a single place, for the price of a small function whose semantics are easy to grasp. I’ve introduced it to multiple teams and programmers with different experience levels, and it worked quite well, especially in combination with Repo.transact mentioned in another thread.

dimitarvp

dimitarvp

I think you all are being too harsh on OP here, he spotted a problem and offered his take on it. Let the guy live, he doesn’t deserve the death sentence you are giving him. :smiley:

@ImNotAVirus I do understand the motivation for your library but I’d be personally against using it in its current form because to me it introduces ambiguity and conditions in pipes, and overall just increases code size without offering clarity of intent in return. Furthermore, the problem statement is to me minor; I never found it problematic to have my guard up and properly catch singular :ok / :error atoms. :person_shrugging: It’s part of the job, plus we have Dialyzer, plus we have “Go to definition” and “Go back” in our IDEs, so… I don’t know. The problem does not seem major to me.

IMO if you want to push the library ahead then you should aim for ultra mega hyper terseness; not just the to_result renaming that you said you’ll adopt, but also you should have only 2-3 functions in the library that do practically everything that your current separate functions are doing right now.

My points:

  • You want to call functions that are not guaranteed to return proper {:ok, value} / {:error, reason} tuples? Use a singular library function e.g. ok_err (or to_result) that absolutely always will return the proper tuple regardless of what is fed to it.

  • You want to have custom validation / error reporting functionality? Well, IMO that is another library. Your post (and likely the library as well) seems to be mixing concerns.

  • The Elixir community usually resists usage of such libraries because (a) most teams feel that doing what the library does is trivial enough and (b) nobody wants to adopt new idioms without a very clear value proposition.

cmo

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.

sodapopcan

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/2 returns nil so 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/1 returns an ok/error tuple because something can go wrong at the database level which usually happens because the user made an error. Finally Ecto.Repo.get!/2 raises 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

ImNotAVirus

Don’t worry, I can take criticism. I’m well aware that this library is a very personal point of view. Honestly, I didn’t expect anyone to use it when I published it. I just wanted to share it because I wish I’d had it years ago, so I’m thinking it might be useful to someone one day.

The personal point is, before discovering with which was recent and not necessarily widespread when I started Elixir, I had come across this article when it first came out which talked about Railway Oriented Programming.

I was immediately hooked on the design and then had a hard time switching to with a few months later when I learned of its existence. At the time, and still very often today, I found its syntax rather complicated to read, even more so when the pattern match types are different and even more when you have to process them in the else.

In my opinion, one of the disadvantages of ROP is that where with is verbose in the function that makes the pattern matching, ROP deports this verbosity to the functions called.

I’ve been doing Elixir for several years now, and it’s almost the only language I’ve touched in a long time. I’ve hardly ever done Rust in my life.

So how did this library come about?
I was developing a feature for work where I needed to make a kind of validation pipeline for some data. I first wrote it with a with as usual, but I didn’t like the result. Then I rethought about ROP (I hadn’t done one for several years now). So I rewrote the functionality and asked my colleagues whether they preferred the version with a with or the ROP version.
One of my colleagues gave me a very interesting thought when he told me that what I wanted to do was similar to what Rust and its Results offered. So I took a look at the API and, as with the ROP, I was immediately hooked. So I rewrote the API as Elixir.

So I’m like a lot of you guys. I’ve been doing Elixir for several years now, I’ve hardly ever done Rust but yet this API suits me just fine. This is the design choice I’ve made, and from my point of view, I find it more appropriate than with in some cases. But I don’t expect you to do the same.

This article explains my point of view quite well too: Chris Bailey · Elixir's `with` statement and Railway Oriented Programming

Where Next?

Popular in Libraries Top

zoltanszogyenyi
Hey everyone :wave: Excited to join this forum - I am one of the founders and current project maintainers of a popular and open-source U...
New
OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 13487 106
New
arkgil
Hi all! I’m happy to announce that Telemetry v0.3.0 is out! This release marks the conversion from Elixir to Erlang so that all the libr...
New
woutdp
Hi! I wanted to introduce my latest project LiveSvelte. It allows you to render Svelte inside LiveView with end-to-end reactivity. It’s ...
New
mindok
What is ContEx? A pure Elixir server-side data plotting/charting library outputting SVG. It has nice barcharts in particular and works g...
New
mcrumm
If you would like to migrate away from node/npm/webpack while still using sass, the dart_sass package provides a installer and runner for...
New
New
danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
asiniy
Hey there! I wrote a download elixir package which does exactly what its name about - an easy way to download files. I saw solutions ...
New
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New

Sub Categories:

We're in Beta

About us Mission Statement