Arc Forumnew | comments | leaders | submitlogin
Alphabetical bracket fns
2 points by shader 5506 days ago | 11 comments
As many of you know, I have suggested the option of an implementation of make-br-fn that binds as arguments the alphabetically sorted unbound symbols contained in it's body.

This still may not appeal to you, and you don't have to use it. But as far as I can tell, all the code needed to implement it is:

  (def unbound (x)
    (let l nil
      (ontree (fn (_) (if (and (isa _ 'sym) (~bound _)) (push _ l))) x)
      (sort < (dedup l))))

  (mac make-br-fn (body)
     `(fn ,(unbound body) ,body))
That is significantly shorter than the current implementation of make-br-fn, which takes 117 lines of code, and should work in complete backwards compatibility with both arc and Anarki. Note that I say this without having tested it in all of its current uses, but it seems to work the same way when nested, and it correctly orders _1 - _9. It doesn't work with _nums larger than 9, though, because alpha sort thinks that 1 < 2 (imagine that)

What do you think? I'm pushing the code to anarki as lib/alpha-br-fn.arc



1 point by shader 5506 days ago | link

I found a bug, surprise surprise. Apparently it doesn't seem to count things bound before it in a withs as bound. I presume it is because the nested lets are not actually executed before the bound function is called in the macro expansion. Any ideas on how to get around that? This sounds like a good place for run-time macros ;) That, or a method of having code be more aware of its context; i.e. having the macro-expansion look to see if it was called inside of a form that would bind something (mainly fn). That sounds even harder.

So, apparently it works ok when called directly from the repl, but due to the compile time nature of macros it has trouble with being built into functions or called in other macros.

What does all of the auxiliary code for make-br-fn do? (all of the *mbr functions) They probably don't have much to do with this, but it seems like an awful lot of code just to implement make-br-fn.

-----

2 points by absz 5506 days ago | link

The auxiliary code is responsible for finding all the variables in the code and then finding all the _\d+s (and __) that occur free in the expression. It's probably bulkier than it needs to be---I wrote it with a decent amount of class experience in writing interpreters, but without any real-world experience. The big thing you seem to be missing is expand, which will macro-expand its argument. That would result in the following change:

  (mac make-br-fn (body)
    `(fn ,(unbound body) ,(expand body)))
(And ontree didn't exist when I was writing make-br-fn; that's a very convenient function :) )

-----

1 point by shader 5506 days ago | link

Indeed, ontree is helpful ;)

I've looked at expand; how does it help there? Is that necessary to expand macros inside a bracket function?

I still have no idea how to check binding at compile time; in theory it could know that it was inside a binding expression (fn) but I think that would take a lot of modification to the language, and probably isn't worth it, unless it allows other nifty features.

-----

1 point by absz 5506 days ago | link

Aha, I see. Yes, in this case, expand probably won't help. But it's actually conceptually simple to checking binding (without eval) at compile time; that's what make-br-fns does, after all. What you do is you run expand on the source tree, then just go through and check and see if it's within a fn. This is what all the auxiliary functions for make-br-fns are doing: checking to see what's in an argument list, seeing if variables used are in argument lists, etc. Obviously this breaks if you run, say, (eval '_2), but it works in other situations.

-----

2 points by shader 5506 days ago | link

Wait, how does it know what context it's run in? I can see checking if the symbol is bound underneath it, but what about checking to see if it is bound above? (i.e. br-fn inside a withs) How does that work?

-----

1 point by absz 5506 days ago | link

...Oh dear :-[ It doesn't, and I hadn't noticed that until right now. Apparently, it's not a problem, presumably because nobody ever declares variables called _1 :P Yeah, I don't see a feasible way to do that, unless make-br-fns produces something like

  (eval '`(fn ,(unbound body) ,body))
which is horribly inefficient and clunky.

-----

2 points by rntz 5506 days ago | link

Even the 'eval thing wouldn't work. 'eval evaluates its argument at top-level, so the function would no longer be lexically scoped within its environment. You wouldn't be able to write anonymous closures anymore, just anonymous toplevel functions. Moreover, 'bound checks whether its argument is bound at global scope.

-----

1 point by shader 5506 days ago | link

Unless there was a way for a function to examine its declaration environment. Then we could have a function that crawls up the tree looking for binding statements, and removing those from the list of unbound symbols. I just don't know how hard that would be to add. It could be useful for other things though, like better error messages. If the context were mutable, it could allow macros to do a) really cool things and b) likely very buggy things. But if people knew to expect it, it might be ok.

How would you implement a read-only context, visible during compile time? Is it even possible? In theory, the reader has already read in the other stuff, and parsed it into a list.

-----

1 point by rntz 5506 days ago | link

Allowing a macro to know what lexical variables are bound in its calling environment is perfectly possible, though it would require some modification to ac.scm. In order to translate arc symbols into mzscheme symbols, the compiler already keeps track of what variables are locally bound. So you'd need to modify the compiler so that this list gets passed in as a "hidden parameter" to macros, and make a special form to access it. However, I'd advise against implementing this, because there's an even more unsolvable problem. Even if you fixed it, the following would still break:

    (def square-thunkify (x) [square x])
    (def square (x) (* x x))
The intended meaning of [square x] here is (fn () (square x)). But because 'square is not bound at the time of [square x]'s macroexpansion, even if you did have the more "intelligent" version of 'make-br-fn, it would end up as (fn (square) (square x)). At present, of course, it ends up as (fn (square x) (square x)).

-----

1 point by shader 5506 days ago | link

Hmmm. So how about looking for only unbound single letter symbols? That would cut out all of the predefined functions, and still provide usability (I wasn't going to use it with anything more than x y z and a b c anyway.

If not, I guess there's no point in continuing to pursue this idea, is there ;)

-----

1 point by shader 5505 days ago | link

Maybe we could look at only single letter symbols, or symbols that start with _. That would give compatibility with (most) of the current uses; the only times that it wouldn't work would be when it was used in a function that had a single letter parameter. So, maybe I should just give up then ;) Even though I don't really like _1 _2, etc., I guess it's the most viable option.

-----