Arc Forumnew | comments | leaders | submit | bogomipz's commentslogin
2 points by bogomipz 4877 days ago | link | parent | on: New wart interpreter: lisp-1, fexprs

About thunks; this is actually a situation where statically typed languages have the potential to do something interesting that is hard in dynamically typed systems.

If foo takes an argument that should be a zero arity function that returns a number, and you write:

  (foo (+ x y))
The compiler could recognize that the expression you used for the argument can be converted into a function with the correct signature. I think I have heard of languages on the JVM that do this, not sure which.

Also note that Smalltalk-80 is very much designed around the principle of thunks. E.g:

  a > b ifTrue: [Transcript show: 'A is great!']

-----


The problem with using function values is that the macro then remembers the original implementation even if the function is redefined.

-----

1 point by aw 4954 days ago | link

Yes, once a macro has been used then redefining helper functions won't affect code which has already been compiled. (New or reloaded code which uses the macro will get the redefined helper function).

It makes the helper functions act like macros in that way: redefining a macro won't affect code which has already been compiled either.

-----

3 points by rocketnia 4954 days ago | link

I like your approach a lot, aw, but I too noticed this issue. I think it can be overcome just by changing the idiom slightly.

First, some definitions:

  (def fn-latemac (mac-getter)
    (annotate 'mac
      (fn args `(,(mac-getter) ,@args))))
  
  ; Not to be hypocritical, let's implement 'latemac as if by using
  ; 'latemac.
  (mac latemac (globalvar)
    `(,(fn-latemac:fn () fn-latemac) (fn () ,globalvar)))
Now we can define your example 'foo and 'bar like this:

  (mac foo ()
    `(,latemac.do
       (,latemac.prn "this is macro foo expanding into bar")
       (,latemac.bar)))
  
  ; Oops, now let's define 'bar. Go go, late binding!
  (mac bar ()
    `(,latemac.prn "yo, this is macro bar"))
As you can see below, this already works in the current version of ar. ^_^

(For future reference, I'm using https://github.com/awwx/ar/commit/c375d77bd2dbf6ca8850fda5f6.... )

  arc> (foo)
  this is macro foo expanding into bar
  yo, this is macro bar
  "yo, this is macro bar"
  arc>
    (def bar ()
      (prn "hi, this is bar"))
  *** redefining bar
  #<procedure:g1590>
  arc> (foo)
  this is macro foo expanding into bar
  hi, this is bar
  "hi, this is bar"
Essentially, these are hygienic macros at this point. They use the syntactic closure technique for hygiene, in this case implemented using regular old lambda closures. ^_^ A syntactic closure approach has undesirable properties like making the expansion hard to code-walk or serialize, but those activities seem rare enough anyway, probably because they're already difficult. I think this'll do just fine as far as modules go.

-----

2 points by bogomipz 5054 days ago | link | parent | on: Why so proper, alist?

Agreed. I always thought of associations as pairs, not lists of length 2.

-----


Ever since I first started learning the fundamentals of Lisps, I always wondered why dot notation couldn't be used interchangibly with 'apply, eg:

  (def foo (x . xs)
    (bar x . xs))
instead of:

  (def foo (x . xs)
    (apply bar x xs))
The first snippet above seems much more intuitive to me than the second.

-----

5 points by aw 5066 days ago | link

Assuming you don't change the syntax of dotted lists, a problem is that you wouldn't be able to use an expression for "xs".

The dot notation creates a cons for you, and a cons creates a regular list if its second argument is nil or another cons (which itself is a regular list).

Thus

  '(a . (b))
works like

  (cons 'a '(b))
which in turn is like

  '(a b)
which you can see for yourself:

  arc> '(a . (b))
  (a b)
So an expression like

  (bar x . (foo))
that we'd want to work like

  (apply bar x (foo))
is parsed by the reader like this:

  arc> '(bar x . (foo))
  (bar x foo)
and by the time the compiler sees the expression there's nothing there to tell it that a dot was used.

Of course, if you change the reader so that a dot doesn't produce a dotted list but instead creates a token for the compiler to see, then you can do whatever you want... though you'd also need to extend the compiler to handle the dot notation in the other places which is currently handled by the reader.

-----

2 points by rocketnia 5066 days ago | link

Great point.

For what it's worth, 'apply could be moved to arc.arc:

  (def apply (self . args)
    (zap rev args)
    (zap [rev:+ (rev:car _) cdr._] args)
    (self . args))
I think the number of times I use 'apply with a variable in the final position is enough for the syntax to be relevant to me... but then I'd be likely to refactor (a b c . d) into (apply a b c (something-else)) and vice versa all the time, which would be a bit of a pain. Then again, it's balanced against the cost of refactoring (a b c) into (a b c . d) and vice versa....

by the time the compiler sees the expression there's nothing there to tell it that a dot was used.

In Racket's syntax there is a way, I think. It makes syntax more complicated than just its conses, though. You'd have to use an Arc variant of syntax-quasiquote (or just syntax-quasiquote) to construct it in macros.

Meanwhile, the (cdr `(foo . ,<scheme-expr>)) quirk would become more of a bug than it already is. But then I assume whatever Arc hack implements this syntax would also have '$ built in, so there's probably no point to keeping the quirk around.

-----

1 point by evanrmurphy 5066 days ago | link

I like that idea.

I don't see a way you could use it if xs were the only argument, i.e. a way to rewrite `(apply bar xs)`, but in that case apply is more paletable anyway:

  (def foo xs
    (apply bar xs))

-----

3 points by rocketnia 5066 days ago | link

  (def foo xs
    (bar . xs))

-----

1 point by evanrmurphy 5066 days ago | link

I see, so `'(bar . xs)` is a cons cell and `(bar . xs)` is equivalent to `(apply bar xs)`.

-----

1 point by rocketnia 5066 days ago | link

I've wondered that too. XD

-----


How about a loader that renames all global names to gensyms by default, but with the ability to preserve the ones you want?

  (load-hidden "arc.arc"
    w/infile w/outfile expander)
This basically means you have to declare every global you want to make available from the file you load. You could even specify what names to give them:

  (load-hidden "arc.arc"
    w/infile w/outfile expander.w/close)
Nested loads would be tricky though. The outer would have to hide names that are preserved by the inner, except when they are wanted of course.

Alternatively, the loader could add a prefix (the file name?) rather than rename to gensyms.

-----


Just a couple of comments on the watch function:

1. Shouldn't the last line be (watch file-name delegate) so that it continues to watch after the first update? Or should that be optional?

2. It might be a good idea to pass file-name to the delegate. Not strictly necessary, and doesn't give any benefit in this example, but it could mean that you sometimes don't need to use a fn as the delegate. If you didn't want to pr "reloaded" for instance, you could just do (watch file-name load).

3. Is the hash table really necessary? Here is a version without it:

  (def watch (file-name delegate)
       (let ts mtime.file-name
         (while (is ts mtime.file-name)
                (sleep 2))
         (delegate)))

-----

1 point by hasenj 5115 days ago | link

I put the call to watch as the last line inside the file itself.

And the hash table is there because I initially wanted to watch multiple files at the same time. But I suppose if the main file in the web app loads the other files, then it's not needed at all.

The other thing I did is running the (asv) thread only once by checking for an unbound symbol, so that when the file is reloaded it doesn't run the server again.

  (def srvr ()
    (if (no (bound 't*))
      (= t* (thread (asv)))))

  (srvr)

-----

1 point by bogomipz 5113 days ago | link

Ah, so loading the file makes it watch itself. You need to do the load in a thread then?

I still fail to see how the hash table helps since my version above should do exactly the same as yours. I would see the point if you made a thread check all files in the table, and made the first call to watch launch the thread, while subsequent calls just add entries to the table.

-----

3 points by hasenj 5113 days ago | link

Well actually, I'm putting the (asv) call in a thread and making it so that reloading the file doesn't restart the server:

  (mac set-once (s v)
       `(if (no:bound ',s)
          (= ,s ,v)))

  (set-once t* (thread:asv))
Watching for changes is not running in a thread, it's just the last line in my "web.arc"

And you're right, the hash-table is not needed at all. That was a premature design.

I actually re-wrote it a bit:

  ; watch the result of (func) for change
  ; and run 'on-change' when value changes
  (def watch-fn (func on-change)
       (let init (func)
           (while (is init (func))
              (sleep 2))
       (on-change)))
  
  ; watch the value-exp expression for changes
  ; and run on-change-exp when it changes
  (mac watch (value-exp on-change-exp)
       `(watch-fn (fn() ,value-exp) (fn() ,on-change-exp)))
  
  (def watch-file (file-name deleg)
       (watch (mtime file-name) (deleg file-name)))
  
  (def auto-reload (file-name)
       (prn "w/auto-reloading " file-name)
       (watch-file file-name load))
  
  (auto-reload "web.arc")

-----

4 points by bogomipz 5128 days ago | link | parent | on: defrule

With defrule and in akkartik's python-like keyword arguments (http://arclanguage.com/item?id=12657), argument names become significant. Normally you're free to change the names of arguments, for instance because you didn't put enough thought into them first time around. Is it a problem to start treating argument names as a set-in-stone kind of thing? I mean, you never know which args someone is going to rely on not changing, so the names become part of the public interface.

-----

4 points by aw 5128 days ago | link

An alternative to public interfaces is to not treat any part of your code as set-in-stone, but instead to improve your code whenever you think of something better -- such as using better argument names as you come up with them.

The traditional assumption is that if I'm using your code and you change it, I'll find it hard and difficult to change my code to use your new version, and so you should put time and effort into coming up with things like stable public interfaces -- things that you wouldn't otherwise do for yourself, but things that you think will help me.

However, using a powerful language such as Arc I've found easy to change my code to make it work with new releases. For example, the home page of arclanguage.org says "Expect change. Arc is still fluid and future releases are guaranteed to break all your code." Which sounds pretty dire. Yet in practice I've easily rebased my code on every release of Arc since arc0.

Easier in fact for me than my experience using other languages that strive to maintain compatibility with earlier versions -- their libraries end up having so many layers and compatibility modes that it becomes tedious for me to figure out what they're doing and where their bugs are.

-----

3 points by bogomipz 5139 days ago | link | parent | on: List of characters vs string

The arguments against strings as lists are that they're going to be incredibly slow and take up many times more memory. Characters will in practice be cons cells, which means they take up something like 8 bytes each, are allocated individually, and generate lots of work for the garbage collector. As others have mentioned, accessing the nth element of a linked list is very slow, and even iterating the list is slower than for arrays.

I would much rather prefer string to be a subtype of list, with the functions that actually access the elements dispatching on the type. This would have all the benefits without the performance loss.

The missing bits to enable this in a general way are transparent type/annotate, generic functions, and hierarcical types. By transparent I mean that annotate should not return a cons cell with the original value inside. The runtime should have support for marking each value with a type.

-----

3 points by bogomipz 5140 days ago | link | parent | on: A better syntax for optional args

I like it. Could also be done with less parentheses but explicit nil, like rocketnia did in the ".o" approach:

  (a b 'opt c 1 d nil e)
On the other hand, the parentheses can be reduced with ssyntax when the default is a literal:

  (a b 'opt c.1 d e)

-----

1 point by akkartik 5140 days ago | link

I like this! How about using ? as the separator?

  (a b ? c 1 d nil e nil)

-----

4 points by waterhouse 5139 days ago | link

I dislike the idea of declaring anything to be a separator--and therefore a reserved, special word--that isn't already illegal to put in a parameter list.

...egad. It turns out that it is not just legal, but it works fine, to use "quote" as a variable name just like any other, in Arc and Common Lisp and Racket.

  > (let ((quote 2)) quote) ;Racket
  2
  > (define quote 10)
  > quote
  10
... So much for brilliant ideas. Hmm. On the other hand, if you actually do that, then Bad Things happen:

  > (define quote 10)
  > '(1 2 3)
  procedure application: expected procedure, given: 1; arguments were: 2 3
Meanwhile, CL does disallow defining a function named "quote". It also disallows using 'flet to create a local function named "quote".

Well, what do you think of forbidding people to rebind quote (locally or otherwise)? I think it's acceptable. quote is a fundamental part of Lisp. If it is rebound, then either that will screw up quoted things, or the Lisp parser will handle (quote blah) forms specially, in which case rebinding quote to a function and attempting to call it will fail (you'll just quote the arguments). In other words, either this will fail to return 12:

  arc> (let quote [+ _ 2] (quote 10))
  12
Or this will cause a presumably unexpected error when '(1 2 3) is interpreted as something other than a quoted list:

  arc> (let quote [+ _ 2] '(1 2 3))
  Error: "application: bad syntax in: (1 2 3 . nil)"
I think both of these possibilities suck[1] and, for the purposes of formally specifying Arc, we should say "This is not supported; we recommend that an implementation throw an error when encountering an attempt to locally or globally rebind quote." I probably wouldn't make it illegal, but I'd make it print something like "COMPILER-WARNING: WTF, you're trying to redefine quote? This will probably not end well."

So, if using "quote" as a parameter is officially unsupported, then this officially makes room for "quote" to be used as a special marker in parameter lists. When a program that parses parameter lists encounters (quote blah), it should stop and say "Aha, this is not a legal parameter. What now?" And at this point we can give it whatever desired features in a nice, modular way.

In official Arc, we would have, say, "If blah is 'opt or 'o or 'optional, then proceed to interpret optional arguments." Then akkartik might add, "If blah is 'key, then proceed to interpret keyword arguments", and aw might add "If blah is 'as, then proceed to interpret coerced arguments", and these would be totally compatible extensions to Arc, as long as they didn't choose the same name.

I do think this is the way to go.

[1]A "Lisp-2 function/variable namespace separation" buff might say at this point, "Aha! See, with the namespace separation, this isn't a problem; you can use quote as a variable all you like and it creates no problems." Retort: "I might just as well want to locally create a quote function with flet, and then you have a problem. (And if your language doesn't let you locally bind functions, then it sucks.)" Example case:

  (flet ((quote (x)
           (format t "~S~%That's what she said!" x)))
    ...)

-----

1 point by akkartik 5138 days ago | link

Yeah you make a lot of sense.

I've been thinking more about making the arc transformer ('compiler' seems excessive) simpler and easier to add hooks into. I don't want to hardcode keywords as non-overrideable; instead I want new keywords like coerced to be easily added to the transformer.

-----

2 points by bogomipz 5140 days ago | link | parent | on: 1000 Days

What a coincidence! My account was a 1000 days old the day I read your post :) At first I wondered if this was a bug in news or something, then I noticed that your account acutally said 1003 now.

-----

More