Comparisons with Java, what does lack of nullability mean in Elixir?

Hey guys, I was reading the Elixir in Action book, and in chapter 2 summary it’s written that:

There is no nullability. The atom nil can be used for this purpose.

So what does it mean?

We won’t run into null values in places we expect data, or our code won’t be riddled with nil checks?

Or, do we have to approach the nil values in a different way, like Rust’s Optional returns. i.e. using Tuples response when nil is supposed to be returned.

{:ok, val}
{:nil}
3 Likes

It means you will constantly forget to add guards to protect against nil and have nil related bugs throughout your code.

5 Likes

While nil is technically an atom and not a special value per se, it’s definitely used as the representation of “null”. All boolean-ish functions in the standard library consider nil and false as the only falsey values: if, unless, &&, ||, and more.

So, we do have null and we do have to have code that accounts for that. Rust’s approach with the Option type is great for a statically-typed language where you can enforce the handling of None, but it’s not feasible in Elixir. The good news is that (by convention) many Elixir and Erlang APIs do return a sort of option type: {:ok, value} | :error. The compiler cannot force you to handle both (since we don’t know types at compile time), but you’ll see a lot of Elixir code that can sort of look like Rust in that sense:

case some_function() do
  {:ok, value} -> # ...
  :error -> # ...
end

Hope this helps!

19 Likes

To add to this:

  1. Even if we didn’t have nil, we would end-up with :error or :undefined as a replacement. Perhaps even worse we could end-up with both or more options, making it hard to standardize (like || and && standardize around nil). There is a great chapter in the Joy of Clojure that talks about this.

  2. One of the biggest issues in Java is that everything is nullable in the type system by default. So you never know when you are expecting it. Rust and even other languages in the JVM make it explicit. In Elixir, nil is also an explicit type (at least in TypeSpecs/Dialyzer but also in any future Type System).

Finally, Rust forces you to disambiguate between Ok(...) | None. You cannot have None | AnyOtherType. But as a dynamic language we cannot enforce it and this leads to ambiguity in situations like opts[:key] || "default" behaves differently to Map.get(opts, :key, "default") depending if :key is absent or if the :key is set to nil. But this would happen regardless if we had nil or not and it literally happens to all dynamic languages.

24 Likes

Since we’re mentioning Java, and I couldn’t find the answer anywhere, I will ask it here: has any thought been given to adding a pattern to Elixir similar to Java’s Optional? Basically, encapsulating nil checks behind a common interface? When writing Java code in functional style, this makes it much easier to deal with all those nulls that may lurk behind every corner.

So instead of writing something like:

discounted_price_tag = item
|> Item.get_price() # could be nil
|> then(& if !is_nil(&1), do: Decimal.mult(&1, discount_rate))
|> then(& if !is_nil(&1), do: "final price is: #{&1}")
|> then(& if !is_nil(&1), do: &1, else: "no price available")

being able to write something like:

discounted_price_tag = item
|> Item.get_price() # could be nil
|> if_present(& Decimal.mult(&1, discount_rate))
|> if_present(& "final price is: #{&1}")
|> or_else("no price available")

I’m curious if the idea was never considered, or considered and then discarded for some reason.

Not everything has to be a pipeline :upside_down_face:

I think it would be more idiomatic to write:

case Item.get_price() do
  nil -> "no price available"
  price ->
    discounted_price = Decimal.mult(price, discount)
    "final price is: #{discounted_price}"
end

The “optional type” in elixir is {:ok, value} | :error by convention as already mentioned. So if your methods return that instead of nil you would use a with statement.

with {:ok, price} <- Item.get_price(),
     {:ok, discounted_price} <- calculate_discount(price, discount) do
  "final price is: #{discounted_price}"
else
  :error -> "no price available"
end

This is also an option, but I’d prefer a case statement :slight_smile:

with price <- Item.get_price() when !is_nil(price), 
     discounted_price <- Decimal.mult(price, discount) do
  "final price is: #{discounted_price}"
else
  nil -> "no price available"
end
3 Likes

Nothing is stopping you from implementing those if_present and or_else yourself if you prefer code like that.

defmodule NilTools do
  def if_present(nil, _), do: nil
  def if_present(value, callback), do: callback.(value)
  def or_else(nil, value), do: value
  def or_else(value, _), do: value
end

and then
import NilTools where you wanna use it.

4 Likes

I’m well aware of that. This is true for maybe half of the functions in the Enum module, yet they exist :slight_smile:

Mine was only a toy example. For something that simple, of course writing everything in one function or single case statement would be better.

My point was about having a series of chained function calls (not necessarily written by you) that sometimes can return nil, requiring early exit from the pipeline. You’re right, with is the elixirly solution in that case, and what I normally use too. But pipelines have also some advantages IMO. I personally find them easier to read and debug (for example using dbg(), but maybe that also works with the with statement by now, I haven’t checked.)

If you require early exits out of a set of chained function calls then that’s no longer a pipeline in the elixir sense. Therefore |> is no longer the tool to use, no matter how much people would like it to be.

5 Likes

I think it would help a lot, despite Elixir being dynamic. The problem actually doesn’t lay in the nil itself, but in having value | nil instead of some(value) | nil or {:some, value} | nil, which happens in many places, including the standard library. When doing

value = Map.get(map, key)
some_fun(value)

it’s easy to pass nil to some_fun by accident. Fortunately, there is Map.fetch, and if you have

value = Map.fetch(map, key)
some_fun(value)

then some_fun will probably crash, since even if the value is there, it’ll get {:ok, value} instead of just value. So, most probably it’ll always crash, so it’s way easier to debug. There’s also a better chance that Dialyzer catches it. Sadly, Map.get is used all over the place and there is Map.pop, get_in, Enum.find and many others that don’t have their {:ok, value} counterparts.

This is actually a main point of the whole nil idea. Generally speaking, this allows us to write much less code. And among whole three error-handling styles Elixir supports (these are Maybe style value | nil, Option or Either style {:ok, res} | :error or {:ok, res} | {:error reason} or Raising style), this one should be used when you write code which can tolerate some missing data.


There is always GitHub - hissssst/pathex: Fastest tool to access data in Elixir which supports both option and raising style

1 Like

Then I find this idea very harmful and not worth what it gives.

It’s a great lib, but accessing collections only covers a part of cases where nil exists. While libraries may go for optional-based API, trying to avoid nil in general isn’t going to be smooth and idiomatic until the stdlib leverages it. That’s only my opinion ofc :wink:

1 Like

HEAR, HEAR

2 Likes

I should qualify the pessimism of my answer. There are plenty of languages with thorough guards against nulls or nil, but a language is only part of the equation. How many ecosystems have feature complete alternatives to mix, phoenix, ecto, and absinthe, along the with thorough online docs and guides? I would start with ecosystem and tooling, and then narrow down by language features once you have that short list.

You should look at @expede’s libraries, particurly GitHub - expede/exceptional: Helpers for Elixir exceptions and README — Witchcraft v1.0.4

Thanks. First link is very interesting (the second one scares me a little bit :smiley:). Always refreshing to see unconventional takes on how things can be done.

1 Like

I feel exactly the same way, and think it’s a shame that nil has been given special treatment in Elixir. I actually created a package to try and address this problem: ok_then v1.1.0 — Documentation. We’ve been using this package to great effect in a relatively complex codebase that needs a lot of error handling and missing-value fallback behaviour.

I’ve found that many (most?) of the nil-related issues I’ve faced are caused by Ecto, which uses a nullable-value pattern for all fields on records it loads from the database. I would have preferred if fields were structs that required pattern matching to check for database NULLs, or at least for it to be an option.

3 Likes

Isn’t that a rather inefficient way to map a table? Say we have 30 columns in a table, you want a struct with 30 structs inside it, If we need to process 10 million of these rows in our application we have to pattern match on every single struct, goodbye performance.

Not necessarily, no. In my experience, this will depend a lot on the domain logic. If each of these fields is legitimately nullable in the domain, we’d need to include a nil-check somewhere anyway, so performance would be comparable. If the field is not nullable in the db, I’d be quite happy for the field to be as-is now. I’d expect Ecto to raise an exception if the db returns null for a field that is not expected to be nullable.

And it doesn’t have to be a struct, of course. If performance does turn out to be a concern for this pattern, a tuple would work just fine. We use {:ok, value} | :none extensively in our codebase to model an optional value.

2 Likes