feed

Mar 2005 Braindump: blocks and expressions by Chris Poirier • in Designpermalink

In Braindump: closures I said:

This is why I’m calling Nexxi a block-oriented language. The language is structured as expressions inside blocks, and all blocks are equal, regardless who is executing them. It should make no difference to the programmer whether the thing controlling the block is language-defined or user-defined — everything should work the same, regardless.

after which I said that there were some flaws in that argument. Consider the following code:


if( x == y ):
   doStuff();
else if( x == z ):
   doOtherStuff();
else if( x == null ):
   doNullStuff();
else:
   doDefaultStuff();

How would a user define such a structure? What if, in fact, the user wanted to write eir own if statement? If the language truly does treat language-defined and user-defined control structures as equivalent, then the programmer should be able to do just that.

So what are the problems? Well, there are two.

First, the else if and else clauses are not really seperate block headers — they can’t occur without the if, and two elses can’t occur in succession. In the end, the else if and else blocks are all clauses to the initial if statement.

And second, the language-defined if statement shortcuts — it only evaluates condition expressions until it finds one that produces true. After that, the remaining else if condition expressions are left unevaluated. This is important to consider if the condition expressions have side-effects (as they generally can in Java).

Multi-block functions

The basic pattern for block-accepting methods is some number of non-block parameters, followed by a single block parameter. What if we were to extend that pattern to allow some number of such sets, and used the block parameter’s name as the keyword for every block parameter after the first? For example:



#
# Define a function to simulate the action of an if/else block.

def iffunc( condition as boolean, block as Block, else as Block = nil ):
   if( condition ):
      block.call( condition )
   else if( else ):
      else.call( condition )


#
# And use it in a sentence.

iffunc( x == y ):
   doStuff()
else:
   doOtherStuff()

Of course, in order for this to work, the language has to support named parameters. But that was kind of on my list of things to do, anyway. :-)

And there are larger implications: the language can’t define its core control structures as keywords — it could with if, I suppose, but else would not be a keyword. However, as the Nexxi language structure is regular, this shouldn’t be a big deal for syntax highlighting. But it will probably take some getting-used-to for programmers. In particular, else.call( condition ) looks a little weird.

There’s also a subtle implication of this design: else if isn’t a single entity — it’s an else block that has a single if “statement as its block. By our current policies, something like:


if( x == y ):
   doStuff()
else if( x == z ):
   doOtherStuff()

should really be written:


if( x == y ):
   doStuff()
else:
   if( x == z ):
      doOtherStuff()

This, of course, would be a pain in the ass. Java doesn’t have this problem because it allows control structures to take single statements in lieu of a block. I’ve already discussed why I think that syntax is a bad idea for general use, but I do think it is worth allowing it to be used when the single statement has a block of its own. I would choose to apply it generally, however, so you could run together any number of block headers before supplying the block to the last one. With this in place, we are back to having a working if/else handler.

Extending the pattern

All is not perfect with this solution. While we can implement else if as an if inside an else, this is not generally how programmers think of it. Let’s change the problem slightly, and postulate Ruby’s elsif instead. Now we have a more complicated problem, because it means our iffunc() must take an optional list of elsif conditions and blocks. This would mean that the language must be able to apply patterns from the parameter list during parsing. Here’s an example:


#
# Define a simple class that has two public properties and generated
# machinery for construction and tupleization.

deftype ConditionAndBlock: condition as boolean; block as Block


#
# Define a function to simulate the action of an if/else if/else block

def iffunc( condition as boolean, block as Block, elsif as List of ConditionAndBlock, else as Block = nil ):
   if( condition ):
      block.call( condition )
   else once:
      elsif.each :: current:
         if( current.condition ):
            current.block.call( current.condition )
            break once

      if( else ):
         else.call( condition )


#
# Use iffunc in a sentence.

iffunc( x == y ):
   doStuff()
elsif( x == z ):
   doOtherStuff()
elsif( x == 10 ):
   doSpecialStuff()
else:
   doNothing()

The parameter list essentially defines a structure in which there will be a single boolean parameter iffunc() with a block, followed by 0 or more single boolean parameter elsifs, each with a block, followed by an optional zero parameter else with a block. When the compiler comes across an iffunc block header in the source, it must then validate that the following parameters and blocks match the pattern supplied on the method.

There is some question about how deep the compiler can decipher these patterns, but I’m currently considering that an implementation detail. :-) A bigger problem is what to do when the parameter list doesn’t match the necessary pattern of non-block parameters followed by block parameters. For instance, if the ConditionAndBlock type where to have a third member after the block, it would not qualify for application this way (as the block parameter has to be last in each subunit). I’m not sure how to deal with this.

Further down the rabbit hole

There’s a problem with the elsif example. The way it is written, all the conditions are evaluated before the iffunc() method is called. And that’s not the way if acts (it shortcuts). As another example, consider the language-defined while loop: the condition is evaluated on every pass.

This would seem to demand that we move another thing from the guts of the compiler into the public arena: the ability to obtain an expression closures. I’m thinking the following for an explicit syntax:


def x = 10, y = 12
def expr = &(x + y)
println( expr.value )   # 22
x = 9
println( expr.value )   # 21

But such an explicit syntax doesn’t really solve the problem, because the language does not require it for the while condition (for instance). Perhaps what we need is an Expression type, and the type coercion for other types is to capture the raw expression:


def x = 10, y = 12
def expr as Expression = (x + y)   # Or maybe even: def expr = Expression(x + y)

In this case, an Expression is little more than a zero-parameter closure. In fact, Block would be a specialization of Expression. And with it in place, the following code becomes a reality:


#
# Define a simple class that has two public properties and generated
# machinery for construction and tupleization.

deftype ConditionAndBlock: condition as Expression of boolean; block as Block

#
# Define a function to simulate the action of an if/else if/else block,
# including shortcutting of side effects.  Note that the first condition
# is always evaluated, but the elsif conditions are not evaluated until
# needed (as ConditionAndBlock uses an Expression for its first value).

def iffunc( condition as boolean, block as Block, elsif as List of ConditionAndBlock, else as Block = nil ):
   if( condition ):
      block.call( condition )
   else once:
      elsif.each :: current:
         if( current.condition.value ):
            current.block.call( current.condition.value )
            break once

      if( else ):
         else.call( condition )

#
# Use iffunc in a sentence.

iffunc( x == printAndDouble(y) ):
   doStuff()
elsif( x == printAndDouble(z) ):
   doOtherStuff()
elsif( x == printAndDouble(10) ):
   doSpecialStuff()
else:
   doNothing()

Closing thoughts

This whole line of thinking may be one colossally bad idea. In particular, if Nexxi actually supports this kind of thing, error reporting could become much harder. Let’s say Nexxi statements have a value (as I am considering). Then imagine trying to make sense of the following:


def something( block as Block, else as Block ) as Block:
   if( block.value == 10 ):
      else
   else:
      block

Now, this isn’t likely to happen often, admittedly, but if the language doesn’t reserve control structure names as keywords, then the following is valid:


def else = 21, y = 10
def x = if( y == else ):
           else + 1
        else:
           else

There’s a lot of thinking to be done before I consider this idea workable.

Related Links

in Design:
on site:

Discussion: 2 Comments

Jump to comment form | comments rss | trackback uri
  1. Jeff Brown 2009-01-11 17:04

    The idea of using named arguments as a way to specify clauses in user-defined control structures has occurred to me before.

    Unfortunately it never seems nearly as clean as the SmallTalk selector syntax . . . 


  2. Chris Poirier 2009-01-12 1:49

    At this point, I’m afraid I’ve forgotten all of the Smalltalk I ever knew.  :-(   I’ll have a look at the selector syntax, one of these days.  And FYI, Nexxi is dead — some of it may resurface, if I ever have time to do Rethink, but, even that’s starting to look like a long shot.


Leave a comment

Markdown: The kinds of formatting markup you'd use in an email will probably work here. For more details on what you can do, check out the Markdown docs.


Site copyright 2007-2008 Chris Poirier.       Powered by Wordpress.       Entries RSS Comments RSS Validate Log in