elvish icon indicating copy to clipboard operation
elvish copied to clipboard

More explicit syntax for temporary assignments

Open xiaq opened this issue 4 years ago • 15 comments

The current syntax for temporary assignments is borrowed from POSIX shell, and requires there to be no spaces surrounding a =, e.g.

foo=bar put $foo
pwd=/tmp touch x

The syntax is quite subtle, and is a source of syntactical complexity - for example, the syntax for lvalues have to support braced lists, in order for code like this to work (suppose that f outputs two values):

{a,b}=(f) echo $a $b

It may be a good idea to make the syntax for temporary assignments more explicit, for example using a special command with. The examples above can be written as:

with foo = bar { put $foo }
with pwd = /tmp { touch x }
with a b = (f) { echo $a $b }

However, it's not straightforward to generalize this to multiple temporary assignments, which can be done easily using the current syntax, such as a=foo b=bar put $a $b. Nesting with certainly works, but is quite verbose. One possibility is to use lists to surround the individual assignments:

with [a = foo] [b = bar] { put $a $b }

In any case, the with command is going to be more verbose than the current syntax, hence the "maybe" tag.

xiaq avatar Aug 17 '20 04:08 xiaq

FWIW, the POSIX semantics for temporary assignments work by instantiating environment variables. Except when it doesn't. That is, x=y ... is (mostly) equivalent to env x=y .... It's an example of a rather silly optimization to eliminate four characters from a statement. The "mostly" is parenthesized because the behavior depends on whether the command is a builtin or external. Even that distinction has subtleties. Consider the following:

bash-5.0$ WTF=wtf eval 'echo $WTF'
wtf
bash-5.0$ WTF=wtf echo $WTF

bash-5.0$ WTF=wtf typeset | grep WTF
bash-5.0$ WTF=wtf typeset -x | grep WTF
bash-5.0$ WTF=wtf eval 'typeset -x' | grep WTF
declare -x WTF="wtf"
bash-5.0$ WTF=wtf env | grep WTF
WTF=wtf
bash-5.0$ type env
env is hashed (/usr/bin/env)

Which is a long winded way of saying that any change to the syntax or behavior of temporary assignments requires careful thought so as not to repeat the mistakes of the POSIX standard.

krader1961 avatar Aug 17 '20 05:08 krader1961

See also, issue #705. Consider the unexpected behavior of this example:

> x = 1
> put $x
▶ 1
> x=2
> put $x
▶ 2
> x=3 true
▶ 2

The second and third "put" should have output 1 rather than 2. If this issue were implemented that would probably resolve issue #705.

krader1961 avatar Sep 17 '20 04:09 krader1961

This comment is indirectly related to this issue since the problem I'm describing involves permanent, rather than temporary, assignments. Consider this pipeline:

echo a=b | cut -d = -f 1

In a POSIX shell (e.g., bash) that will output a. In Elvish it outputs nothing because the RHS of the pipeline performs two assignments: cut = -f and -d = 1. That pipeline is a simplified version of a more complex pipeline. I am not arguing that the current Elvish semantics are wrong and should be changed. I'm simply pointing out that the surprising behavior of the above pipeline should be considered in resolving this issue.

krader1961 avatar Oct 31 '20 03:10 krader1961

Getting back to this. I'm considering stealing a design from Raku.

Introduce a special command (say tmp) that does temporary assignment within the current scope. An example:

var x = foo
{
  tmp x = bar
  # $x is "bar" for the remainder of the scope
  command using $x
}
# $x reverts to "foo"

The original original with proposal had the downside that it was tricky to do multiple temporary assignments. With tmp, just have multiple tmp commands:

var x = foo
var y = lorem
{
  tmp x = (some command)
  tmp y = (some other command)
  command using $x and $y
}

One-liner length comparison:

x=foo some command
{ tmp x = foo; some-command }
with x = foo { some-command }

Still more verbose than the current syntax, but same as the original with proposal.

xiaq avatar Dec 07 '21 22:12 xiaq

Also, if there is already a scope, using tmp is actually more concise.

For example, to and run something using each subdirectory as the working directory:

for d [*/] {
  tmp pwd = $d
  # do things in $d
}

While the current syntax will require an additional level of nesting, unless there's only one command in the body:

for d [*/] {
  pwd=$d {
    # do things in $d
  }
}

Another nice thing about tmp is that it has exactly the same syntax with var and set (and the same length; this is why I shortened Raku's temp to tmp).

xiaq avatar Dec 07 '21 23:12 xiaq

I like the idea, if only I could suggest let in lieu of tmp. It's javascript's syntax for the same construct, and I prefer it for purely ergonomic reasons. Another option is the even shorter my from perl, again with the same behavior.

fennewald avatar Dec 08 '21 00:12 fennewald

This new construct has no corresponding construct in JavaScript or Perl. It does correspond to Bash's local declaration.

Both JavaScript's let and Perl's my are lexical declarations and correspond to Elvish's var.

xiaq avatar Dec 08 '21 00:12 xiaq

I don't think referencing Bash's local keyword clarifies the matter and should be avoided. See, for example, https://stackoverflow.com/questions/4419704/differences-between-declare-typeset-and-local-variable-in-bash. The distinction between typeset, declare and local is a muddle in Bash and other POSIX shells. Having said that, a focus on how the proposed tmp keyword differs from var for magic vars like pwd would help. I'm basically a +1 on the proposal to introduce a tmp keyword from a functionality perspective. The challenge will be documenting the behavior (I found the Raku document linked to above borderline incomprehensible).

krader1961 avatar Dec 08 '21 02:12 krader1961

I've done a little more research, and did originally misunderstand. Would tmp be dynamically scoped, like local in perl?

var x = 1

fn foo []{
  echo $x
}

fn bar []{
  tmp x = 2
  foo
}

foo
bar
foo

Would this print 1, 2, 1?

fennewald avatar Dec 08 '21 03:12 fennewald

Would this print 1, 2, 1?

That's how I read it. But I wasn't sure, until I saw the pwd example. And otherwise, it would be just like var. In fact, many of the examples given would produce the same result using var, except for what's under the hood.

hanche avatar Dec 08 '21 05:12 hanche

@fennewald You're right.

Indeed Perl's local does this too - it is confusingly named and Raku's name temp better reflects what it does.

xiaq avatar Dec 08 '21 20:12 xiaq

A +1 for tmp

iandol avatar Dec 08 '21 20:12 iandol

0.18.0 will deprecate the legacy syntax.

After 0.18.0 is released, the legacy syntax will be removed. Moving this to the 0.19.0 milestone.

xiaq avatar Jan 03 '22 00:01 xiaq

I think variables should be limited to their scope, unless specified otherwise, like Python's global or nonlocal.

If we support a simple scope with { ... } which is like ( ... ) except it doesn't return the value, just like Go, then instead of

with [a = foo] [b = bar] { put $a $b }

we write

{ var a = foo; var b = bar; put $a $b }

Also maybe a new command to set multiple variables like

{ vars &a = foo &b = bar; put $a $b }

Doesn't make much different in verbosity though.

ilius avatar Jan 08 '22 12:01 ilius

@ilius Variables in Elvish are already lexically scoped, so in { var a = foo; var b = bar; put $a $b }, the variables $a and $b are not visible outside the closure. This issue was about giving an already existing variable a temporary value for a certain scope.

Anyway this has been implemented; I'll leave this open until the legacy syntax is fully removed.

xiaq avatar Jan 14 '24 12:01 xiaq

I've decided to implement the originally proposed with command as well: tmp is technically fine, but it can sometimes feel counter-intuitive (https://github.com/elves/elvish/issues/1776#issuecomment-2245282976)

xiaq avatar Jul 23 '24 13:07 xiaq