Variable Declaration, Mutation, and Scope

This doc addresses these questions:

Table of Contents
Oil Design Goals
Keywords Are More Consistent and Powerful Than Builtins
Declare With var and const
Mutate With setvar and setglobal
"Return" By Mutating Out Params With setref (advanced)
Comparison to Shell
Keywords Behave Differently at the Top Level (Like JavaScript)
Usage Guidelines
The Top-Level Scope Has Only Dynamic Checks
proc Scope Has Static Checks
Procs Don't Use "Dynamic Scope"
Shell Language Constructs Affected
Builtins Affected
More Details
Examples of Place Mutation
Syntactic Sugar: Omit const
Appendix A: More on Shell vs. Oil
Appendix B: Problems With Top-Level Scope In Other Languages
Related Documents

Oil Design Goals

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

Keywords Are More Consistent and Powerful Than Builtins

Oil 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

This is similar to JavaScript.

proc p {
  var name = 'Bob'
  const age = (20 + 1) * 2
  echo "$name is $age years old"  # Bob is 42 years old
}

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 Out Params With setref (advanced)

"Out Params" are a more controlled version of shell's dynamic scope. They reuse the nameref mechanism.

proc p(s, :myout) {           # declare out param with :
  setref myout = "prefix-$s"  # mutate it to "return" value to caller
}

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 Oil code.

Keywords Behave Differently at the Top Level (Like JavaScript)

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

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

Experienced Oil users should know that keywords like var behave differently in the top-level scope vs. proc scope. This is due to the tension between shell's interactive nature and Oil's strictness (and to the dynamic nature of the source builtin).

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:

oil$ setvar x = 42   # create variable 'x'
oil$ 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 Scope Has Static Checks

Procs are Oil's stricter notion of "shell functions", and they have additional static checks (parse errors):

Procs Don't Use "Dynamic Scope"

Procs are designed to be encapsulated and composable like processes. But Bourne shell functions use a rule called dynamic scope for variable name lookup that 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

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

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 + 1  # look up foo in expression mode
}

Shell Language Constructs Affected

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

Builtins Affected

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

Oil Builtins:

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
setvar d['key'] = 3
setvar d->key = 3               # syntactic sugar for the above
setvar func_returning_list()[3] = 3
setvar x, y = y, x              # swap
setvar x.foo, x.bar = foo, bar

Syntactic Sugar: Omit const

In Oil, but not OSH, you can omit const when there's only one variable:

const x = 'foo'

x = 'foo'  # Same thing.  This is NOT a mutation as in C or Java.

To prevent confusion, x=foo (no spaces) is disallowed in Oil. Use the env command instead:

env PYTHONPATH=. ./foo.py  # good
PYTHONPATH=. ./foo.py`.    # disallowed because it would be confusing

Appendix A: More on Shell vs. Oil

This section may help experienced shell users understand Oil.

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 (Oil doesn't have this)
}

Oil:

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

proc myproc {
  var x = 'L'              # local variable
  const y = 'Y'            # local constant

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

                           # There's no dynamic scope, but you can use
                           # "out params" with setref.

}

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

Related Documents


Generated on Fri May 7 23:42:32 PDT 2021