Guards

Guards are used as a convenient syntax for enforcing user-defined invariants. They can be used as types, but can also enforce arbitrary constraints. Use guards to annotate variable and parameter declarations, function return results, and property definitions. Each guard is asked to approve an incoming value, the specimen, to determine whether to let it pass, coerce it, or reject it. Examples of uses of guards include:

  let x :: Number = 37;
  function f(p :: String, q :: MyType) :: Boolean { ... }
  let o = {a :: Number : 42, b: "b"};

The syntax is:

  Guard :
      :: GuardExpression
  GuardExpression :
      ConditionalExpression

A Guard annotation evaluates its GuardExpression to a value in that scope in the usual manner. (See clarifications below for which scope that is, and when the evaluation occurs.) The GuardExpression should evaluate to an object with a [[Brand]] internal property which is then consulted to determine whether to pass or coerce the specimen or not. In detail:

  1. Let t be the result of evaluating GuardExpression.
  2. If t is not an object or doesn’t have a [[Brand]] internal property, throw a TypeError. The [[Brand]] lookup is done on t only; it does not follow t‘s prototype chain.
  3. Let brand = t.[[Brand]] and save it for the guard check.

Later, when checking a specimen s:

  1. Call brand.coerce(s). This will either return a value (either s or possibly a coerced version of s) or throw an exception.

Creating Guards

The [[Brand]] internal property can be set when creating a guard using whatever syntax or API is approved for setting metaproperties during object creation. Once set, it cannot be changed. Also, user code cannot read it except indirectly via ::.

Optionally we may have an Object.setBrand(guard, brand) method that creates a [[Brand]] internal property on an extensible object guard that doesn’t already have one. It is an error to use Object.setBrand on a guard object that already has a [[Brand]] internal property.

The brand object (the value of the [[Brand]] internal property) can be anything. To be useful, it should be an object with a coerce method.

Guarding Variables

Here, we define the ConstDeclaration and LetDeclaration from block_scoped_bindings to generalize the defining position from an Identifier to a Pattern. This is a somewhat different factoring than the Pattern at destructuring, but the “...” below should include all the Pattern productions there.

We do it this way, rather than extend the ES5 VariableDeclaration, since we are not trying to enhance the deprecated VariableStatement.

  ConstDeclaration :
      const Pattern = AssignmentExpression
  LetDeclaration :
      let Pattern = AssignmentExpression  
  Pattern : ... // 
      Identifier Guard?

The guard expression, if present, is evaluated in left to right order, and therefore before the AssignmentExpression initializer.

A guarded const variable simply inserts an initialize-time check — the value of the brand guard is used immediately to check the initializer and coerce it before assigning it to the variable.

A guarded let variable inserts the corresponding check and coercion on initialization and on all assignments to the variable. Therefore, reading the variable, which is not translated, can only either

  • throw a ReferenceError if read before initialization, or
  • yield a value which the guard considers acceptable.

There intentionally is no way to guard var variables. Doing that would be problematic because var variables can be written before their definitions are evaluated and there can be multiple definitions for the same var variable.

Guarding Parameters and Results

  FunctionDeclaration :
      function Identifier FunctionHead { FunctionBody }
      const Identifier FunctionHead { FunctionBody }
  FunctionExpression :
      function Identifier? FunctionHead { FunctionBody }
      const Identifier? FunctionHead { FunctionBody }
  FunctionHead :
      ( FormalParameterList? ) Guard?

  FormalParameterList :
      FormalParameter
      FormalParameterList , FormalParameter
      // extend for optional and rest parameters
  FormalParameter :
      const? Pattern
  Pattern : ...
      Identifier Guard?

When a FormalParameter variable is annotated with a Guard, the corresponding argument value is first checked and coerced via the guard before binding to the parameter. When a Guard appears after the close paren of a FunctionHead, then whatever value would be returned is checked and coerced via the guard before being returned.

Guard expressions on function parameters and results are not evaluated when the function is called, but rather when the function definition is evaluated. Thus, they are not evaluated in the scope defined by this function, but rather in the scope in which this function definition itself is evaluated. The resulting guard values are therefore captured by the created function.

Guarding Properties

The reason our guard syntax uses “::” rather than the “:” of ES4 is so that we can annotate property definitions in object literals.

  PropertyAssignment : ...
      PropertyName Guard? : AssignmentExpression

As with function parameter and result guards, literal property guards are evaluated in the scope in which the object literal appears, and are evaluated prior to evaluating the object literal.

Here we have a choice of either making the guard an attribute of the annotated property or turning the annotated property into an accessor property whose setter enforces the guard.

Open Issues

  • What, if any, syntax do we want to integrate guards with classes? A lot depends on the choice of class proposal.
  • Should guarded properties be enforced by attributes or be turned into accessors?

See

 
strawman/guards.txt · Last modified: 2011/05/25 21:59 by waldemar
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki