Skip to content

Conversation

@faho
Copy link
Member

@faho faho commented Jul 15, 2021

This makes the "unspecified" scope available - the one that is used
when no variable exists.

It's either the enclosing function's topmost local scope, or global
scope if there is no function.
(after deliberation, using global felt awkward because that would make the variable available everywhere, so we're just local)

This removes the need to declare variables locally before use. E.g.:

set -l thing
if condition
    set thing one
else
    set thing two
end

could be written as

if condition
    set -f thing one
else
    set -f thing two
end

Note: Many scripts shipped with fish use workarounds like and/or
instead of if, so it isn't easy to find good examples.

Also, if there isn't an else-branch in that above, just with

if condition
    set -f thing one
end

that means something different from setting it before! Now, if
condition isn't true, it would use a global (or universal) variable of
te same name!

Some more interesting parts:

Because it is a local scope, setting a variable -f and
-l in the toplevel of a function ends up the same:

function foo2
    set -l foo bar
    set -f foo baz # modifies the *same* variable!
end

but setting it locally inside a block creates a new local variable
that shadows the function-scoped variable:

function foo3
    set -f foo bar
    begin
        set -l foo banana
        # $foo is banana
    end
    # $foo is bar again
end

This is how local variables already work. "Local" is actually "block-scoped".

Also set --show will only show the closest local scope, so it won't
show a shadowed function-level variable. Again, this is how local
variables already work, and could be done as a separate change.

Fixes #565

One of the tests explicitly checks one case mentioned by @ridiculousfish in #565 (comment):

function f
    set -l fruit banana
    if true
        set -f fruit orange
    end
    echo $fruit #banana
end

This isn't an issue, because function scope is the outermost local scope of the function. There is no difference. It's not a new node.

TODOs:

  • Changes to fish usage are reflected in user documentation/manpages.
  • Tests have been added for regressions fixed
  • User-visible changes noted in CHANGELOG.rst

@faho faho added this to the fish 3.4.0 milestone Jul 15, 2021
Copy link
Contributor

@krobelus krobelus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, this is really nice!
We should try to make this the default behavior eventually - during the transition period we could print a deprecation warning when a statement set foo bar inside a function sets a global or universal var.

@faho
Copy link
Member Author

faho commented Jul 15, 2021

We should try to make this the default behavior eventually - during the transition period we could print a deprecation warning when a statement set foo bar inside a function sets a global or universal var.

Wait, you mean requiring an explicit --global to set a global variable? That's a large change that could work out, but has one wart: You'd have to choose between global and universal.

Anyway, that's for another time.


I want to make this kind of scoping prominent. I believe that it's the much better model from --local (which is okay for specific situations and should be set --block, really).

But switching that is a much much much bigger change, so I would appreciate any ideas on how to do that - --function is a bit of an awkward name ("does this set a function?"). Is there anything better? set --real-local?

@krobelus
Copy link
Contributor

You'd have to choose between global and universal.

That problem can go away once we "merge" global and universal scopes


maybe --function-local?

@faho
Copy link
Member Author

faho commented Jul 16, 2021

maybe --function-local?

"Does this make my function a local function?"

Maybe "--function-scoped", but that's a bit asymmetrical to what's already there.

Also neither option really explains what happens when there isn't a function - set --function is global in that case.

@faho
Copy link
Member Author

faho commented Jul 21, 2021

So, looking over #565, there's a fair amount of talk over what set -f should do at the top level. Some are even arguing for making it error out.

I don't agree with that. I want set -f to be the default you reach for when you don't specifically need something else, and having it error out makes for more thinking about when it can be used.

Now, this PR currently uses the global scope at the top level, and that's probably not great - now suddenly the variable is available everywhere. Instead I'm going to make it the top level local scope.

That also makes explaining what this does easy:

set --function sets a variable in the function's top local scope. If used outside of a function, it sets it in the top local scope.

(this could be made shorter by explaining that it's "the top-most accessible local scope" or some such, but that is a very dense explanation)

Of course that means it's not entirely the same as the "undefined scope" anymore, but tbh that's not great anyway.

@faho
Copy link
Member Author

faho commented Jul 21, 2021

Okay, I have now made set --function use the top local scope if used outside of a function.

Note that this now means functions with --no-scope-shadowing can set local variables:

> function set-my-thing --no-scope-shadowing
     set -f $argv 
   end
> function foo
      set -S bar
      set-my-thing bar nana
      set -S bar
   end
$bar: set in local scope, unexported, with 1 elements
$bar[1]: |nana|

(to be clear that's the -f scope from foo! Even with begin; end around set-my-thing it's accessible)

I'm not entirely sure if I like that or not. They could already read local variables (that's the point of it!), and they could always set global variables, but now they can set local variables as well. I think --no-scope-shadowing is a sufficiently expert level thing that it might be okay, and possibly even useful? Maybe? Or is this the most disgusting thing since the discovery of mushrooms?

Compare also #3540.

faho added 4 commits July 23, 2021 18:24
This makes the "unspecified" scope available - the one that is used
when no variable exists.

It's either the enclosing function's topmost local scope, or global
scope if there is no function.

This removes the need to declare variables locally before use. E.g.:

```fish
set -l thing
if condition
    set thing one
else
    set thing two
end
```

could be written as

```fish
if condition
    set -f thing one
else
    set -f thing two
end
```

Note: Many scripts shipped with fish use workarounds like `and`/`or`
instead of `if`, so it isn't easy to find good examples.

Also, if there isn't an else-branch in that above, just with

```fish
if condition
    set -f thing one
end
```

that means something different from setting it before! Now, if
`condition` isn't true, it would use a global (or universal) variable of
te same name!

Some more interesting parts:

Because it *is* a local scope, setting a variable `-f` and
`-l` in the toplevel of a function ends up the same:

```fish
function foo2
    set -l foo bar
    set -f foo baz # modifies the *same* variable!
end
```

but setting it locally inside a block creates a new local variable
that shadows the function-scoped variable:

```fish
function foo3
    set -f foo bar
    begin
        set -l foo banana
        # $foo is banana
    end
    # $foo is bar again
end
```

This is how local variables already work. "Local" is actually "block-scoped".

Also `set --show` will only show the closest local scope, so it won't
show a shadowed function-level variable. Again, this is how local
variables already work, and could be done as a separate change.

Fixes fish-shell#565
@faho
Copy link
Member Author

faho commented Jul 23, 2021

Alright, I added the docs, which kinda shows how convoluted all of this is.

We now have:

  • Universal
  • Global
  • Local (which should really be "block-scoped")
  • Function-local (which is the same as block-scoped outside of a block!)
  • Undefined scope (which is function-local inside of functions, global outside)

This should really be simplified. The classic "local" is probably mostly obsolete, unless you really need a variable only valid in a block, like for:

begin
    set -lx PAGER cat
    # do things that use $PAGER
end

So removing it, or changing the default meaning of -l is probably out of the question?

We also can't really make the undefined scope local instead, because millions of config.fish have cruft like

set -x PAGER cat

(alternative: Make exported variables global by default?)

Merging universal and global scope sounds tempting, but what if you e.g. want to try out a new value for a universal variable?

@krobelus
Copy link
Contributor

Yeah this is getting fairly complex. I wonder how we can provide a simpler UI with fewer scopes.

My main gripe with (top-level) local scope is that local variables are not inherited by functions by default.

function f
    echo "not visible inside function:" $foo
end
set --function foo bar # same with set --local
f

I'm not sure if not inheriting locals is really useful, especially since as of now, function authors should declare their own variables with --local/--function. It is occasionally a problem when writing completion scripts that want to use private local variables in functions.

If we could make set foo bar inside functions never set globals/universals that would be nice..

Merging universal and global scope sounds tempting, but what if you e.g. want to try out a new value for a universal variable?

Right, that wouldn't really work, we can't use locals for trying out values.

@faho
Copy link
Member Author

faho commented Jul 24, 2021

My main gripe with (top-level) local scope is that local variables are not inherited by functions by default.

Oh I heavily disagree with this!

Not passing local variables everywhere is good because it encapsulates things. It leaves knowledge of these variables where it is, instead of spreading it all over the place.

Your example here is a toy. In a real situation often the function would be defined elsewhere, even in a different file. So having random variables defined there would be quite confusing.

And if you do need to pass a local variable, we have an option for that: Exporting them:

function f
    echo "not visible inside function:" $foo
end
set --function --export foo bar # same with set --local
f

The function will receive a local copy of the variable.

@ridiculousfish ridiculousfish self-requested a review July 26, 2021 21:13
Copy link
Member

@ridiculousfish ridiculousfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks excellent to me. I agree with having top-level set -f set in the top-most local scope, not a global. I think this actually simplifies scoping, since users can now mostly ignore set -l.

Conceptually we should not think of "function-local" but just "function variables" to avoid confusion. A local variable is local to the innermost block; if that block is a function it is a function variable. Local variables become the "advanced" case.

One wrinkle is that set --show will still call function variables "local" but I think that's not a big deal.

Now in terms of function-scope, local is the odd one out.
@faho faho merged commit 733114f into fish-shell:master Aug 1, 2021
@faho
Copy link
Member Author

faho commented Aug 1, 2021

Alright, merged.

@kidonng kidonng mentioned this pull request Aug 3, 2021
3 tasks
@faho faho deleted the set-function branch September 23, 2021 09:39
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 26, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add function scope option to the set command

3 participants