Arc Forumnew | comments | leaders | submitlogin
2 points by akkartik 3501 days ago | link | parent

Looks prettier!

I can tell that backslashes are comments. What are slashes and what are parens? See, that's the level I'm at :)



2 points by rocketnia 3501 days ago | link

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.

-----

2 points by akkartik 3500 days ago | link

Why 'defun factorial () n' and not 'defun factorial (n)'?

-----

2 points by rocketnia 3499 days ago | link

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:

  Current return value: 3
  Remaining stack:
  (factorial)
  (times 4)
  (times 5)
  
  Current return value: (nil)
  Remaining stack:
  (one)
  (negate)
  (plus 3)
  (factorial)
  (times 3)
  (times 4)
  (times 5)
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.

-----

2 points by akkartik 3498 days ago | link

How would you represent multiple arguments? Or a variable number of them?

-----

2 points by rocketnia 3498 days ago | link

"How would you represent multiple arguments?"

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:

  (foo/list ...)

-----

1 point by akkartik 3497 days ago | link

Oh so there actually is a constraint on the number of arguments you can pass in! Interesting.

-----