Co-optional arguments?

Racket and many other languages have the concept of optional arguments at the receiver side:

(define (a-procedure mandatory-arg #:foo [foo 123])
  ;; Use mandatory-arg and foo as normal
  ...)

(a-procedure mandatory-value #:foo 234) ;; override foo
(a-procedure mandatory-value) ;; leave foo alone

Here, the procedure is offering its caller the option of supplying a value for foo, and if the caller does not do so, uses 123 as the value for foo. The caller is permitted not to care (or even to know!) about the #:foo option.

Almost no language that I know of has the symmetric case: an optional argument at the caller side (and because the feature doesn’t exist, I’m having to invent syntax to show what I mean):

(define (run-callback callback)
  (callback mandatory-value [#:foo 234]))

(run-callback (lambda (mandatory-arg) ...)) ;; ignores the extra value
(run-callback (lambda (mandatory-arg #:foo [foo 123]) ...)) ;; uses it

The intent here is that the procedure is permitted not to care (or even to know) about the additional value its caller supplies.

This would be useful for callbacks and other situations where the code invoking a procedure has potentially-useful information that the specific (and statically-unknown) procedure being called may or may not care about.

I said above that almost no languages have this feature: it turns out that Squeak and Pharo have something similar in their BlockClosure>>#cull: methods. The following code works when given a closure accepting zero, one or two arguments:

someBlock cull: 1 cull: 2 cull: 3

(Update: @msimoni points out that Common Lisp’s allow-other-keys is similar, too. It still requires the callee to declare something extra, though.)

Comments (closed)
Jason 00:14, 2 Aug 2012

Add C and C++ to the list of languages with optional arguments.  Yes, it's not Lispy, but it's been around a while...  C/C++ can do this through varargs (which sucks), and you can also do it with function pointers and casting.  The receiving code doesn't look at or care about additional parameters on the stack.  C will even do it without casting, just compile with different header files.

But then, you'd have to program in C, which would be nasty.

Tony Garnock-Jones 06:07, 2 Aug 2012 (in reply to this comment)

 Excellent point!

David 07:45, 2 Aug 2012

Ruby's slightly odd support for keyword arguments supports what co-optional arguments.   In fact, it's kind of the default.

Ruby supports keyword arguments by collecting the keyword arguments into a Hash (= dictionary).  For example (with ruby 1.9 syntax):

def foo opts={}
  puts opts[:x].inspect
end
                                                                               
foo x: 10
# => 10
foo

# => nil

If you want to provide default values for these keyword arguments, you write something like:

def foo opts={}
  opts = { x: 42 }.merge(opts)
  puts opts[:x].inspect
end
                                                                               
foo x: 10
# => 10

foo
# => 42

But nothing stops the caller providing other keywords, which are ignored:

foo y: 20
# => 42

Despite the room for error, all Ruby code is written in that style.  No-one goes to the trouble of detecting superfluous arguments, which would require writing something like:

def foo opts={}
  opts = { x: 42 }.merge(opts)
  opts.keys.all? { |k| [:x, :y].include? k } or raise "unexpected keys"
  puts opts[:x].inspect
end

Tony Garnock-Jones 07:52, 2 Aug 2012 (in reply to this comment)

That is very close, too. It does still require the receiver to expect a hash, though. A truly 0-ary function raises ArgumentError when given such a keyword argument.

Jason's point about C, and your point about Ruby, make me realise that Javascript also supports something along these lines.

Random Person 03:04, 25 Oct 2012 (in reply to this comment)

 JavaScript is similar. Extra arguments are discarded, extra parameters get 'undefined'.