OndrejValenta

OndrejValenta

Based on our discussion here and to create some kind of guide for new players..

What are your guidelines and recommendations on method signatures and return values?

Since everyone can get creative in a dynamic language like Elixir you probably have some guidelines in your companies on how to define new methods and what should they return so you have easier code transfers from one programmer to another.

For example:

  • How many parameters is too much for a function? When do you rather create a structure to contain the incoming data?

  • Do you rather use single parameters or do you prefer receiving a map that you map and deconstruct?

  • What is your ordering of parameters? Do you put the most static parameters to front or back?

  • Do you override methods with specific mappings or rather have one method with a case inside?

  • What do you return from methods? Are there methods that are returning just plain values in your projects and when do you switch to {:ok, data…} tuples?

  • Do you return {:ok} or just plain :ok? For me {:ok} is more consistent with {:ok, data}, for others it’s not.

  • How many return values do you put in your return statements? Just one or two? For example, {:ok, data}, {:ok, data1, data2}

  • When do you create a return structure?

More questions will come from the discussion.

Showing Posts 1 to 10

sodapopcan

sodapopcan

I’m heading off for the weekend after work and since I ran my mouth pretty hard about asking questions in that other thread, I wanted to respond! There is a lot of content here, maybe a bit much for one thread (maybe not). I’d love to talk about most of it but since I don’t have much time I’m gonna zero in on the return values because that one interests me and is also related to “let it crash”.

In short, {:ok, resp} and {:error, message} specially is simply a convention used when something can go wrong. @stefanchrobot had a perfect example in this answer. As illustrated there, it’s often used where an exception would be thrown in other languages. I don’t have a lot of experience in languages where frequent exception-throwing is the norm but as I see it, this enforces explicit error handling at the source and frees up exceptions for cases that are truly “exceptional”. Without getting too much into it, this is where “let it crash” ties in. If you know how to handle something, by all means handle it! But ideally do it through some kind of well-formed return value and leave exceptions to be caught by supervisors. I could get more into this but trying to stay focus :sweat_smile:

So getting back on track, you essentially want to use the tuple convention when you need some kind of status code, and it doesn’t have to be :ok/:error, again that is just a convention. You could have a function that makes an HTTP request and could have return values like {200, "body"}, {400, "body"}, {500, "body"} etc. If your functional doesn’t need to check a status, for example String.capitalize/1, just return a bare value. It would be pointless, not to mention super annoying, if String.capitalize("hello") returned {:ok, "Hello"} since there is nothing else other than :ok to match on. A string is always going to successfully capitalize and if doesn’t, there is something seriously wrong and let it crash :slight_smile: (That is a super contrived “let it crash” example but I’m kind of rushing here).

Lastly, a simple :ok it returned when there is no other meaningful data to return in the success case. If the error case doesn’t have a message to go with it (which would be weird) you could just return :error, but generally it has a message so they are wrapped in a tuple. You could also just return a bare string in the error case if you really wanted—again, these are all just conventions. IE, there is no need the different return possibilities to be wrapped in the same data structure. For example, ExUnit.Callbacks.setup/1 can return :ok, {:ok, %{}}, or simply %{}. It pretty much comes down to {:ok} is just weird because a one-element tuple doesn’t make any sense. And in fact, it’s not as inconsistent as you might think since in Haskell (and possibly other functional languages), tuples of different lengths aren’t considered to be of the same type!

Anyway, I hope this helps a bit. I apologize that it’s a bit verbose—I would normally try and edit it down, but I’m now late for work and still have to pack for the weekend!

Edited to fix a small but significant typo: (I wrote “consistent” instead of “inconsistent”!)

OndrejValenta

OndrejValenta OP

Ok, to elaborate more on this.. if you have a multiple errors that a method can return, say file doesn’t exist, file is currently locked by another process, file is too large to process.

Would you return {:error, {:file_too_large, “file path”}} or just {:err_file_to_large, “file path”}, I would choose the former, just asking what do you prefer.

sodapopcan

sodapopcan

Good question! I’ve never run into that. I think that comes down to taste. In these cases I like to do some “wishful programming” and see what the implementation looks like

case MyFile.open(file) do
  {:ok, contents} ->
    contents

  {:error, {:file_too_large, path}} ->
    path

  {:error, {:something_else_is_wrong, path}} ->
    path
end

vs

case MyFile.open(file) do
  {:ok, contents} ->
    contents

  {:file_too_large, path} ->
    path

  {:something_else_is_wrong, path} ->
    path
end

I personally prefer the second as it’s just more concise. Since I would hope that anything other than :ok would be an error, I don’t feel adding the extra :error tag adds much value. But I really feel this is a of taste. If you do like the :error tag, {:error, :file_too_large, path} is also perfectly legit. You did ask about tuple size. For me I generally think 2-3 is good. 4 is also good but starting to push it. I pretty much avoid 5 completely and would use a map at that point. But I really stress that this is a matter of taste and what you find readable.

LostKobrakai

LostKobrakai

I really like the approach described in this, though I need to add that I never managed to work on a codebase, which consistantly did that. It’s for sure overhead, but on the other hand I really like the explicitness.

al2o3cr

al2o3cr

IMO this choice is very context-dependent and driven by usage:

  • if the error isn’t going far (used by another function in the file) then the second form is shorter
  • on the other hand, if the error is part of an API it’s easier to document and for callers to handle an {:error, any()} result than {atom(), any()}
OndrejValenta

OndrejValenta OP

Actually, this makes a lot of sense, I like it.. With this approach there is clear understanding that when an error the method returns an error it is announced with :error atom and you don’t have to think about if the first value is just an value or it’s an error.

So following code is what I will use.. It’s more code but it speaks more loudly..

OndrejValenta

OndrejValenta OP

Just a simple map and not any kind of defstruct, for example OrderProcessingResult? I’m not sure if people use defstructs or it’s just too much hassle.

kasvith

kasvith

hmm this looks fine until you forgot to implement format_error on a module

LostKobrakai

LostKobrakai

Oh god. I did just skim over the article and it mentioned the places I had read before. I’m mostly in favor of the {:error, exception} return value instead of {:error, something}. Exceptions have API to be turned into strings, they however can include structured data, which might be interesting if the caller wants to log the error. I don’t think the exact implementation shown makes too much sense.

gregvaughn

gregvaughn

I first saw that done in the exceptional library. I advocated for it in my talk at last year’s ElixirConf US, but I’ve only used it for one particular situation.

Note: I haven’t actually used the library at all, just the approach of returning {:error, exception}

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 & 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