I wrote a library called Chancery[1] for procedurally generating strings (and other data). It's inspired by Tracery[2] but takes advantage of macros to make it easier to read and feel more like part of the language. I use it to write stupid Twitter bots like https://twitter.com/git_commands and https://twitter.com/rpg_shopkeeper for fun.
As an example let's say we want to generate a message about the loot we receive from a monster in a fantasy, D&D-style story. Maybe we'll start with some random weapons:
(chancery:define-string weapon-type
"sword"
"spear"
"lance"
"flail"
"mace")
(weapon-type) ; => "mace"
(weapon-type) ; => "sword"
This expands like so: (macroexpand-1
'(chancery:define-string weapon-type
"sword"
"spear"
"lance"
"flail"
"mace"))
; =>
(DEFUN WEAPON-TYPE ()
(CASE (CHANCERY::CHANCERY-RANDOM 5)
(0 "sword")
(1 "spear")
(2 "lance")
(3 "flail")
(4 "mace")))
This simple case actually could be done just by delaying evaluation, as long as we get every body clause as a separate thunk. Now let's define a rule for generating the material of a weapon: (chancery:define-string (weapon-material :distribution :weighted)
(100 "iron")
(40 "steel")
(5 "silver")
(4 "gold")
(1 "adamantine"))
This will generate the materials according to a weighted distribution, and macroexpands to: (DEFUN WEAPON-MATERIAL ()
(CASE (CHANCERY::WEIGHTLIST-RANDOM #)
(0 "iron")
(1 "steel")
(2 "silver")
(3 "gold")
(4 "adamantine")))
This case needs more than just delayed evaluation. If you receive `(100 "iron")` as an opaque thunk, where all you can do is evaluate it, there's no way to pull out the weight and body components.If we add a few more rules, we can see more cases where we need to go beyond delayed evaluation:
(defun currency-amount ()
(+ 10 (random 100)))
(chancery:define-string (currency-type :distribution :zipf)
"copper"
"silver"
"gold"
"platinum")
(chancery:define-string loot
#((weapon-material weapon-type) chancery:a)
(currency-amount currency-type "coins"))
(chancery:define-string discovery
("You open the chest and find" loot :. ".")
("You find" loot "in the monster's hidden stash.")
("You find nothing but dust and cobwebs."))
(discovery) ; => "You find nothing but dust and cobwebs."
(discovery) ; => "You find an iron sword in the monster's hidden stash."
(discovery) ; => "You find 61 copper coins in the monster's hidden stash."
(discovery) ; => "You open the chest and find a steel sword."
Macroexpanding the last one: (DEFUN DISCOVERY ()
(CASE (CHANCERY::CHANCERY-RANDOM 3)
(0 (CHANCERY::JOIN-STRING "You open the chest and find"
(PRINC-TO-STRING #\ )
(LOOT)
"."))
(1 (CHANCERY::JOIN-STRING "You find"
(PRINC-TO-STRING #\ )
(LOOT)
(PRINC-TO-STRING #\ )
"in the monster's hidden stash."))
(2 (CHANCERY::JOIN-STRING "You find nothing but dust and cobwebs."))))
Here we can see the macro walking the lists and doing different things to each element: strings are included raw, symbols are turned into function calls, and the special keyword :. suppresses the usual joining space character inserted between everything. There's also some special handling of vectors, in the LOOT example, which I won't go into. This is more than just delayed evaluation — we're inspecting the actual structure of the code received by the macro at macroexpansion time. If all we had were an opaque thunk that we could evaluate later, we couldn't do this.Delayed evaluation is enough for certain kinds of abstraction, like writing basic control structures, but isn't as powerful as full macros. Macros let you transform arbitrary code into other arbitrary code using the full power of the language.
[1]: https://sjl.bitbucket.io/chancery/ [2]: http://tracery.io/