EEP 70: Non-filtering generators

As a long-term goal, yes. This shouldn’t be rushed, because it is often not obvious whether a strict generator is safe. It is important to check the code coverage to ensure that all changes are covered.

It shouldn’t be. No extra tests are added. Of course, as everybody that has tried to optimize something knows, any change to a program will make it slower. :wink:

2 Likes

I would say accepted but not encouraged. I if remember correctly I once introduced or wanted to introduce a compiler warning for comprehensions without generators, but I had to withdraw it because far more code than expected used it.

I usually don’t use this style myself, but to my surprise I recently found that used it in the compiler a long time ago.

2 Likes

Hi,

Maybe I missed something in this longer thread, but isn’t if_this:is_true() andalso do:something() nicer? [1]

Olivier.

[1] And also more useful as, if needed, the value of this expression (if if_this:is_true() is true, of course) is directly do:something().

1 Like

For almost as long as if has existed in Erlang, people had come up with creative ways to write else-less ifs:

  • [do:something() || if_this:is_true(), this:is_true(too)]
  • if_this:is_true() andalso this:is_true(too) andalso do:something()
  • catch if This == true -> do:something() end (yeah, only works for guards)
  • some people even used macros or parse_transforms (luckily, I don’t remember examples of those).

I showed some of these examples in this talk.

In the end, most of it boils down to how much do you want to confuse future developers reading your code and, as @bjorng points out, how much can OTP change in the future… It seems like LCs without generators are here to stay because they’re used quite a bit in OTP itself, but nothing prevents the OTP team from eventually enforcing the presence of at least one generator in LCs, or the type of the second argument to andalso, etc… :person_shrugging:

1 Like

I don’t see any reason to do that. Forgetting to add a generator to a LC is neither a common nor a subtle bug. One will notice that bug quickly.

We don’t want to do that.

When we first implemented andalso, it did check its right-hand operand. That had the unfortunate consequence that a function such as the following would not be tail-recursive:

all_even([H|T]) ->
    H rem 2 =:= 0 andalso all_even(T);
all_even([]) ->
    true.

To make it tail-recursive, one would have to use a case or an if:

all_even([H|T]) ->
    case H rem 2 of
        0 -> all_even(T);
        _ -> false
    end;
all_even([]) ->
    true.

With the current behavior of andalso, the two versions of all_even/1 compile down to identical BEAM code.

3 Likes

What I was actually driving at was, should relaxed generators be changed to strict ones even where it doesn’t (can’t) make a change semantically (like here)?

That is, should we aim for making strict generators a best practice, and using relaxed ones only if there are reasons not to use strict ones (like here)?

… and if it doesn’t, there is a bug, yes :wink:

No, I don’t think we should change existing code in Erlang/OTP where it doesn’t matter. There are far too many list comprehensions in the OTP codebase.

For new modules and applications, I think using strict generators by default is a good idea.

2 Likes

My biggest problem with Erlang’s if is that the catch-all clause isn’t else; it’s true. That’s just weird. To be clear: I understand it. I just hate it.

I’ll jump through any number of hoops to avoid using if (though not the empty generator trick; that’s worse, somehow).

2 Likes

How about using a macro to ease the pain?

-define(else, true).

test(X) ->
    if
        X -> foo;
        ?else -> bar
    end.

That said, the most esoteric “workaround” I have heard of (I don’t remember where) is this :crazy_face::

if
    X -> foo;
    el/=se -> bar
end
4 Likes

I used to hate that true was used for “else” in if statements, for about 10…15 years, but then I came to my senses and realized that the intention of the Erlang if is to select the first true - not a two way boolean selection.

So I saw the light, accepted that, and found peace in enlightenment…

:slight_smile:

3 Likes

The documentation shows a great example:

-module(tut9).
-export([test_if/2]).

test_if(A, B) ->
    if
        A == 5 ->
            io:format("A == 5~n", []),
            a_equals_5;
        B == 6 ->
            io:format("B == 6~n", []),
            b_equals_6;
        A == 2, B == 3 ->                      %That is A equals 2 and B equals 3
            io:format("A == 2, B == 3~n", []),
            a_equals_2_b_equals_3;
        A == 1 ; B == 7 ->                     %That is A equals 1 or B equals 7
            io:format("A == 1 ; B == 7~n", []),
            a_equals_1_or_b_equals_7
    end.

Simple and clear

Well… that’s precisely a great example for when not using if actually generates better code (IMHO)…

-module(tut9).
-export([test_if/2]).

test_if(5, _) ->
    io:format("A == 5~n", []),
    a_equals_5;
test_if(_, 6) ->
    io:format("B == 6~n", []),
    b_equals_6;
test_if(2, 3) -> %That is A equals 2 and B equals 3
    io:format("A == 2, B == 3~n", []),
    a_equals_2_b_equals_3;
test_if(A, B) when A == 1 ; B == 7 -> %That is A equals 1 or B equals 7
    io:format("A == 1 ; B == 7~n", []),
    a_equals_1_or_b_equals_7.

…or even…

-module(tut9).
-export([test_if/2]).

test_if(5, _) ->
    io:format("A == 5~n", []),
    a_equals_5;
test_if(_, 6) ->
    io:format("B == 6~n", []),
    b_equals_6;
test_if(2, 3) -> %That is A equals 2 and B equals 3
    io:format("A == 2, B == 3~n", []),
    a_equals_2_b_equals_3;
test_if(1, _) ->
    io:format("A == 1~n", []),
    a_equals_1_or_b_equals_7;
test_if(_, 7) ->
    io:format("B == 7~n", []),
    a_equals_1_or_b_equals_7.

And it’s also important to notice that the code with the if has no else clause… so it’s not an else-less if in the sense we were discussing before.

Personally I think having to repeat the function headers makes the code less clear. When you have many function clauses it may become harder to check them against each other to see if there are any logical gaps, than when having one if or case statement,

Note that your rewrite changes from number comparison (==) to matching (that is: =:=), and in your first example you mix them. A float() argument will cause confusion,

You are very right that this example has no catch-all clause, which the discussion had derailed into from the original non-filtering generators…

Right, this thread has become a little frayed after the strict generators had been merged :sweat_smile: But actually, personally, I don’t mind :wink:

Anyway, I think nobody has pointed out yet that an if does not have to have an else branch, same as a case (or a function) does not have to have a catch-all clause. The compiler won’t complain. You will get an if_clause runtime error if your if doesn’t hit any true branch, same as case_clause or function_clause errors which you will get if none of your clauses match.

1 Like

Which is exactly the issue… Developers coming from other languages usually expect if’s without else to just do nothing if the condition(s) is/are not met. Nobody expects the Spanish Inquisition here that instruction to fail.

Well, it’s probably true that developers not familiar with Erlang wouldn’t expect this behaviour, but there’s also a limit to how much a language shall try to match the expectations of developers with different backgrounds.

Said developers would not expect e.g. Prolog to backtrack or Haskell to be lazy or Bash to split strings to words or inheritance in JavaScript to be based on the prototype property. But these are all important design decisions in these languages that shouldn’t be changed to match the expectations of developers who are just learning the language. And I think the design decision that every expression shall evaluate to a value in Erlang is in this category. It’s much more important than the lack of elseless ifs.

5 Likes

If you think Erlang’s if expression is confusing, try Coq:

Inductive Bool := false | true.

Goal (if true then 1 else 2) = 2.
 reflexivity.
Qed.

I personally think if should have been called when:

when
  A =:= 5 -> io:format("A is 5~n", []);
  true ->  io:format("A is something else ~n", [])
end
5 Likes

ifs without a catch-all clause are also following Erlang’s celebrated Let It Crash philosophy - if no condition succeeds, we apparently don’t know what to do, so let it crash! (if clause)

1 Like