Project

General

Profile

Actions

Feature #20326

closed

Add an `undefined` for use as a default argument.

Added by shan (Shannon Skipper) 9 months ago. Updated 9 months ago.

Status:
Feedback
Assignee:
-
Target version:
-
[ruby-core:117067]

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?

Actions #2

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 or default = (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)
Actions

Also available in: Atom PDF

Like0
Like1Like0Like0Like0Like0Like0Like0