Feature #20326
closedAdd an `undefined` for use as a default argument.
Description
Variations around UNDEFINED = Object.new
are a fairly common pattern to see used as default arguments to distinguish between nil
and no argument provided. For example, a Ruby implementation of Array#count might look something like:
class Array
UNDEFINED = Object.new
def UNDEFINED.inspect = 'UNDEFINED'
UNDEFINED.freeze
def count(item = UNDEFINED)
if item == UNDEFINED
# ...
end
end
end
I'd like to propose adding an undefined
module function method on Kernel to remove the boilerplate for this fairly common use case. An __undefined__
method or __UNDEFINED__
keyword would be alternatives to undefined
. An undefined?
helper would also be an optional nicety:
class Array
def count(item = undefined)
if item.undefined?
# ...
end
end
end
A Ruby implementation might look like:
module Kernel
UNDEFINED = Object.new
def UNDEFINED.inspect = -'undefined'
UNDEFINED.freeze
private_constant :UNDEFINED
def undefined? = self == UNDEFINED
module_function
def undefined = UNDEFINED
end
Updated by jeremyevans0 (Jeremy Evans) 9 months ago
The problem with this is you could pass undefined
just the same as nil
. You would end up in a similar situation to JavaScript, which has both null
and undefined
.
The correct way to handle this is by setting a local variable inside the default argument value:
class Array
def count(item = (item_not_set = true))
if item_not_set
# item argument was not passed
else
# item argument was passed
end
end
end
Admittedly, this is a bit ugly, but the need to differentiate between passed default value and no supplied argument is fairly rare. Maybe there is a nicer way of supporting the same idea, that can still differentiate between passing the default value and not supplying an argument?
Updated by nobu (Nobuyoshi Nakada) 9 months ago
- Status changed from Open to Feedback
Updated by nobu (Nobuyoshi Nakada) 9 months ago
Any idea/feedback is welcome.
Updated by mame (Yusuke Endoh) 9 months ago
Personally, I don't like an API that distinguishes between "nil is passed" and "nothing is passed". I don't think we should introduce any feature to encourage such an API.
When we want to conditionally pass the argument to such an API, we need to write optional_val ? api(optional_val) : api()
or args = [optional_val].compact; api(*args)
or something ugly. Instead, I hope such an API just accepts and ignores nil
. It would be good enough in most cases.
Updated by Dan0042 (Daniel DeLorme) 9 months ago ยท Edited
mame (Yusuke Endoh) wrote in #note-4:
Personally, I don't like an API that distinguishes between "nil is passed" and "nothing is passed".
I fully agree. As far as options/keywords go, an empty hash should be considered semantically identical to {a: nil}
That being said, I do understand there's a problem with default values, because in this case "nil is passed" is indeed different from "nothing is passed". But adding a new "undefined" type of object has problems, as said above. I think it would need to be some kind of syntax in the method definition, so that a nil value passed as argument is interpreted as "use the default value".
#currently:
def example(v = 1) = v
example() #=> 1
example(nil) #=> nil
#crazy idea:
def example(v = 1 if nil) = v
example() #=> 1
example(nil) #=> 1
#or this?
def example(v ||= 1) = v
Updated by zverok (Victor Shepelev) 9 months ago
Personally, I don't like an API that distinguishes between "nil is passed" and "nothing is passed".
Unfortunately, such APIs are sometimes unavoidable, especially in implementing various generic algorithms (and not business logic).
Take, for example, Hash#fetch
. fetch('key')
and fetch('key', nil)
have different meanings. If such an API is to be implemented in Ruby, there are several options:
- use the "value provided" guard like discussed in this ticket (
default = UNDEFINED
ordefault = (default_not_set = true))
; - use
(*args)
and then check real arguments provided in the method body (by.count
or some form of pattern matching).
C implementations of the core methods do this constantly (either argument count check, or pattern matching-like rb_scan_args
), demonstrating the necessity. Maybe having the default way to say "argument was not provided" will help to define cleaner APIs.
Another common case is search algorithms, where the initial value for the found
local variable is set to some sentinel value and checked after the algorithm (while having found == nil
might be a legitimate case of "value is found, and it is nil
"). Alternative, is, again, to have an additional boolean is_found = false
and when the answer is found, set two local vars.
For these reasons, I think I saw a lot of codebases (including some written by me and my colleagues) going with some kind of UNDEFINED
/NOT_SET
constant, which feels like a kind-of hack, but still clearer than alternatives.
That being said, I am not sure I am in favor of having nil
/undefined
distinction in Ruby, and never proposed it myself (while considering it several times).
Updated by shan (Shannon Skipper) 9 months ago
zverok (Victor Shepelev) wrote in #note-6:
Unfortunately, such APIs are sometimes unavoidable, especially in implementing various generic algorithms (and not business logic).
I agree it's unfortunately unavoidable too often, so I resign myself to default = UNDEFINED
or default = default_not_set = true
like you mention above.
zverok (Victor Shepelev) wrote in #note-6:
That being said, I am not sure I am in favor of having nil/undefined distinction in Ruby, and never proposed it myself (while considering it several times).
I'm not sold on undefined
either, but would love some better way to detect default arguments that aren't set.
Crimson on Ruby Discord suggested something similar to block_given?
might be nice, like an arg_given?(arg)
.
def fetch(item, default = nil)
if arg_given?(:default)