Skip to content

TM026 block shorthand

Joe Politz edited this page Apr 8, 2016 · 1 revision

Explicit Block Shorthand

Currently in Pyret, each block can hold multiple whitespace-separated expressions, and blocks typically evaluate to the value of the last (check bocks are an exception).

The vast majority of blocks hold a single expression. Pedagogically, it's not frequent that a student intends to write two expressions in a block until they are a pretty savvy programmer. If they do accidentally, it's all of a sudden necessary to explain that Pyret evaluated their first expression and threw out the value, which is why they're seeing the behavior they're getting.

It would be nice to restrict blocks by default to have only a (possibly empty) sequence of let-bindings followed by a single expression. Of course, this restriction on its own is quite severe; it directly impacs print debugging, having a sequence of when-based error checks at the front of a function, and writing stateful algorithms that need to sequence operations.

This proposal aims to reconcile these two desires.

A Restriction and an Addition

This proposal has two parts. For the first, the goal is to logically introduce a new production—body—which is as described above:

body := (let-expr *) binop-expr

This new body production will replace block in the following contexts:

  • fun-expr
  • multi-let-expr
  • letrec-expr
  • type-let-expr
  • when-expr
  • lambda-expr
  • method-expr
  • obj-field (in the method case)
  • if-expr
  • else-if
  • if-pipe-expr (AKA "ask")
  • if-pipe-branch
  • cases-branch
  • for-expr

It does not replace block in the following contexts:

  • check-expr
  • where-clause
  • user-block-expr
  • program

Since changing the grammar in this way will simply cause poor parse error messages, we will instead keep the block production in place, and check for this shape in well-formedness, where we can give a much more useful error message.

Note that if this was all we did, we could still write sequencing code by using block: ... end. So a function that needed to mutate a variable and return value could do so:

var x = 0
fun gensym(s):
  block:
    x := x + 1
    s + num-to-string(x)
  end
end

The extra typing and indentation, especially to add a call to a debugging helper, is quite onerous. It would be great to have a way to not need to introduce block: ... end into all of these contexts.

In order to recover the ability to use blocks with sequences of expressions, we will augment all of the forms above with an optional additional keyword that indicates all direct sub-blocks may have sequences of expressions, and are not limited to body. So instead of the above, we would write:

var x = 0
fun gensym(s) block:
  x := x + 1
  s + num-to-string(x)
end

With annotations:

var x = 0
fun gensym(s :: String) -> String block:
  x := x + 1
  s + num-to-string(x)
end

(The keyword seq as opposed to block has also been proposed.)

Some other examples, which would all be well-formedness errors with block omitted:

ask block:
  | key == "left" then:
    print(key)
    move-player-left()
  | key == "right" then:
    move-player-right()
end
for map(elt from lst) block:
  store-in-cache(elt)
  transform(elt)
end
cases(List<Number>) l block:
  | empty => empty
  | link(f, r) =>
    when f == "sentinel":
      raise("Sentinel value detected, that's an error")
    end
    link(transform(f), r)
end

These additions would happen in the grammar, and would require adding flags or new variants for all of the relevant AST types so that well-formedness could make the right decisions. Some form of flag is probably the best to avoid bloating the size of ast.arr more than necessary.