Warning: variable 'A' exported from 'case'

I’ve got the following code (greatly simplified):

L = get_leader(),
Me = get_me(),
A = case L of
    Me ->
        X = something_interesting_to_forestall_certain_answers(),
        {ok, A} = make_assignments(X),
        A;
    _Other ->
        Y = something_else_for_the_same_reason(),
        {ok, A} = get_assignments(Y),
        A
end

I get the warning variable 'A' exported from 'case'. But it’s not really exported: the variable A inside the case is hidden by the variable A assigned to (and they’re actually the same value anyway). This is the case (heh) for every path.

Why does Erlang warn here? What can I do to make it happy?

I see two options here:

  1. Just remove the A =. It will be available outside the case:
L = get_leader(),
Me = get_me(),
case L of
    Me ->
        X = something_interesting_to_forestall_certain_answers(),
        {ok, A} = make_assignments(X),
        A; %% This line is not required
    _Other ->
        Y = something_else_for_the_same_reason(),
        {ok, A} = get_assignments(Y),
        A %% This line is not required
end,
A.
  1. Change the pattern match:
L = get_leader(),
Me = get_me(),
{ok, A} = case L of
    Me ->
        X = something_interesting_to_forestall_certain_answers(),
        make_assignments(X);
    _Other ->
        Y = something_else_for_the_same_reason(),
        get_assignments(Y)
end,
A.

EDIT

  1. One more option is to rename the variable inside the case:
L = get_leader(),
Me = get_me(),
A = case L of
    Me ->
        X = something_interesting_to_forestall_certain_answers(),
        {ok, A0} = make_assignments(X),
        A0;
    _Other ->
        Y = something_else_for_the_same_reason(),
        {ok, A0} = get_assignments(Y),
        A0
end
1 Like

See the Scope of variables in if/case/receive section here.

Thanks. Some thoughts:

I don’t like that feature of Erlang (I lean towards everything’s-an-expression where possible), and that’s what the warning’s attempting to prevent.

There’s some other stuff in the actual code that makes that impossible. This is what the “forestall certain answers” was intended for, but I messed it up :slight_smile:

Yeah; I’m aware. My point is that the compiler shouldn’t be warning, because the thing that’s “escaping” is the final expression in all paths, and it’s assigned to the variable with the same name. Is there something I’m missing about the semantics of the language that makes that impossible to detect?

For example: it might be escaping, but since it’s the final expression, and then it’s getting matched against itself, that’s fine (I think?), and the warning could be suppressed.

Yeah; that’s what I ended up doing.

Agree.

I’ve found this answer in the mailing list and maybe it can help to clarify:

case, if, comprehensions, etc. are assumed to introduce a new scope of their own (whether they actually do or not). From this view you are accessing a variable defined within a different conceptual scope, even if it actually is accessible in this particular case.

The reason this has been made into a warning (and many tools compile with warnings-as-errors set) is that it is possible to not assign a variable you access outside the case in every branch of it and still get a clean compile:

foo() →
case bar() of
{bing, Spam} → eat(Spam);
{bong, Eggs} → eat(Eggs)
end,
puke(Spam).

VS

foo() →
Food = case bar() of
{bing, Spam} → Spam;
{bong, Eggs} → Eggs
end,
ok = eat(Food),
puke(Food).

Hey, look, one is more likely to be self-documenting now…

This has been discussed several times (here, for example: [erlang-questions] case expression scope), and because of the weirdness of case scope being one way (its a scope semantically and conceptually, but its not really) and list comprehensions being another (it really is a separate scope, but that’s not how some other languages work) this has been made into a warning.

I remember the warning/error discussion happening, but can’t find the notes for it (I had thought the “this is always a warning” thing came up with R16 or 17…?). :frowning: Anyway, what I remember of it was the danger of not assiging a variable in every branch, that being a silly thing to even worry with since cases are sorta-kinda their own scope, and case statements have a return value anyway that should be used as per the way everything else in Erlang works.

-Craig

I think the thing to be aware of is that if in an if/case/receive you bind a variable in ALL the clauses then that variable will be exported and can be used later.

So in your code the case did export the variable so immediately outside the case the variable A is now bound. This means that the A in A = is actually bound and what you are doing is matching the returned value from the case with the variable A which in this case just happened to be bound in the case.

So you need to be very very VERY aware that in a function clause there is no variable scoping at all. So if you have a variable A in the function clause it is the SAME variable A everywhere, there is only ONE A in the whole clause.

The reason that Erlang warns you here is that it is easy to export a variable from an if/case/receive by mistake and get really weird behaviour. For example what if your you had bound A in each clause but returned something completely different? Then in this case the match would fail as it was matching against the A which has a value.

We were thinking of adding scoping but it would have broken too much existing code. Btw that is why let is a reserved word, we did that “just in case”.

1 Like

Perhaps a case statement is not the best construct to use here? It is leaking too much abstraction outside. A function would both reduce nesting and scope, plus might even be shorter under certain conditions:

foo() ->
    foo(get_leader(), get_me()).

foo(Leader, Leader) ->
    X = something_interesting_to_forestall_certain_answers(),
    {ok, A} = make_assignments(X),
    A;
foo(_Leader, _User) ->
    Y = something_else_for_the_same_reason(),
    {ok, A} = get_assignments(Y),
    A.

(The name is just a placeholder since I don’t know what your code is actually supposed to be doing).

If I interpret your comments correctly, I assume not every clause in check_leader/2 will match against {ok, Something} so having a separate clause for each individual case makes total sense here, I think.

If foo(Leader, Leader) is to confusing, there are always guards: foo(Leader, User) when User =:= Leader -> .... In this case I might compose even further:

foo() -> foo(is_leader(get_me()).

foo(leader) ->
    X = something_interesting_to_forestall_certain_answers(),
    {ok, A} = make_assignments(X),
    A;
foo(user) ->
    Y = something_else_for_the_same_reason(),
    {ok, A} = get_assignments(Y),
    A.

role(User) -> role(User, get_leader()).

role(Leader, Leader) -> leader;
role(User, Leader) -> user.

Could this now be solved with a feature flag?

-feature(sane_scoping, enabled).

This was suggested a long time ago.

The variable A outside the case form is the very same variable as the variable A inside the case form.
No variables are “hidden” in your example. Why would they be?

What’s wrong with

{ok,A} = if L =:= Me -> ..., make_assignments(X)
; true -> ..., get_assignment(Y)
end

Or

ok({ok,X}) -> X.

A = if L =:= Me -> ..., ok(make_assignments(X))
; true -> ..., ok(get_assignment(Y))
end

or even

if L =:= Me -> ..., {ok,A} = make_assignments(X)
; true -> ..., {ok,A} = get_assignment(Y)
end

Binding A inside the case and then trying to (re)bind the same variable again outside the case is going to confuse any numan trying to read it, never mind the compiler.

Since the code you showed is not the actual code, I suspect that there is actually a branch in the real case that does NOT bind the variable A, because that’s what complaints about variables exported from cases normally mean.