How is it recommended to return result from a boolean function which includes "error_data" in case of fault?

What’s the idiomatic and recommended way of returning data from a function which is supposed to return a boolean value, if additional info if case of error?

def validate(input) do
  
end

Is it:

  1. true | {false, reason}

  2. {true} | {false, reason}

  3. :ok | {:error, reason}

  4. {:ok} | {:error, reason}

?

Or anything else? Why?

1 Like

I’d return something like your third option.

2 Likes

It totally depends…

If it is just validating data, I’d prefer classical Boolean values true and false, if though you want to differentiate between data that is invalid (some numbers are out of bounds) and illegal (a binary instead of a struct), I’d prefer to simply not having a function head that doesn’t match for illegal data and therefore raise FunctionClauseError implicitly or alternatively I’d return still proper booleans plus an :error tuple in illegal input describing what’s wrong.

If though I got the question totally wrong, and you think more about explaining in the return value what makes the data invalid, you should take a look at how ecto Changesets handle this. Perhaps even postpone your development a bit until the ecto team managed to extract them into their own library? I’ve heard it’s on the roadmap…

Driven to the extreme I’d say Ecto’s approach suggests:

  • {:ok, ""} # valid/no validation message
  • {:error, "message"} # invalid with validation message

or alternately

  • {:ok, nil} # valid/no reason
  • {:error, reason} # invalid with reason

i.e. the return type of a validation isn’t boolean at all, it either

  • passes validation or
  • is a failure reason

See also

https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1

3 Likes

For what it is worth (3) is the most common idiom among Erlanger. IMHO unary like {true} and {:ok} are visually ugly and something I avoid.

1 Like

If it’s boolean I’d do true/{:error, reason} with reason being an exception structure that holds whatever needs to be reported. (This is the pattern I use in my projects)

Although if you use this pattern you have to be extra careful not to write code like this:

if is_valid(my_data) do
  # is valid
else
  # invalid
end

Because if the data is invalid is_valid/1 will return {:error, "Invalid data!"} which is considered true. I know this also goes for many of the other options as well, but returning true makes it more tempting to use if instead of a more explicit case statement.

Which you need to make sure of regardless, and is a great reason to always always explicitly test (wish credo had a lint for that), I.E. to implicit ‘casting’ from is_valid(my_data) but explicit true == is_valid(my_data). Should always always do that regardless as it makes it explicit not only the truthiness of a value but the explicit value as well, which is useful for documentation and type checking both. :slight_smile: