std_result - A way to standardize function returns

lol, ya! It’s actually someone pointing out on this forum a few years ago that {:ok, response} is a monad is what made me finally understand wtf a monad is. I had a decent, most-of-the-way understanding already, but it never quite clicked. I said this recently but I really see Elixir as “elegant yet scrappy” and I love that about it. I am also a big fan of convention and really dislike codebases that are a mish-mash of styles. I have a love/hate relationship with reading code and and dealing with a frustrating bug is not when I want to be met with someone’s creative coding around a straightforward problem.

@dorgan there is actually a lot to respond to there since you touch on a few things I find myself thinking about a lot.

Warning: Lots of stream-of-consciousness word vomit ahead… please feel free to skip :slight_smile:

The easiest to address it that my only problem with with as it is how the formatter handles it. I wish it would let you just put the do on its own line! It’s literally the only thing about the formatter I really dislike.

Otherwise, the whole “extracting private functions” thing has been on my mind a lot recently. I feel that a lot of people coming to Elixir from OO, particularly Ruby, have been burned by the gross overuse of single-use private functions. I feels that many have the perfectly common human response to revolt by going too much the other direction. I try and just extract private functions when the meaning starts to get muddled. I’ve never actually thought too much about how they will get used by other functions in the module and honestly, this is because I don’t much experience working alongside other Elixir developers (only about 8 months-worth). What I’m getting at is that I really only use with with library functions that have consistent return types or each step is itself a significant local function (or in another module that I own). If I can’t do that I just don’t use with, although I don’t have any good examples of what that looks like.

Otherwise, I do think it’s great to be able to mold the language around so people can experiment with new ideas. But for me, it’s not really about big names it’s more whether or not it will ever make it into the language that I would continue to use it. It’s funny you mention the transact thread because I’ve been following along and, even though Ecto is a library itself and I do find Ecto.Multi a bit clunky at times, it’s so ubiquitous that I’d have a hard time adopting transact in my own projects. I also actually found myself needing to compose multis in a recent project which was my immediate question when first reading that thread and it got answered without my asking :sweat_smile: In any event, maybe it’s a little closed-minded of me but I like to keep dependencies lean and I’d much rather pull in libraries that are solving the tough application problems I don’t want to deal with.

One thing I am very interested in is using macros to mold the language into a DSL for your business domain. From what I understand this was one of the original ideas behind LISP that has never caught on (and I do think I understand why). To me that would be well worth coming onto a project and learning. But when teams pull in libraries just to make something “a little more like other functional languages” I find it a bit annoying. It means I’m going to get used to random little things and then probably miss them when they are gone, ha. As I said earlier, I really love the scrappy side of Elixir and that a lot of stuff is left up to convention.

2 Likes

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.

5 Likes

I have the luck of not having to deal with any of that before coming to Elixir, and I’m not against splitting things into private functions per se, I’m against being forced(or rather, encouraged) by the language or libraries to do so be it because of formatting or because that’s just the path of least resistance. The length of the function is of little concern to me, really. I’m forming this opinion after years of writing Elixir. One thing is to write a private function because it makes sense to you or it warrants being its own thing, a completely different thing is to have to do that because it is required by library code, or not doing so makes the code hard to follow and/or format badly.

You wrote what I meant, but better :slight_smile: When more than one solution to a problem exists, it’s easier to adopt something when it’s already mainstream and there’s tons of ways of rationalizing it, even if there’s probably a better solution around the corner.

Doing that for the sake of it rarely works out well in my experience. Taking inspiration from other languages to design something that fits with the language you’re working on is good though. A lot of elixir constructs were born this way.

I support them creating the library and sharing it! Most of the code I’ve seen contain some variation of what the library offers, so it’s clear to me it’s something people deal with a lot. that it’s discussed and solutions are proposed is always great

1 Like

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

4 Likes

If you imply that using bang functions that raise instead of with is more readable, I completely disagree (at least this is what I understood from that article), otherwise the other examples showed there use with too.

Exception handling as control flow is unreadable and hard to debug. Why? It is simply because they are global, even if you catch them locally. I understand the appeal of having custom exceptions defined, however we are coming once around to types, specs and having a convention.

There are places where you want your process to crash, some errors can’t be handled, that’s when you want to use bang functions. with is used when you want to short-circuit a pipe, having the option of dealing with the error. Also, since with is a expression you can use its return value, it is not uncommon to have this kind of code:

with {:ok, token} <- fetch_token(),
        :ok <- check_validity(token),
        :ok <- check_signature(token) do
   ...yaaay
else
     _ -> {:error, :invalid_token}
end

You don’t care about the specific error and return a new error that makes sense for you, this is IMO the greatest power of with expression.

1 Like

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.

7 Likes

I’m not talking about bang function nor exception here. They have their own uses and have nothing to do with with.

And we’re in complete agreement on this point :slight_smile:

1 Like

That’s what I often do currently

1 Like

This reminds me somewhat of my library ok_then: ok_then v1.1.0 — Documentation

1 Like

Are bang functions supposed to be used with all functions whose error raises an exception? I thought they were only meant as a complement to functions that passed :ok/:error tuples (the implicitly “non-bang” functions).

I came across it yesterday :sweat_smile:

I like it! The main difference between our libraries I think (apart from the API) is your handling of nil with :some / :none.

The naming convention docs explain here. Generally it’s what you say but they also give the example of alias! and var! from macros whose variants don’t return tuples (it is, after all, basically just a much better alternative to only matching on the :ok case). It also points out that there are cases where there is a bang variant with no non-bang variant. For those who are familiar with Ruby, this is differs says if you have a bang you also have a non-bang (again, just convention). Though in Ruby it’s also often used to signify the mutation version of a method and many methods only mutate and just go with the non-bang (eg, Array#delete). I always found this a bit confusing.

Getting back on track, though, in all cases in Elixir’s stdlib, ! always signify’s something’s going to raise. As much as I like to personally stick to this convention, I don’t think it’s a huge deal when people want to stray from it. I think it can add a certain expressiveness in some domains. In libraries it’s certainly very iffy but I still believe it’s up to the author. Just my two cents, of course.

1 Like

I’m surprised exceptional by Brooklyn Zelenka hasn’t been mentioned yet. It’s another, interesting approach for working with exceptions and tagged tuples.

1 Like

I’ve wanted similar and will toss out my own take on this concept with ExResult — ex_result v0.0.5 - Ultimately, I feel that this sort of concept would need to be an actual core Result type and it’s just not. Any of these utilities attempting to inject their own take on a result type sadly will be fighting years of conventions and developer understanding. Regardless, I think it’s worthwhile to find ways to standardize for your own team.

1 Like

Damn, I look at the examples and it feels like a foreign language:

[1,2,3] ~> Enum.sum()
#=> 6

Enum.OutOfBoundsError.exception("exception") ~> Enum.sum
#=> %Enum.OutOfBoundsError{message: "exception"}

[1,2,3]
|> hypothetical_returns_exception()
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
#=> %Enum.OutOfBoundsError{message: "exception"}

0..10
|> Enum.take(3)
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
#=> 6

:clown_face: What is going on here?

I find that VS Code os flaky with this - perhaps someone has a vscode settings for live_view, elixir and phoenix that they’s be willing to share.

I use the example on the blog by Pragmatic studio.