[Proposal] ActiveSupport or_else

This would allow some cleaner expressions by allowing us to remove some outer parentheses.

This:

def one_of(possibilities = [])
  (possibilities.find(&:present?) || "").split(",").first.try(:strip) || ""
end

Becomes:

def one_of(possibilities = [])
  possibilities.find(&:present?).or_else("").split(",").first.try(:strip).or_else("")
end

This code would go into Object and NilClass. Perhaps something like this:

class Object
  def or_else(default_value)
    present? ? self : default_value
  end
end

class NilClass
  def or_else(default_value)
    default_value
  end
end

edit 1:

I’d also propose having a block variant as well. This would make the API a bit more useful, and a bit more like the inverse of the try API.

Similar to how we can have:

Person.find_by(name: params[:name]).try do |person|
  # do things with person
end

We would be able to handle the non-existing case:

Person.find_by(name: params[:name]).try(:touch).or_else do
  MissingPersonReportJob.perform_later(params[:name])
end

Also, try + or_else is useful for non-activerecord objects. Whereas ActiveRecord has APIs such as find_or_create_by, plain objects can have or_else.

Slightly contrived, but an example would be:

DEFAULT_VALUES = { a: 1, b: 2 }
# Process a JSON response from an external API.
# Necessary since we don't own the API and can't
# make changes directly to it
# @param [Hash, NilClass] hash
def process_a_json_response(hash)
  hash.try(:merge, DEFAULT_VALUES)
      .or_else(DEFAULT_VALUES)
end

I’m not sure I really follow what your one_of method is doing. The name sort of implies it should behave like in? but it doesn’t.

As to your actual proposal of of_else this seems vaguely like the presence. I.E.

variable.presence || 'default'

would be the same as your:

variable.or_else(default)

You said your goal is to remove parens but seems like both methods have the same number of parens if you wanted to chain additional methods just in different locations. I.E.

(variable.presence || '').split(',')

vs

variable.or_else('').split(',')

Just my two cents. Others may have different opinions.

Definitely not opposed to this suggestion, just wanted to say that I’d be curious to see a few more examples of where the proposed method could be used as in my opinion the shared example is hard to approach from reading point of view in both of the versions.

Currently it’s a one-liner that performs some transformations on primitives which makes it really hard to make assumptions about the code or learn the context.

I think in a real application I’d prefer splitting it into several methods with guard clauses and descriptive names that represent some concepts from my app

This is still far from being accessible but I believe it reveals opportunities to extract some concepts into separate methods

def one_of(possibilities = [])
  possibilities.compact!
  return "" if possibilities.empty?

  possibility = possibilities.first

  first_possibility_component = possibility.split(",").first

  return "" unless first_possibility_component

  first_possibility_component.strip
end

As to your actual proposal of of_else this seems vaguely like the presence . I.E.

Indeed! I would even like for it to use presence within the implementation.

You said your goal is to remove parens but seems like both methods have the same number of parens

To be clear, I want to remove outer parentheses. This makes the chain of calls clearer, especially across multiple lines in a codebase with aligned method chains:

(variable.presence || '')
         .split(',') # should it be indented to this point?
         .first
         .try(:strip) || '' # this also feels sort of awkward

vs:

variable.or_else('')
        .split(',')
        .first
        .try(:strip)
        .or_else('')

To be fair the example could also be written as

variable.presence
            &.split(',')
            &.first
            &.strip || ''

but it still doesn’t align as nicely as your second version.

An alternative might be to add an argument to presence similar to Hash#fetch’s default

variable.presence('')
        .split(',')
        .first
        .presence('')
        .strip