Variable Declaration, Mutation, and Scope

This doc addresses these questions:

Table of Contents
YSH Design Goals
Keywords Are More Consistent and Powerful Than Builtins
Declare With var and const
Mutate With setvar and setglobal
"Return" By Mutating a Place (advanced)
Comparison to Shell
Keywords Behave Differently at the Top Level (Like JavaScript)
Usage Guidelines
The Top-Level Scope Has Only Dynamic Checks
proc and func Scope Have Static Checks
Procs Don't Use "Dynamic Scope"
Reading Variables
Shell Language Constructs That Write Variables
Builtins That Write Variables
Reminder: Proc Scope is Flat
More Details
Examples of Place Mutation
Bare Assignment
Temp Bindings
Appendix A: More on Shell vs. YSH
Appendix B: Problems With Top-Level Scope In Other Languages
Related Documents

YSH Design Goals

YSH is a graceful upgrade to shell, and the behavior of variables follows from that philosophy.

Keywords Are More Consistent and Powerful Than Builtins

YSH has 5 keywords affect shell variables. Unlike shell builtins, they're statically-parsed, and take dynamically-typed expressions on the right.

Declare With var and const

It looks like JavaScript:

var name = 'Bob'
const age = (20 + 1) * 2

echo "$name is $age years old"  # Bob is 42 years old

Note that const is enforced by a dynamic check. It's meant to be used at the top level only, not within proc or func.

const age = 'other'  # Will fail because `readonly` bit is set

Mutate With setvar and setglobal

proc p {
  var name = 'Bob'       # declare
  setvar name = 'Alice'  # mutate

  setglobal g = 42       # create or mutate a global variable
}

"Return" By Mutating a Place (advanced)

A Place is a more principled mechanism that "replaces" shell's dynamic scope. To use it:

  1. Create a place with the & prefix operator
  2. Pass the place around as you would any other value.
  3. Assign to the place with its setValue(x) method.

Example:

proc p (s; out) {  # place is a typed param
  # mutate the place
  call out->setValue("prefix-$s")
}

var x
p ('foo', &x)  # pass a place
echo x=$x  # => x=prefix-foo

Comparison to Shell

Shell and bash have grown many mechanisms for "declaring" and mutating variables:

Examples:

readonly name=World        # no spaces allowed around =
declare foo="Hello $name"
foo=$((42 + a[2]))
declare -n ref=foo         # $foo can be written through $ref

These constructs are all discouraged in YSH code.

Keywords Behave Differently at the Top Level (Like JavaScript)

The "top-level" of the interpreter is used in two situations:

  1. When using YSH interactively.
  2. As the global scope of a batch program.

Experienced YSH users may notice that var and setvar behave differently in the top-level scope vs. proc scope. This is caused by the tension between the interactive shell and the strictness of YSH.

In particular, the source builtin is dynamic, so YSH can't know all the names defined at the top level.

For reference, JavaScript's modern let keyword has similar behavior.

Usage Guidelines

Before going into detail on keyword behavior, here are some practical guidelines:

That's all you need to remember. The following sections explain the rationale for these guidelines.

The Top-Level Scope Has Only Dynamic Checks

The lack of static checks affects the recommended usage for both interactive sessions and batch scripts.

Interactive Use: setvar only

As mentioned, you only need the setvar keyword in an interactive shell:

ysh$ setvar x = 42   # create variable 'x'
ysh$ setvar x = 43   # mutate it

Details on top-level behavior:

Batch Use: const only

It's simpler to use only constants at the top level.

const USER = 'bob'
const HOST = 'example.com'

proc p {
  ssh $USER@$HOST ls -l
}

This is so you don't have to worry about a var being redefined by a statement like source mylib.sh. A const can't be redefined because it can't be mutated.

It may be useful to put mutable globals in a constant dictionary, as it will prevent them from being redefined:

const G = { mystate = 0 }

proc p {
  setglobal G.mystate = 1
}

proc and func Scope Have Static Checks

These YSH code units have additional static checks (parse errors):

Procs Don't Use "Dynamic Scope"

Procs are designed to be encapsulated and composable like processes. But the dynamic scope rule that Bourne shell functions use breaks encapsulation.

Dynamic scope means that a function can read and mutate the locals of its caller, its caller's caller, and so forth. Example:

g() {
  echo "f_var is $f_var"  # g can see f's local variables
}

f() {
  local f_var=42 g
}

f

YSH code should use proc instead. Inside a proc call, the dynamic_scope option is implicitly disabled (equivalent to shopt --unset dynamic_scope).

Reading Variables

This means that adding the proc keyword to the definition of g changes its behavior:

proc g() {
  echo "f_var is $f_var"  # Undefined!
}

This affects all kinds of variable references:

proc p {
  echo $foo         # look up foo in command mode
  var y = foo + 42  # look up foo in expression mode
}

As in Python and JavaScript, a local foo can shadow a global foo. Using CAPS for globals is a common style that avoids confusion. Remember that globals should usually be constants in YSH.

Shell Language Constructs That Write Variables

In shell, these language constructs assign to variables using dynamic scope. In YSH, they only mutate the local scope:

Builtins That Write Variables

These builtins are also "isolated" inside procs, using local scope:

YSH Builtins:

Reminder: Proc Scope is Flat

All local variables in shell functions and procs live in the same scope. This includes variables declared in conditional blocks (if and case) and loops (for and while).

proc p {  
  for i in 1 2 3 {
    echo $i
  }
  echo $i  # i is still 3
}

This includes first-class YSH blocks:

proc p {
  var x = 42
  cd /tmp {
    var x = 0  # ERROR: x is already declared
  }
}

More Details

Examples of Place Mutation

The expression to the left of = is called a place. These are basically Python or JavaScript expressions, except that you add the setvar or setglobal keyword.

setvar x[1] = 2                 # array element
setvar d['key'] = 3             # dict element
setvar d.key = 3                # syntactic sugar for the above
setvar x, y = y, x              # swap

Bare Assignment

Hay allows const declarations without the keyword:

hay define Package

Package cpython {
  version = '3.12'  # like const version = ...
}

Temp Bindings

Temp bindings precede a simple command:

PYTHONPATH=. mycmd

They create a new namespace on the stack where each cell has the export flag set (declare -x).

In YSH, the lack of dynamic scope means that they can't be read inside a proc. So they're only useful for setting environment variables, and can be replaced with:

env PYTHONPATH=. mycmd
env PYTHONPATH=. $0 myproc  # using the ARGV dispatch pattern

Appendix A: More on Shell vs. YSH

This section may help experienced shell users understand YSH.

Shell:

g=G                         # global variable
readonly c=C                # global constant

myfunc() {
  local x=X                 # local variable
  readonly y=Y              # local constant

  x=mutated                 # mutate local
  g=mutated                 # mutate global
  newglobal=G               # create new global

  caller_var=mutated        # dynamic scope (YSH doesn't have this)
}

YSH:

var g = 'G'                 # global variable (discouraged)
const c = 'C'               # global constant

proc myproc {
  var x = 'L'               # local variable

  setvar x = 'mutated'      # mutate local
  setglobal g = 'mutated'   # mutate global
  setglobal newglobal = 'G' # create new global
}

Appendix B: Problems With Top-Level Scope In Other Languages

Related Documents

Generated on Wed, 17 Jul 2024 00:39:14 +0000