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
ENDand the keyword’s own words close it. AnEND IFin 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 WHENison("coin", (amount) => { ... })with the punctuation taken away.
From Python: A
SCOPEBINDis awithblock you can build in three lines, and aHANDLERBINDis a decorator that takes a block. ATEXTBINDblock 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,WITHand every special block statement you remember are made this way now, by whoever needs one — including you.