Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keywords with blocks

A keyword can own a block of lines too. There are four kinds, and they differ in one thing: what happens to the block.

BLOCKBIND: the block runs once for each item

The function returns a list (or a range, or a map), and the block runs for each thing in it. This is all FOR is.

FUNCTION countdown(from)
    LOCAL steps = []
    WHILE from > 0
        PUSH steps, from
        from = from - 1
    END WHILE
    RETURN steps
END FUNCTION

BLOCKBIND COUNTDOWN countdown

COUNTDOWN n = 3
    PRINT n, "..."
END COUNTDOWN
PRINT "lift off!"
3 ...
2 ...
1 ...
lift off!

The name before = is the block’s own variable, just as in a FOR. BREAK, CONTINUE and NEXT all work in it.

SCOPEBIND: the block runs once, with something around it

One function runs before the block and makes a value. The function named after END is handed that value when the block is over. In between, anything called inside can ask for it with SCOPE.

FUNCTION list_open(title)
    RETURN {title: title, lines: []}
END FUNCTION

FUNCTION list_add(text)
    PUSH SCOPE("CHECKLIST").lines, text
END FUNCTION

FUNCTION list_close(list)
    PRINT list.title + ":"
    FOR line = list.lines
        PRINT "  [ ] " + line
    END FOR
END FUNCTION

SCOPEBIND CHECKLIST list_open END list_close
BIND ITEM list_add

CHECKLIST "Before launch"
    ITEM "write the docs"
    ITEM "feed the cat"
    IF 2 + 2 = 4 THEN ITEM "tell everyone"
END CHECKLIST
Before launch:
  [ ] write the docs
  [ ] feed the cat
  [ ] tell everyone

Look at what the last four lines turned into: a little language for checklists, with ordinary code allowed in the middle of it. This is how you describe a menu, a level or a song in a script and still have loops and IF to hand.

Keywords that belong to a block

ITEM above works anywhere, which is a waste of a good word: it only means something inside a CHECKLIST. Bind it inside a NAMESPACE and it’s a keyword only where an ALLOW brings it in. That way two blocks can each have an ITEM of their own.

FUNCTION list_open(title)
    RETURN {title: title, lines: []}
END FUNCTION

FUNCTION list_close(list)
    PRINT list.title + ":"
    FOR line = list.lines
        PRINT "  " + line
    END FOR
END FUNCTION

NAMESPACE Checking
    FUNCTION add(text)
        PUSH SCOPE("CHECKLIST").lines, "[ ] " + text
    END FUNCTION
    BIND ITEM add
END NAMESPACE

NAMESPACE Cooking
    FUNCTION add(amount, what)
        PUSH SCOPE("RECIPE").lines, amount + " " + what
    END FUNCTION
    BIND ITEM add
END NAMESPACE

SCOPEBIND CHECKLIST list_open END list_close ALLOW(Checking)
SCOPEBIND RECIPE list_open END list_close ALLOW(Cooking)

FUNCTION chores() ALLOW(Checking)
    ITEM "feed the cat"
    ITEM "water the plants"
END FUNCTION

CHECKLIST "Before launch"
    ITEM "write the docs"
    chores()
END CHECKLIST

RECIPE "Pancakes"
    ITEM 2, "eggs"
    ITEM "1 cup", "flour"
END RECIPE

item = "a variable, out here"
PRINT item
Before launch:
  [ ] write the docs
  [ ] feed the cat
  [ ] water the plants
Pancakes:
  2 eggs
  1 cup flour
a variable, out here

I work out which ITEM a line means from where it’s written, before the program runs. Inside a block, the innermost block wins. A function’s body gets what its own FUNCTION line allows, since it can be called from anywhere. A function you write in place, in the middle of a block, gets what the block allows, the same way it sees the variables around it. Write ITEM somewhere nothing allows it and I’ll say so straight away.

To allow just one keyword out of a namespace, name it with its namespace: ALLOW(Checking.ITEM).

HANDLERBIND: the block is kept for later

The block is kept. It’s handed to the function as a function, to call whenever it likes. GIVES names what the block will be told each time.

listeners = {}

FUNCTION listen(event, handler)
    listeners(event) = handler
END FUNCTION

FUNCTION announce(event, amount)
    LOCAL handler = listeners(event)
    IF handler = NULL THEN RETURN NULL
    RETURN handler(amount)
END FUNCTION

HANDLERBIND WHEN listen GIVES(amount)

WHEN "coin"
    PRINT "picked up a coin worth", amount
    RETURN amount * 2
END WHEN

PRINT "nothing has happened yet"
PRINT "banked", announce("coin", 5)
PRINT "banked", announce("gem", 1)
nothing has happened yet
picked up a coin worth 5
banked 10
banked NULL

TEXTBIND: the block isn’t code at all

Sometimes the lines inside a block aren’t mine to read: a level drawn in characters, a database query, a shader. With TEXTBIND I don’t look at them. I hand them to the function as one piece of text, exactly as you wrote them.

levels = {}

FUNCTION keep_level(name, rows)
    levels(name) = rows
END FUNCTION

TEXTBIND LEVEL keep_level AS "map"

LEVEL "cave"
    #####
    #@.E#
    #####
END LEVEL

PRINT LEN(levels("cave"))
PRINT levels("cave")(6 ... 10)
17
#@.E#
  • The text arrives as the function’s last parameter. Anything written after the keyword fills the parameters before it, as usual.
  • It’s moved left as far as its least indented line, so you can indent the block to match your program.
  • Only END and the keyword’s own words close it. An END IF in the middle is just more text.
  • AS "map" says what kind of text it is. I don’t use that myself. An editor can, to color the text the right way.

Put the TEXTBIND line above the first block that uses it, or in a file you import.

From JavaScript: WHEN "coin" ... END WHEN is on("coin", (amount) => { ... }) with the punctuation taken away.

From Python: A SCOPEBIND is a with block you can build in three lines, and a HANDLERBIND is a decorator that takes a block. A TEXTBIND block is a triple-quoted string that knows which function it’s for.

From Lua: You’d pass a function to get these. Here the block is the function, and the keyword says so.

From BASIC: ON ... GOSUB, WITH and every special block statement you remember are made this way now, by whoever needs one — including you.