I reconsidered what kinds of macros to define, and I found a sweet spot this time! Now I can boil down the examples to this:
(defun factorial () n
(times n /call-new factorial () /plus n /negate/one))
(defun inc-if-nat () preds
(each-caselet pred preds
nil () succ.pred
succ (pred-pred) succ.pred
pred))
(defun inc-to-everyone () everyone
(each-case everyone yep (a)
/each-case everyone yep (b)
(link b inc-if-nat.a)))
Here's the same code with thorough comments:
\ Define a type (factorial) that can be called with argument `n`.
(defun factorial () n
\ Since the (factorial ...) macro hasn't been defined yet, we spell
\ it out with (call-new factorial () ...) instead.
(times n /call-new factorial () /plus n /negate/one))
\ Define a type (inc-if-nat) that can be called with argument `preds`.
(defun inc-if-nat () preds
\ For each singleton subset `pred` in `preds`...
(each-caselet pred preds
\ If it's a natural number, return its successor.
nil () succ.pred
succ (pred-pred) succ.pred
\ Otherwise, return it unmodified.
pred))
\ Define a type (inc-to-everyone) that can be called with argument
\ `everyone`.
(defun inc-to-everyone () everyone
\ For each (yep val) in `everyone`...
(each-case everyone yep (a)
\ For each (yep val) in `everyone`...
/each-case everyone yep (b)
\ Connect one channel to the incremented value(s) of the other,
\ and return the empty set.
(link b inc-if-nat.a)))
When using these new macros, every single macro call and subexpression is in a full-powered computation context. This way the programmer doesn't have to convert between time-limited contexts and full-powered contexts using (return ...) and save points. And the macros interpret raw symbol arguments as local variables, so the programmer can say succ.pred rather than (succ/return local.pred).
Meanwhile (each-caselet ...), (each-case ...), and (link ...) make it convenient to do iteration, destructuring, and continuation calls which are usually syntactically restricted to the beginning of a function. These macros simply generate a new function with a gensym name to host the operation.
Overall, this amounts to a really clean functional style, and I'll have to stop saying it's cumbersome to program in Tenerezza. :) These are still just macros, and they're not even code-walking macros, so all the Tenerezza primitives are close at hand when that level of control is needed.
That's the kind of question I want to hear. :) Does this help any?
Here are the relevant syntaxes of the Era reader:
\ A backslash followed by a space starts a line comment.
\ This is the string "this-is-a-string". This string representation
\ can only contain certain characters, but those are all the
\ characters we need for the examples at hand.
this-is-a-string
\ This is a three-element list. Its elements are the string "a", the
\ string "b", and a list of two strings.
(a b (c d))
\ All of the following examples are equivalent to the above list:
\ Forward slashes are just like ( but they don't consume ).
(a b /c d)
\ Dots abbreviate two-element lists.
(a b c.d)
\ Whitespace around parentheses, forward slashes, and dots is ignored.
( a b ( c d ) )
(a b/ c d)
(a b/c d)
(a b
/c d)
(a b c . d)
In Tenerezza code using the macro system, a list that begins with a string is usually a macro call, and it behaves however that macro wants it to behave.
In raw Tenerezza code (without macros), the grammar is very regular. Every list in Tenerezza code is a special form (a list beginning with a particular string), and Tenerezza's special forms always have a particular number of subexpressions. Each subexpression has a particular grammar nonterminal that describes what forms can appear there.
For instance, whereas you might expect most things to be "expressions," Tenerezza draws a distinction between get-exprs and env-exprs, among other things. A get-expr returns first-class values, whereas an env-expr is dedicated to creating (second-class) collections of named values.
Consider this Tenerezza code:
(singleton yep (env-cons val (local x) (env-nil)))
The (singleton ...) special form is an example of a get-expr. This special form takes two arguments: A stack frame name (in this case "yep"), and an env-expr to specify what the variables map to in this instance of that stack frame. Overall, the (singleton ...) expression represents a singleton set containing the specified stack frame instance.
The (env-cons ...) special form is an example of an env-expr. It takes three arguments: An environment key (in this case "val"), a get-expr providing some first-class value to associate with that key, and an env-expr representing the rest of the environment. Overall, this special form represents the same thing as the env-expr, but modified to add the given binding.
The (local ...) special form is an example of a get-expr. It takes one argument: A local variable name (in this case "x"). It represents the value of that variable.
The (env-nil) special form is an example of an env-expr. It takes no arguments. It represents an empty environment.
I might agree with you there, but I'll take it as a question rather than a suggestion. :-p
The "n" is the function's argument, and () is the list of variables in the stack frame's environment.
In most Scheme-like languages, a visualization of the stack would be several levels of function calls. Every expression to the left would have been evaluated already, and every expression to the right would still be waiting. For instance:
Current return value: 3
Remaining stack (starting with the freshest activation):
(plus _ #suspended:(negate (one (nil))))
(factorial _)
(times 3 _)
(times 4 _)
(times 5 _)
Staccato and Tenerezza have a more constrained model of the stack. In particular, the stack can never contain suspended code. As such, the program never quite lands in the above state, but I'll show the states before and after it:
This makes stack frames very simple. They're just tagged collections of values. This meshes into the rest of the language: To call a function is simply to push it onto the stack and to proceed with computing its argument. The (fn ...) syntax is just sugar for writing (def ...) and constructing a stack frame using variables from the surrounding lexical scope.
---
Incidentally, I had a bug in the abbreviated versions of factorial:
(defun factorial () n
- (times n /call-new factorial () /plus n /negate/one))
+ (times n /call-new factorial () /plus n /negate/one/nil))
These fancy macros can be called in two ways, either to construct a value or to call it as a function:
(cons a b) \ creates a (cons car cdr) first-class stack frame
(cons a b c) \ gets the result of (call (cons a b) c)
(one) \ creates a (one) first-class stack frame
(one (nil)) \ gets the result of (call (one) (nil))
When I just wrote (one), I was generating a (one) stack frame, rather than actually retrieving the proper representation of 1. This could still work if the (one) stack frame was a proper representation of 1, but I intended the representation to be flexible.
Right now I'm juggling two ways to pass multiple arguments.
For globally defined functions, such as the ones in the examples, I construct a data structure like (plus term) which holds all but one of the "arguments," and then I call it with the final argument.
For higher-order functions, the call site is using a function that was constructed elsewhere, so it has to use some other method to cram in more than one argument. I think my favorite approach right now is to define a data structure specifically to carry these arguments. This way if the program is refactored and the arguments change, the new code can still be backwards compatible with old stack snapshots that mention the old data structure. All it takes is for the new code to have a conditional where it handles both the old tag and the new one.
The second calling convention could work for global functions too, and it might be a lot less awkward for documentation; when I describe a two-argument function called (plus term) or a four-argument function called (my-func a b c), it looks pretty weird. :) Unfortunately, it really commits us to having messy stack snapshots full of macro-generated steps:
Current return value: (one-args)
Remaining stack:
(one)
(gs-12301) \ will construct (negate-args _)
(negate)
(gs-12302 3) \ will construct (plus-args 3 _)
(plus)
(gs-12303) \ will construct (factorial-args _)
(factorial)
(gs-12304 3) \ will construct (times-args 3 _)
(times)
(gs-12304 4) \ will construct (times-args 4 _)
(times)
(gs-12304 5) \ will construct (times-args 5 _)
(times)
Hmm, maybe this isn't much worse than before. Factorial is a funny example because I can put all the function calls in the last argument, which makes the stack look nice. If I put them all in the first argument instead, we'd see some macro-generated save points:
(defun factorial () n
(times (call-new factorial () /plus (negate/one/nil) n) n))
Current return value: (nil)
Remaining stack:
(one)
(negate)
(gs-12305 3) \ will call (plus _ #suspended:n)
(factorial)
(gs-12306 3) \ will call (times _ #suspended:n)
(gs-12306 4) \ will call (times _ #suspended:n)
(gs-12306 5) \ will call (times _ #suspended:n)
Real code is likely to have a handful of save points per function, so the stack will usually look something like that.
Oh. Actually, if I passed arguments in the form of data structures, that bizarro-factorial would look a little nicer on the stack. You can actually tell that it's going to call (plus) and (times) even if you don't look up the definitions of gensyms:
Current return value: (one-args)
Remaining stack:
(one)
(gs-12301) \ will construct (negate-args _)
(negate)
(gs-12302 3) \ will construct (plus-args _ #suspended:n)
(plus)
(gs-12303) \ will construct (factorial-args _)
(factorial)
(gs-12304 3) \ will construct (times-args _ #suspended:n)
(times)
(gs-12304 4) \ will construct (times-args _ #suspended:n)
(times)
(gs-12304 5) \ will construct (times-args _ #suspended:n)
(times)
Okay, I might settle on using this calling convention for everything in this convenience layer. :) Thanks for prompting me to think about this.
I guess I'll probably use a slightly tweaked set of macros in future examples, taking this calling convention into account.
---
"Or a variable number of them?"
One option is to pass a cons list. Assuming there's a (list ...) macro, it's easy to call (foo ...) with a list: