Arc Forumnew | comments | leaders | submitlogin
Making keyword args read better
6 points by akkartik 5119 days ago | 15 comments
Here's an old favorite of mine (http://arclanguage.org/item?id=10692):

  (mac paginate(url numitems ? nextcopy "next" prevcopy "prev" . body)
    `(withs (start-index (arg req "from")
             end-index (+ start-index ,numitems))
        ,@body
        (nav ,url start-index end-index)))
In vanilla arc you'd have to specify all optionals if you wanted to give a body, which sucks.

With keyword args (http://arclanguage.org/item?id=12657) I can now say:

  (paginate "/foo" 10
    :body
      ..)
Much better. But I've started to notice that when macros have both optionals and body args the body is rarely unspecified. It seems unfortunate to need the keyword arg for the common case.

My first solution was to make :do a synonym for :body. Still the extra token, but somehow I don't notice it as much. It also reduced my hankering for racket's fully-flexible keyword args (http://arclanguage.org/item?id=12944).

Now I realize[1] that you can indeed eliminate the extra token: just make rest args take precedence over optional args. In wart (http://github.com/akkartik/wart) I can now say:

  (paginate "/foo" 10
    ..) ; body
Rest args are now greedy and eager; anything left over after required args goes to them. To add optional args you have to explicitly specify keywords:

  (paginate "/foo" 10 :nextcopy "newer →" :prevcopy "← older"
    ..)
Behavior in the absence of rest args should not change.

-

[1] Credit goes primarily to evanrmurphy, for resurfacing the design of optional args (http://arclanguage.org/item?id=13029), and for the idea of making all variables optional (http://arclanguage.org/item?id=13030). When I thought of creating a new category of lazy optionals (http://arclanguage.org/item?id=13056) I realized I already have two kinds of optionals; I never bothered adding any error checking to my parsing and wasn't actually checking that functions got all their required args :) Optionals were providing merely the ability to provide a different default value. Now they're pulling more weight.

Here's my current definition of updating, with example uses:

  (mac updating(place ? with t unless 'iso . body)
    (w/uniq rhs
      `(let ,rhs ,with
         (unless (,unless ,place ,rhs)
           (= ,place ,rhs)
           ,@body))))


  (mac firsttime(place . body)
    `(updating ,place
        ,@body))

  (updating (uvar u last-login) :with (date) ; extra token; seems useful
    (++ karma.u))

  (ret max 0
    (each elem '(1 3 2 6 5)
      (updating max :with elem :unless >=
        (prn "max now: " max))))


1 point by rocketnia 5119 days ago | link

You have a nice system going here. ^_^

Do you plan to include support for functions that accept arbitrary keywords? How about a version of 'apply that can pass keywords too? These are useful for functions that just want to pass most of their arguments along, as in this extreme example:

  ; Build a function whose behavior imitates the behavior of
  ; calling a nonconstant expression, such as a potentially
  ; hackable global variable.
  (def fn-late (body)
    (fn args (apply (do.body) args)))
  (mac late body
    `(fn-late:fn () ,@body))

-----

2 points by akkartik 5118 days ago | link

Thanks :)

Hmm, I'm afraid I don't understand. Could you give an example where you would use late, and why keyword args would help?

-----

3 points by rocketnia 5118 days ago | link

Suppose someone defines utilities this way:

  (= first car)
  (= rest cdr)
  (= past rev:rest:rev)
  (= last first:rev)
If someone redefines 'car and 'cdr to work with strings, 'first and 'rest won't reflect the change. If someone redefines 'first and 'rest, I think 'past and 'last won't reflect those changes either (but I'm not sure right now). They're not hackable. Here are two ways to make them hackable again:

  (= first [car _])
  (= rest [cdr _])
  (= past [rev:rest:rev _])
  (= last [first:rev _])
  
  (= first late.car)
  (= rest late.cdr)
  (= past late.rev:late.rest:late.rev)
  (= last late.first:late.rev)
I prefer the first way, but you never know. ^_^

Now suppose you want to use 'late to make a function that delegates to a function that takes keyword arguments. Will the technique 'late uses, [fn args (apply _ args)], still be aufficient? I suspect it would need to change to [fn (?. keys . rest) (apply _ :keys keys rest)] or something, and I was wondering if you already had a plan for those extra features.

By the way, macros complicate this issue. If a macro is given keywords it doesn't recognize, are they just passed to it as symbols in its rest arg, or should it be treated the same way as a function?

-----

2 points by akkartik 5118 days ago | link

Ah I see now.

A pattern is starting to emerge. You're thinking about redefinition much harder than me. My approach so far has been: compiler warns of redefinition, I stop the presses and go figure out if it's a problem.

"If a macro is given keywords it doesn't recognize, are they just passed to it as symbols in its rest arg, or should it be treated the same way as a function?"

I hadn't considered that, but fwiw the current wart implementation simply strips out unrecognized keyword args and the values provided for them. Hmm, that's probably wrong.

-----

2 points by rocketnia 5118 days ago | link

My only point with 'late is to demonstrate a utility that needs a different implementation once keyword arguments are introduced to the language, and which may have no implementation at all if the keyword argument framework isn't comprehensive enough.

But yeah, I do think about redefinition a lot. ^^ Penknife's core library, when I get around to it, will be designed for people to be able to modify it, the way people do with arc.arc. I'm also trying to give it a module system that plays nicely with that kind of invasive coding style. It's for customizability's sake.

-----

2 points by akkartik 5118 days ago | link

If someone redefines 'car and 'cdr to work with strings, 'first and 'rest won't reflect the change.. They're not hackable.

It's interesting; there seems to be a tension between future hackability and <strike>verbosity</strike> brevity. Perhaps the best way to get the best of both worlds is to go for an even more dynamic interpreter. Like forth or factor, just have all name lookups happen at runtime.

-----

1 point by rocketnia 5118 days ago | link

Do you mean that if you say (= foo (a b c)), then (a b c) shouldn't be evaluated until you look up foo? Well, I know I wouldn't want that all the time:

  (= setter (table))  ; oops ^_^
It's interesting; there seems to be a tension between future hackability and [brevity].

I think that's only a tension between being brief and being specific. It can't be avoided; if I specifically want things to be a certain way (e.g. hackable), I'm willing to leave the beaten path to get there. The real issue for me is how far off the beaten path I can get while still being brief enough not to get fed up and make a more hospitable language. :-p

There's another kind of brevity tension. I think a language that tries to maximize brevity doesn't need to worry about having a small implementation too. After all, it's probably built in a more verbose language. ...But now that's really off-topic. XD

-----

1 point by akkartik 5118 days ago | link

> (= setter (table)) ; oops ^_^

Yeah I'm not sure I understand all the implications. It probably wouldn't be an entirely new evaluation model. Perhaps you just have def store pre-evaluated names.

I went back to your examples, and I think first:rev is already resistant to changes to first or rev (Update: confirmed [1]). What's hard is:

  (= first car)
Perhaps we should give def a specialcase so that

  (def first car)
creates a synonym without hardcoding the implementation. How about this?

  (mac alias(a b) `(= ,a (late ,b)))
[1] Observe:

  > (= first car)
  > (= last first:rev)
  > (last '(a b c d))
  d
  > (= first cadr)
  > (last '(a b c d))
  c

-----

2 points by akkartik 5118 days ago | link

Assuming the only case we care about is function name aliases, I've pushed out the following simpler version to my repo (https://github.com/akkartik/arc/commit/fe21a3456fc7e69fc6ec1...)

  (mac alias(f g)
    `(= ,f (fn args
             (apply ,g args))))
For example, compare using =:

  > (= be iso)
  > (be 3 3)
  t
  > (= iso +)
  > (be 3 3)
  t ; didn't get new iso
..with alias:

  > (alias be iso)
  > (be 3 3)
  t
  > (= iso +)
  > (be 3 3)
  6 ; got new iso
So now I have two questions:

a) Do we need to handle rhs being anything more complex than just a symbol?

b) Neither your late nor my simplified version seem to work with macros. Is it possible to get alias work with macros?

-----

2 points by rocketnia 5118 days ago | link

I was going to call that '=late. ^_^ There are still other cases where a global function can end up stored somewhere it doesn't stay up-to-date with the variable binding, like storing it as a behavior in a data sructure or using (compare ...). But I don't expect to need anything more brief than 'late, actually.

It's funny, I hadn't settled on the name 'late until I posted it here, and before that point I was thinking of it as 'alias. ^_^ I wanted to avoid confusion with Racket's aliases, so I changed it at the last second.

macros?

Macros generally aren't hackable anyway, right? I don't have any ideas to change that.... Well, except these I guess. :-p

a) Change the whole language over to fexprs. This is probably the most elegant way to make things hackable, but it'll probably be inefficient without a convoluted partial evaluation infrastructure (ensuring things are free of side effects, and headaches like that ^_^ ).

b) Record which macros are expanded every time a command is evaluated, and re-run commands whenever their macros change. Running commands out of order and multiple times would be pretty confusing. (I bet there'd be ways to manage it, but I for one would forget to use them at the REPL.) It would also be easy to fall into an infinite loop by trying to redefine a macro using a command that actually uses that macro.

-----

1 point by rocketnia 5118 days ago | link

first:rev is already resistant to changes in first or rev

Oh, good to know. ^_^ For Penknife, I tried to make [= foo a:b] produce a custom syntax if a or b was a custom syntax, but I got stuck. I ended up with [foo ...] using the local values of a and b, I think. In the process I got a bit confused about how a:b was supposed to work in the edge cases, from a design point of view.

This experiment was reflected in two commits in the Git repo: One introducing it, and one removing it 'cause of the poorly thought-out complexity it introduced. :)

-----

1 point by akkartik 5118 days ago | link

Link or it didn't happen :)

-----

1 point by rocketnia 5118 days ago | link

Here it is. https://github.com/rocketnia/penknife/commit/3ca7a6c216cc022... Penknife's significantly more complicated than what I've been able to talk about so far, with its own ad hoc vocabulary throughout, so good luck. ^^; Then again, that's a pretty old commit, so it's actually a simpler codebase in ways. XD

-----

1 point by SteveMorin 5107 days ago | link

Does Arc have keyword arguments in PG's release? I have seen a reference to it in the tutorial

-----

2 points by evanrmurphy 5107 days ago | link

Official arc has optional arguments, designated by `o` in parameter lists:

  arc> (def hello ((o name "stranger"))
         (string "Greetings, " name "!"))
  #<procedure:zz>
  arc> (hello)
  "Greetings, stranger!"
  arc> (hello "Steve")
  "Greetings, Steve!"
The difference between this and keyword parameters is that, with the latter, the caller of a function can bind its arguments to the function parameters in any order by specifying each argument's keyword. In akkartik's system, this looks something like:

  wart> (def hello (? greeting "Greetings" name "stranger")
          (string greeting ", " name "!"))
  #<procedure:zz>
  wart> (hello)
  "Greetings, stranger!"
  wart> (hello :greeting "Hello" :name "Steve")
  "Hello, Steve!"
  wart> (hello :name "Steve" :greeting "Goodbye")
  "Goodbye, Steve!"
But this doesn't work in official arc.

-----