cjbottaro

cjbottaro

I’ve been primarily doing Elixir development for the past 6 years or so, and during that time whole heartedly committed to functional paradigms.

But recently I did some Go programming, and I hate to admit it, but working with early returns again was kinda nice.

I’m not a fan of lots of small little functions, and using with seems to necessitate that; a lot of the times I’ll just deal with case/if nesting.

So one day, I threw (pun intended) in the towel and refactored a plug that has many success cases as well as error cases that need to return early, to use throw/catch to emulate early returns… and I was really happy with the results.

But there is this nagging feeling that I’ll be excommunicated from the community if this code ever sees the light of day publicly. I kid, I kid… :joy:

So why are early returns bad? I’ve since wrapped up the throw/catch paradigm into a tiny library that let’s it be used like this:

v = returnable do
  if some_condition?()
    return "foo"
  end
  ...
end

I understand that early returns make mechanical “refactor into function” difficult, but I think wrapping it up in an expression like above negates the issue. Not sure.

I’ve seen some posts (on Reddit, not here) where people suggest you can use throw/catch to emulate early returns “but you need to be an expert to do it safely and properly.” Why is that? What pitfalls are they alluding to?

As always, thanks for the help and info!

P.S. A little further in that video, he describes a use block that he wishes existed, but doesn’t know any programming language that has something like. I’m nearly positive it can be accomplished with metaprogramming in Elixir… but probably a topic for a separate post.

Showing Posts 1 to 10

D4no0

D4no0

Early returns are a remnant of goto statements from languages like cobol, they encourage break of flow. A classical example would be:

def first_id_or_null(list, id) do
  Enum.map(list, fn el -> if el.id == id, do: return el end)
  return null
end

Now you can notice here that while such a function would work without problems if early returns were a thing in elixir, it is introducing a notion that we can return data from everywhere, hence breaking the concept of what a map function should be able to do, introducing a new concept that you can break out of all contexts.

This example can be as well rewritten to:

def first_id_or_null([], id), do: null
def first_id_or_null([h | t], id) do
  if(h.id == id), do: h, else: first_id_or_null(t, id)
end

What is interesting is that there are cases where you would want to return early, a good example would be Enum.reduce_while/3 and it is easily possible to implement this without using throw/catch.

cjbottaro

cjbottaro OP

I guess it’s a testament to how deep I am into functional programming these days that I never even considered returning from an enumeration, ha. That’s a really good point.

How do you feel about it in the case of replacing a with statement, or generally unravelling heavily nested cast/if/cond?

D4no0

D4no0

I think that this is easily solvable by refactoring to small functions and match parameters instead of using case/cond.

codeanpeace

codeanpeace

tl;dr in my experience, multiple function heads + guards > early returns

Coming from Ruby where I appreciated how early returns could un-nest code for readability, I remember searching for an equivalent when I began learning Elixir. My rule of thumb when using early returns in Ruby were to limit them to the beginning of a function body to avoid the need to “chase” down all the possible returns lurking within a function, which would reduce readability.

What I soon realized was that leveraging function arity aka multiple function heads and guards in Elixir accomplished much of what I wanted out of early returns in Ruby while pulling it out of the function body and into the function head – arguably improving readability. ¯\_(ツ)_/¯

cjbottaro

cjbottaro OP

That’s what I’ve been doing for the past many years and I guess why I’m struggling with this now. I prefer the early returns in some, maybe a lot of, cases now that I’ve just given in and started using them again.

I guess it comes to down to style. I find it massively difficult to trace through code that is broken up into tons of small functions. The constant jumping between files/modules/functions adds a lot of mental overhead compared to just reading a larger function top to bottom.

Side note, our Ruby code base adhered to the small methods, single responsibility object mantras and it’s one of the worst codebases to work in ever, imo. Literally everyone hates its. And it’s all because seemingly simple things (like generating an Elasticsearch query, executing it, massaging the results) are spread across dozens of files and classes.

I’m starting to feel that a tiny bit in our Elixir codebase, I think due to a lot of reasons discussed here about with: https://forum.elixirforum.com/t/with-statement-else-index/56914 and I kinda feel like early returns helps in some cases.

sodapopcan

sodapopcan

Ya, this was a big problem with a lot of Rubyists. At an old job there were literally single use private functions that had the same body as the function name just without underscores. Stuff like:

def raise_unless_complete(thing)
  raise unless complete(thing)
end

Agreed that this was a nightmare to work on.

Of course it all comes down to style, but I feel like there is a good middle ground. I’ve been writing larger functions since moving to Elixir and haven’t ever wished for early returns. Not like my code is particularly amazing or anything but I often strive for a large “primary” function that literally lays out everything (especially side effects) then have a few smaller functions for implementations. Though they are “smaller” in the Ruby sense—they could 10-20 lines each themselves depending on what they do.

The super small functions are only good in my estimation if they are very reusable everywhere and you get to know them. But single line private functions are generally the worst.

D4no0

D4no0

My general opinion on early return is that this is a optimization feature and in no way increases readability or correctness of the code, it is an unsafe feature that is used for code optimization in low-level languages. A simple example:

for (int i = 0; i < 5; i++) {
  if(i == 2) { return i; }
  printf("%d\n", i);
}

This can be easily refactored to:

bool found_number = false;
int number = -1;

for (int i = 0; i < 5 && !found_number; i++) {
  if(i == 2) { 
    number = n;
    found_number = true;
  }
  printf("%d\n", i);
}

We can notice that we have to allocate 2 variables and write more code, essentially the primitives and the programming style does not offer support to write code in a way where you would be encouraged to not use a break or return early.

I write now a lot of Kotlin and the functions that are always the most unreadable and hardest to debug are functions that return early, especially since you can early return there from anywhere, be it callbacks, loops, you name it.

D4no0

D4no0

My 5 cents on one of the potential problems on having small functions and having a hard time to navigate to them is over-engineering.

I tend to write those small functions always in the same module, usually near the function as private functions, while others tend to extract them to a separate module, thinking that they will be able to use them in the future in other places, witch they will most definetly not use and if they do they can write the same function again there. I think that this optimization of code duplication is the main reason of such problems, and this problem is even bigger if we talk about typed code, classes, people there end up with some abomination inheritance trees and generic classes that you have to look at for 10 minutes to understand what it actually does.

al2o3cr

al2o3cr

FWIW, one of the few uses of throw in the Elixir stdlib is in Keyword.keys inside a :lists.map call - specifically for stacktrace-hygiene reasons:

https://github.com/elixir-lang/elixir/pull/10214#issuecomment-661473002

Regarding OP’s code, I’m not a fan - in a function called by a function that said returnable, the return function will do a nonlocal return (!) which is unlikely to match the expectations of anybody whether they like early returns or not. IMO try/catch and throw aren’t complicated enough to be worth hiding behind a thin abstraction layer.

cjbottaro

cjbottaro OP

Our Ruby code base should be a case study on exactly this. Inheritance 5 levels deep, but then also composed of multiple other objects with similar inheritance levels. It’s absolute insanity.

I also find a lot of the times that inheritance is abused just to get rid of explicit branching logic (i.e. people don’t like using if statements for some reason).

Can you explain what you mean by this?

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

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
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews