Arc Forumnew | comments | leaders | submitlogin
A better syntax for optional args
5 points by akkartik 4931 days ago | 23 comments
Lisp has (a b c &optional opt1 (opt2 3) ..) which is perhaps most concise but kinda unlispy.

Scheme has no syntax for optional args, which seems incredible.

Arc uses (a b c (o opt1) (o opt2 3)) which is a few more parens than common lisp. Worse, using the symbol 'o' in this way seems like a wart on a 100-year language.

One solution I've been growing to like more and more is to just say (a b c (opt1 nil) (opt2 3)). But my solution right now can't distinguish destructuring and optional args when the default arg is a bound symbol.

Regardless of whether it can be done, what do people think of this syntax?



3 points by akkartik 4929 days ago | link

I have some new suggestions:

a) Quote optional args. (Thanks waterhouse)

  (a b (c d)) ; destructured
  (a b '(c 3) 'd) ; optional args
The quotes are only required to resolve ambiguity.

  (a b (c 3) (d nil)) ; optional args - defaults aren't variables

  (a b '(c A) '(d) ; needs quote to disambiguate from destructuring
b) Quote destructured args to distinguish them from optional args.

  (a b '(c d)) ; destructured
  (a b (c 3) (d)) ; optional args
Unlike the previous case you'll never be able to drop the quotes. But you'll rarely need more than one.

c) Use dot to delimit optional args from required ones. So arg lists could now have 3 different zones: required/destructured args, optional, and rest/body args.

  (a b . c d) ; optional args since you can only have one rest arg
  (a b . c) ; rest args
  (a b . (c)) ; c is optional - rest args will never be destructured.
  (a b . (c 3) (d 4) . e) ; optional args with defaults and a rest arg.
The dot delimiter for optional args is only needed for disambiguation, so this is equally clear:

  (a b (c 3) (d 4) . e) ; defaults aren't variables
But this requires the dot:

  (a b . (c A) . e) ; A is bound in the environment

What do y'all think of these options?

-----

4 points by conanite 4929 days ago | link

(a) might look funny when you want to evaluate an expression for the default value of an optional arg

  (def foo (a b '(c (defaults 'c x y z)) ...
To the untrained eye, (defaults 'c x y z) looks like it should not be evaluated because it's quoted

(c) makes parsing harder ... the assumption of only one element after the dot may be built into the parser

  arc> '(a b c . d e)
  Error: "UNKNOWN::8: read: illegal use of `.'"
It could be some privileged symbol instead of "." though ...

-----

1 point by akkartik 4929 days ago | link

Great points. I realized c was breaking the metaphor of '.'; I didn't realize it would actually refuse to parse.

It doesn't make sense to quote forms that may have expressions to evaluate, so b is better than a.

-----

2 points by fallintothis 4929 days ago | link

I dunno. It seems I'm in the minority, but I actually like using (o arg default). Only problem is the whole "don't use o when destructuring" thing, which is annoying. As for these options, I agree with conanite. In general, overloading either quotes or dots would be weird.

You could tweak o to be nicer to multiple parameters, though. E.g.,

  (def trim (s (o where 'both) (o test whitec))
    ...)
could instead be

  (def trim (s (o where 'both test whitec))
    ...)
Not that it's much of an issue:

  arc> (def optional (parm) (caris parm 'o))
  #<procedure: optional>
  arc> (ratio (andf alist ~dotted [some optional _]) (vals sig))
  75/494
  arc> (ratio (andf alist ~dotted [>= (count optional _) 2]) (vals sig))
  15/494
  arc> (apply max (trues (andf alist ~dotted [count optional _]) (vals sig)))
  3

-----

2 points by akkartik 4929 days ago | link

If CL's &optional came to this forum it would say: If you're going to have a new keyword like o, at least take advantage of the fact that required args can't follow optional args, and get rid of the parens.

I like how you argue this is a rare case :) My motivation is purely aesthetic, under the assumption that I'll want to make a long term commitment to a 100-year language something like arc. I'm trying to add keyword arguments to arc, and I figure I might as well revisit this decision at the same time.

-----

3 points by conanite 4929 days ago | link

Two problems with (o arg default) - you need to remember not to use 'o at the start of a destructuring list (and not get confused when you see (def handle-request ((i o ip)) ...) ), and as akkartik says it's paren-inefficient, a single keyword to delimit required/optional args would mean fewer tokens.

The first problem is easy to fix though - use a symbol that's less likely to be an arg name to identify optional args. How about '= ?

  (def myfun (a b (= c (something)) ...)
it has the advantage of similarity with ruby:

  def myfun a, b, c=something
disadvantage: looks silly when you don't supply a default value:

  (def myfun (a b (= c) ...)

-----

2 points by fallintothis 4929 days ago | link

get rid of the parens

Thinking about the paired-o suggestion I made, it has an advantage over grouping single optional arguments by their own parens -- at least if you have more than 2 optional parameters :). Compare:

  (a b (opt-c default-c))
  (a b (o opt-c default-c))

  (a b (opt-c default-c) (opt-d default-d))
  (a b (o opt-c default-c opt-d default-d))

  (a b (opt-c default-c) (opt-d default-d) (opt-e default-e))
  (a b (o opt-c default-c opt-d default-d opt-e default-e))
It's like a 3-character &optional, just with one of the characters at the end. However, you'd need to specify nil defaults by hand (though you might let an uneven pair at the end default to nil). E.g.,

  (def markdown (s (o maxurl) (o nolinks))
    ...)
could, at best, become

  (def markdown (s (o maxurl nil nolinks))
    ...)
But paired-o also generalizes the current behavior, so we could still use the original signature.

Just food for thought.

-----

1 point by akkartik 4929 days ago | link

Yeah I'd thought of that and forgotten about it :) Thanks.

-----

1 point by akkartik 4928 days ago | link

BTW, could you tell me how common destructured args are in the arc codebase, just for comparison?

-----

3 points by fallintothis 4928 days ago | link

In def and mac parameter lists, they're pretty much never used.

  arc> (def destructuring (parm) (and (acons parm) (isnt (car parm) 'o)))
  #<procedure: destructuring>
  arc> (ratio [errsafe:some destructuring _] (vals sig))
  1/247
  arc> (keep [errsafe:some destructuring _] (vals sig))
  (((y m d)) ((y m d)))
They're most often used implicitly with let.

  $ grep "(let (" *.arc | grep -v "(let (unquote"
  app.arc:      (let (f url) afterward
  app.arc:                        (let (typ id val sho mod) it
  app.arc:  (let (nums (o label "")) (halve s letter)
  app.arc:         (let (ms ds ys) toks
  arc.arc:      (let (vars prev setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:        (let (binds val setter) (setforms place)
  arc.arc:        (let (binds val setter) (setforms place)
  arc.arc:    (let (binds val setter) (setforms place)
  arc.arc:  (let (y m d) (date s)
  arc.arc:  (let (binds val setter) (setforms place)
  html.arc:      (let ((opt val) . rest) options
  news.arc:             (let (t1 t2 t3 . rest) toks
  srv.arc:  (let (i o ip) (socket-accept s)
  srv.arc:                (let (type op args n cooks) (parseheader (rev lines))
  srv.arc:  (let (type op args) (parseurl (car lines))
  srv.arc:  (let (type url) (tokens s)
  srv.arc:    (let (base args) (tokens url #\?)
  srv.arc:      (let (kill keep) (split (rev fnids*) nharvest)

-----

3 points by waterhouse 4930 days ago | link

Suggestion: No one should be trying to use the word "quote" as a variable name. Therefore, if (quote o) or (quote optional) or (quote opt) [depending on how verbose we want the word to be; we could even make all of these work] shows up in a parameter list, then the rest of the list shall be optional args. If desired, we could do something similar for keyword arguments. Model:

  (fn (a b 'opt (c 1) d e) ...) --> c,d,e optional args
  (fn (a b (quote opt) (c 1) d e) ...) --> same thing
  (fn ((a b)) ...) --> destructuring

-----

3 points by bogomipz 4924 days ago | link

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 4923 days ago | link

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

  (a b ? c 1 d nil e nil)

-----

4 points by waterhouse 4923 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 4922 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.

-----

4 points by akkartik 4928 days ago | link

Ah, turns out PLT/racket does support keyword and optional args: http://www.cs.utah.edu/plt/publications/scheme09-fb.pdf

Optional args:

  (lambda([a 3]) ..)
Keyword args:

  (lambda(#:a a) ..)
which seems a bit redundant.

Optional keyword args:

  (lambda(#:a [a 3]) ..)
And if you have a rest argument you must specify all optional and keyword arguments. Most interesting..

-----

1 point by rocketnia 4930 days ago | link

I've been considering using ssyntax in parameter lists to set apart special behavior in parameter lists, with destructuring being the normal behavior. (I was inspired by aw and someone else using (def foo (string:x) ...) syntax for coercing parameters, but I can't find that thread....)

Anyway, here's one of the takes on optional arguments I've been pondering:

  (a b c .o opt1 nil opt2 3)
This is pretty similar to the Common Lisp approach, and you can even use &optional instead of .o for added similarity. :-p The main advantage in my mind is that it's less of an abstraction leak; Arc doesn't really support argument lists of the form ((o a) b), so a function with optional arguments has only one extra conceptual boundary in its argument list, and you should only have to punctuate it there.

For a more customizable approach (especially if you do want ((o a) b) to work after all), I added a backtracking pattern matching library to Lathe a while back (http://arclanguage.org/item?id=11956) just for the purpose of making destructuring more customizable. I didn't end up doing anything with it after that, but if and when I did, parameter lists would probably look like this:

  (pfn (a b c (.o opt1) (.o opt2 3))
    ...)
The parameter list here would be wrapped up as '(struct (a b c (.o opt1) (.o opt2 3))) and compiled via the library to determine the variables it binds and what code it uses to bind them. In particular, the library would identify the 'struct operator as a "patmac," and it would use that patmac's implementation like a macro to determine what the rest of the expression meant. In the process, the expressions '(atom a), '(atom b), '(atom c), '(o opt1), and '(o opt2 3)--without the dots--would be sent off to another library that implemented a "list-patmac" system for monadic parsing of sequences.

As I mentioned, I never got this far. The second library doesn't exist at this point, and I haven't added even one patmac since I finished the proof of concept and made that post. It's not like it would be hard to get to this point in the design, but I've just been focusing on niftier things. ^_^;

So there you go, one straightforward syntax and one general-purpose syntax. The ultimate elegance struggle, eh? :-p

-----

2 points by aw 4930 days ago | link

syntax for coercing parameters: http://awwx.ws/fn-arg-converter, thread at http://arclanguage.org/item?id=10539

-----

1 point by rocketnia 4930 days ago | link

There it is, thanks!

-----

2 points by hasenj 4930 days ago | link

optional args don't map nicely to a list, they map more naturally to a dictionary (hash table).

Perhaps, the `o` is not needed?

  (fn (a b c (d 1) (e) (f)) ....)
d is optional, but defaults to 1 e and f are optional with not default.

perhaps the builtin special function operator shouldn't take any named argument at all: just a list and a hash table, sort of like a generic python function:

  def barebones(*args, **kwargs):
      pass

  (bare-function args kwargs (body))
and, fn would be implemented as a macro on top of that.

-----

2 points by akkartik 4930 days ago | link

Yeah perhaps we should just add dictionary literals to arc: http://news.ycombinator.com/item?id=1804558

My problem right now: fn((a b)) could define a function that takes a list of two args, or an optional arg a with a default value of b bound in the enclosing scope. This seems like a more serious problem than when I posted this thread.

Update: prior implementation of dictionary literals in arc: http://hacks.catdancer.ws/table-reader-writer.html. Discussion: http://arclanguage.org/item?id=10678

-----

2 points by aw 4930 days ago | link

Most recent implementation: http://awwx.ws/table-rw3

Though (for what it's worth) I've since realized that I don't like the {a 1 b 2} syntax; I find the key value pairs aren't grouped together well enough visually for me, and the curly brackets don't stand out enough themselves.

-----

1 point by zck 4930 days ago | link

Some optional arguments are better done as a list. The greater the number of optional arguments, though, the better off you are using keyword arguments.

-----