[racket-dev] Proposal for a "no-argument"
I'm not enthusiastic about this proposal.
As you say at the start, it seems like a rare case. My main objection
is that it's too rare to merit a change to the main `lambda' form and
other parts of our infrastructure, such as documentation tools.
As a weaker objection, I don't agree with the suggestion that naive
users can somehow ignore the `no-argument' value. I think it would show
up prominently in documentation, contracts, and types.
Weaker still, in terms of how well I can defend it: I'm suspicious of
"no-value" values in general. Granted, we already have `#f', #<void>,
and #<undefined>, but I'm not really happy about that (and I keep
thinking about ways to get rid of #<undefined>).
At Sun, 1 Jul 2012 09:27:00 -0400, Eli Barzilay wrote:
> There rare cases where it is useful to have a value that means that no
> argument was passed to a function. In many of these cases there is a
> plain value that is used as that mark, with the most idiomatic one
> being #f, but sometimes others are used. IMO, while such uses of #f
> are idiomatic, they're a hack where an argument's domain is extended
> only to mark "no argument".
>
> A more robust way to do that, which has become idiomatic in Racket is
> to use (gensym). (And as a sidenote, in other implementations there
> are various similar eq-based hacks.) IMO, this is an attempt to
> improve on the #f case by guaranteeing a unique value, but at its core
> it's still a similar hack.
>
> Recently, I have extended the `add-between' function in a way that ran
> against this problem at the interface level, where two keyword
> arguments default to such a gensymed value to detect when no argument
> is passed. Natually, this "leaked" into the documentation in the form
> of using `....' to avoid specifying the default value and instead
> talking about what happens when no argument is given for the keywords
> in question.
>
> After a short discussion that I had with Matthew, the new version uses
> a new keyword that holds the unique no-value value, to simplify
> things:
>
> (define (foo x #:nothing [nothing (gensym)] [y nothing])
> (printf "y is ~s\n" (if (eq? y nothing) 'omitted y)))
>
> The idea is that this does not depend on some specific unique value,
> since one can be given. For "end-users" of the function, there is no
> need to know about this. It's useful only for wrapper functions which
> want to mirror the same behavior, and call `foo' in a way that makes
> their own input passed to it, including not passing it when its own
> input is missing. In this case, you'd do something like this:
>
> (define (bar #:nothing [nothing (gensym)] [x nothing])
> (foo 10 x #:nothing nothing))
>
> This works, but I dislike this solution for several reasons:
>
> 1. Instead of finding a solution for the `gensym' problem, this
> approach embraces it as the proper way to do such things.
>
> 2. But more than that, it also exposes it in the interface of such
> functions, which means that "simple end users" need to read about
> it too. There is no easy way to somehow say "you souldn't worry
> about this unless you're writing a function that ...", and if you
> look at the current docs for `add-between' you'd probably wonder
> when that #:nothing is useful.
>
> 3. There is also a half-story in this documentation -- even though the
> docs look like the above function definition, you obviously would
> want to define a single global gensymmed value and use it, to avoid
> redundant allocation. By the way the docs read, the above does
> look like the way to do these things, and I can see how a quick
> reading would make people believe that it's fine to write:
>
> (define (foo)
> (define (bar [x (gensym)])
> ...)
> ... call bar many times ...)
>
> I considered a bunch of alternatives to this, and the one closest to
> looking reasonable is to use the #<undefined> value: it makes some
> sense because it is a value that is already used in some "no value"
> cases. However, it is probably a bad idea to use it for something
> different. In fact, that's how many languages end up with false,
> null, undefined, etc etc.
>
> (As a side note, a different approach would be to use a per-argument
> boolean flag that specifies if the corresponding argument. Since this
> started with a documentation point of view, I'm assuming that it won't
> be a good solution since it doesn't solve that problem -- a function
> that uses it similarly to `add-between' would still need to avoid
> specifying the default.)
>
> Instead, I suggest using a new "special" value, one that is used only
> for this purpose. The big difference from all of these special values
> is that I'm proposing a value that is used only for this. To
> "discourage" using it for other reasons, there would be no binding for
> it. Instead, there would be a fake one, say `no-argument', which is
> used only as a syntax in a default expression and only there the real
> no-argument gets used -- so the value is actually hidden and
> `no-argument' is a syntactic marker that is otherwise an error to use,
> like `else' and `=>'. (I'm no even suggesting making it a syntax
> parameter that has a value in default expressions, because you
> shouldn't be able to write (λ ([x (list no-argument)]) ...).) The
> only real binding that gets added is something that identifies that
> value, or even more convenient, something like `given?' that checks
> whether its input is *not* that value.
>
> To demonstrate how this looks like, assume that the kernel has only a
> three-argument `kernel-hash-ref', and you want to implement `hash-ref'
> on top of it without using a thunk (which avoid the problem in a
> different way). The so-far-idiomatic code could be as follows:
>
> (define none (gensym)) ; private binding
> (define (hash-ref t k [d none])
> (cond [(not (eq? d none)) (kernel-hash-ref t k d)]
> [(not (has-key? t k)) (error "no such key")]
> [else (kernel-hash-ref t k 'whatever)]))
>
> Using the new idiom, it would be:
>
> (define default-nothing (gensym)) ; private binding
> (define (hash-ref t k #:nothing [nothing default-nothing] [d nothing])
> (cond [(not (eq? d nothing)) (kernel-hash-ref t k d)]
> [(not (has-key? t k)) (error "no such key")]
> [else (kernel-hash-ref t k 'whatever)]))
>
> And using my suggestion:
>
> (define (hash-ref t k [d no-argument])
> (cond [(given? d) (kernel-hash-ref t k d)]
> [(not (has-key? t k)) (error "no such key")]
> [else (kernel-hash-ref t k 'whatever)]))
>
> Note that the code is essentially the same, only now there's no need
> for the gensym hack or any of the similar things, and the interface to
> the function is back to its uncluttered form. The documentation would
> use `no-argument', which would be linked to a description of how/when
> to use it when people need to do so, while for most people the
> description is clear from the name.
>
> Two notes:
>
> 1. You might notice that there's no real need for the fake binding
> since it's just syntax. For example, the same could be done with
> just dropping the default expression, as in
>
> (define (hash-ref t k [d]) ...)
>
> Keeping the binding there is useful since the syntax is still the
> same (eg, macros don't need to change since it looks like just
> another expression), and in case it is later desirable, it's easy
> to replace it by an actual binding (see below).
>
> 2. Unlike the #:nothing keyword, this is not 100% robust, since you
> *can* grab the actual value and pass it. For example:
>
> (hash-ref t k ((λ ([x no-argument]) x)))
>
> I think that this is not a problem in practice, at least no a new
> one, since it's essentially the same situation as with
> #<undefined>, where it is possible to write an expression that
> evaluates to it.
>
> In addition, it is not 100% robust in that you can write broken
> code like:
>
> (define (hash-ref t k [d no-argument])
> (kernel-hash-ref t k d))
>
> and end up with the no-argument value as a result. But this is
> also not a new problem, since the same mistake can apply to the
> other cases too.
>
>
>
> -----
>
> [Footnote:
>
> Like I said, further on, there is the possibility of having a proper
> `no-value' binding. There are some obvious uses for it (eg, call a
> function with two optional values and specify only the last); these
> uses are arguably ones that should use keywords instead, but maybe
> there's still a point for older functions. Regardless of whether it's
> a good idea or not, I think that if the need comes, then it could be
> implemented similarly to a `Maybe' thing. To abuse the names from a
> different language, add a `some' constructor that is optional for any
> value other than no-value, which makes it a kind of a quotation -- so
> inputs like `5' are the same, `no-value' is the same as above, and
> (some x) is the same as x for any value, including `no-value'. (And
> functions would need to check if it's a `some?'.) But that's just a
> random though, not related to the above in any way other than showing
> how that syntax leaves this possibility open.
>
> ]
>
> --
> ((lambda (x) (x x)) (lambda (x) (x x))) Eli Barzilay:
> http://barzilay.org/ Maze is Life!
>
> _________________________
> Racket Developers list:
> http://lists.racket-lang.org/dev