What would you remove from Elixir?

I assume you know but if not check out OK. It is very similar to exceptional; However I am trying to keep it really small so it could act as a blueprint to what would support for result tuples could be added to the language. As a bonus it has a with macro.[quote=“Qqwy, post:22, topic:5622”]
I think you will like Ratio, because it does much of this.
[/quote]

That also looks good. I have just played around and 30mins allowed me to do the following.

use Num.Rational

a = 1/2
b = 3/4
assert "2/3" == "#{a / b}"
2 Likes

I just have to repeat what José said in IRC yesterday:

José Valim: honestly, i only added nil because i heard it was the one billion dollar mistake
but i was thinking that the person made one billion dollar with the mistake
so i was waiting for my one billion dollar but alas :cry:

:'D

As to why use nil over [], I think the reason is to show intent more clearly, as [] might mean ‘no results’ rather than ‘nothing’. nil is more search-able than [], just like Enum.map is more searchable than <$>.

5 Likes

Lol, that is awesome! ^.^

I still hold that nil and [] both are not appropriate in most places they are used, properly named returns are always better. :slight_smile:

2 Likes

I would remove nil.

2 Likes

I’m reminded of old days in Erlang (decade+ ago) where there was discussion about some of the API’s (especially ETS) about some things taking or returning magic values (things like :"$ENDOFTABLE$" or something like that) to indicate empty and so forth, it is unlikely to be a valid value, but it ‘could’ be a valid value, and that they should have returned tagged-tuples instead, it was a wart on the system.

Right now Elixir is doing the same, it is returning the value nil, which actually is a valid value in many cases right now (due to how ubiquitous Elixir uses nil all over the place) so sometimes you cannot tell what is valid and what is not, there have been multiple conversations on the Issue Tracker and here and other places talking about those API’s and how to figure out if the nil is the non-value nil or if it a valid return from what they were trying to get. It is interesting how things are repeating in the Elixir world without learning from the old Erlang days. ^.^

Tagged tuples really should be far far more ubiquitous in Elixir, it could have made a better syntax for those perhaps, or just use them as-is is perfectly fine.

Turning to tuples everywhere, unfortunately, isn’t a viable solution. Just couple days ago, there was an issue about replacing the “awfully nil-ridden” Enum.find_value (and friends). This would mean, however, in many places replacing code that is very readable and simple with something very complex.

Let’s look at some examples I posted in the issue:

In Phoenix here we would change from:

content =
  Enum.find_value(sources, fn source ->
    File.exists?(source) && File.read!(source)
  end) || raise "could not find #{source_file_path} in any of the sources"

to

res = 
  Enum.seach(sources, fn source ->
    if File.exists?(source) do
      {:ok, File.read!(source)}
    else
      :error
    end
  end) 
content =
  case res do
    :error -> raise "could not find #{source_file_path} in any of the sources"
    {:ok, content} -> content
  end

Another similar use case is present just couple lines lower in the same file. I also found a couple more uses with an || after find_value/2 in Phoenix, but those could be arguably replaced by passing the default value to find_value/3. It’s not possible in case we wanted to raise, though.

Another use that would require more convolution would require changing from:

Enum.find_value(headers, fn({k, v}) -> k =~ ~r/^origin$/i && v end)

to

Enum.search(headers, fn({k, v}) -> if k =~ ~r/^origin$/i, do: {:ok, v}, else: :error end end)

with additional modification of the consumer, from a simple if to a case match on the ok tuple.

Last use I found after a brief grep of deps of my app is in ecto (and that one I wrote myself). It would require changing:

defp check_operations_valid(operations) do
  Enum.find_value(operations, &invalid_operation/1) || {:ok, operations}
end

defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}),
do: {:error, {name, changeset, %{}}}
defp invalid_operation(_operation),
do: nil

to
```elixir
defp check_operations_valid(operations) do
  case Enum.search(operations, &invalid_operation/1) do
    :error -> {:ok, operations}
    {:ok, invalid} -> {:error, invalid}
  end
end

defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}),
do: {:ok, {name, changeset, %{}}}
defp invalid_operation(_operation),
do: :error

While it does not introduce more code, it’s much harder to understand, since we’re looking for an error, but have to return it tagged in an ok tuple, just to switch tags at the end.

The situation is far more nuanced than “nil is bad”. I urge some of you that propose removing it, to try to remove it from your code and get back to us with results.

Also, switching from atom nil to [] would make the issue far, far worse. An empty list is so much more ubiquitous than atom nil, it would require that much more attention when used. It would also lead to conflating two notions - empty collections and “no value”.

4 Likes

About if you can use with for the same effect :stuck_out_tongue:

But in general, i highly prefer function pattern matching because

  1. it makes the scoping evident
  2. it gives the test a name which explains really well what you are doing
  3. it is extendable
  4. it is really not a way of thinking about your flow that i want to offer to people.
1 Like

Why use boolean operators in the befores but not the afters? Seems unfair.

content =
  case Enum.search(sources, &(File.exists?(&1) && {:ok, File.read!(&1)} || :error)) do
    {:ok, content} -> content
    :error -> raise "could not find #{source_file_path} in any of the sources"
  end
Enum.search(headers, fn({k, v}) -> k =~ ~r/^origin$/i && {:ok, v} || :error end)

I wrote why I dislike hiding an if behind && and else behind || in this article.

When you think about teaching programming, what’s the first thing you want someone to learn? It’s if! It’s the most basic construct in nearly every programming language. Every programmer knows if, every programmer wrote thousands upon thousands of ifs. Every programmer knows what if means and what was the intention of the coder who wrote it.
[…]

1 Like

I disagree with your article but that’s not the point. The point is that if you rewrote your befores using if they would be verbose too.

The thing is, I didn’t have to. I feel completely comfortable with a single && operator. I feel completely not comfortable with a combination of && and || to emulate an if.

1 Like

I often find myself writing multiline statements that depend on boolean values, which however do not warrant their own named function. For this, if/else is very useful.

(For the min example of @michalmuskala’s blog post, I think I’d be most happy with this form:

def min(a, b) when a < b, do: a
def min(_a, b), do: b

)

1 Like

Also, I don’t think {:ok, _} and :error are very appropriate in this situation. It’s not an error if the element doesn’t match what you’re looking for. Something more descriptive would be better, like :notfound.

It’s unconventional (as far as I know), but I think an idiom where a single element tuple holds a value in cases where only a single value must be returned could be introduced to reduce complexity.

Enum.search(list, fn(item) -> ... && {item} || :notfound end)

Your arguments against boolean operators seem to be a) that they don’t make long conditionals like

  if (vehicle.type == :car)
    drive(vehicle)
  elsif (vehicle.type == :plane)
    fly(vehicle)
  else
    sail(vehicle)
  end

any better, and b) that there’s nothing wrong with if in the first place. However, wouldn’t you agree that the complexity in your after examples is cause by the ifs? The boolean operators in my example are not long conditionals either. From your article, I don’t see any reasons not to choose && || over if in this situation.

I don’t know about others, but whenever I see foo? && bar || baz I have to “parse” it to an if before I can understand the code. That’s why I call it “emulation” - for me it looks like a fancy way to make your code “if-free” (that seems to be a popular metric lately) without actually giving any real benefits.

Firstly I would like to remove bugs :smiley:

1 Like

I only do that if I’m doing ElixirGolf and trying to save a few characters for fun. Do you really see that in production code?!? :scream:

1 Like

Indeed it would, but that is only because of the current patterns that are used. Following your example but with a monadic style (from this thread) it would change from:

content =
  Enum.find_value(sources, fn source ->
    File.exists?(source) && File.read!(source)
  end) || raise "could not find #{source_file_path} in any of the sources"

To if Enum.find_value’s callback an option tuple ({:ok, blah}|:error) (using module helpers as stated should be made in the above linked thread):

{:ok, content} =
  Enum.find_value(sources, &Option.from_result(File.read(&1)))
  ~> fn _ -> raise "could not find #{source_file_path} in any of the sources" end
  # Or do `|> unwrap()` here and replace the binding above with just `content =`

And for the other example of:

Enum.find_value(headers, fn({k, v}) -> k =~ ~r/^origin$/i && v end)

To if Enum.find_value’s callback an option tuple ({:ok, blah}|:error) (using module helpers as stated should be made in the above linked thread):

Enum.find_value(headers, fn({k, v}) -> Option.wrap_if(k =~ ~r/^origin$/i, v) end

And the third example of:

defp check_operations_valid(operations) do
  Enum.find_value(operations, &invalid_operation/1) || {:ok, operations}
end

defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}),
  do: {:error, {name, changeset, %{}}}
defp invalid_operation(_operation),
  do: nil

To if Enum.find_value’s callback an option tuple ({:ok, blah}|:error) (using module helpers as stated should be made in the above linked thread):

defp check_operations_valid(operations) do
  Enum.find_value(operations, &invalid_operation/1)
  |> Result.from_option(operations) # Takes the optional error value, only evaluated if an `:error`
  |> Result.flip() # Flip the `:error` and `:ok` cases
end

defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}),
  do: {:ok, {name, changeset, %{}}}
defp invalid_operation(_operation),
  do: :error

Which although a bit longer I find more readable rather than the easily missed (since I did originally miss it) trailing || {:ok, operations} bit.

Tagged Tuples are far superior, especially with conditional piping as well.

Entirely true, and gets back to what I say should be properly named return values. Although if we could drop the erlang heritage and big erlang style I’d say the option type should be {:some, value}/:none and the result type should be {:ok, value}/{:error, result} or something along those lines, but {:ok, value}/:error is definitely more erlang’y and there is a lot of history and code there.

I’ve also been for that too, doing the option as {value}/:error (or :none or whatever) would make sense, but there is no precedence for that either.

That example would be significantly better with cond, yet another reason I encourage either purging if as-it-stands from the language, or forcing an else clause, always. I really do not like this magically appearing nil from invisible else’s.

Agreed yep, hence why I want some construct that forces the else to exist, no magically appearing nil’s and such.

Which gets back to my wanting to fix the scoping brokenness. :-V

1 Like

I really don’t see what’s so bad about foo? && bar || baz. If anything, it’s often easier for me to parse than if as I find it less visually complex (either vertically, or by characters). I don’t spend any extra time parsing it because I already know what it does—it’s a familiar pattern.

1 Like

It is easy to me because overwhelming perl-work in last job, but I still do not like it. Even C’s pred ? true : false is clearer to me for some reason…

1 Like