I have the following problem:

Given an Array, apply a certain method x to each Array element (yielding a new Array), with the restriction that array elements which are nil, should be mapped to some default value. Method and default element are parameters. Example:

Array: ['abc',nil,'defg']

Method: :size

Default element: -1

Expected result: [3, -1, 4]

I implemented this by adding a method map_with to the Array class:

class Array
  def map_with(accessor, default_value=nil)
    map {|e| e.nil? ? default_value : e.send(accessor) }

['abc',nil,'defg'].map_with(:size, -1)

I'm looking for an improvement of my current solution. Can the solution be improved in the following sense?

  • Provide the same result with simpler code
  • Make the function more flexible (more generally useful) by not adding too much code

migrated from stackoverflow.com Jun 15 '16 at 18:56

This question came from our site for professional and enthusiast programmers.

  • 3
    \$\begingroup\$ This method is concise enough. I'd suggest move it to Enumerable thus it can be applied to sets, queues and so on. \$\endgroup\$ – Aetherus Jun 13 '16 at 11:57
  • 1
    \$\begingroup\$ What is correct behaviour if object dont have method size ? \$\endgroup\$ – Lukas Baliak Jun 13 '16 at 12:04
  • 3
    \$\begingroup\$ this question must be moved into code review network \$\endgroup\$ – Ilya Jun 13 '16 at 12:16
  • 1
    \$\begingroup\$ What do you mean by more general/flexible? \$\endgroup\$ – sawa Jun 13 '16 at 13:07
  • \$\begingroup\$ Your code is already concise. By "by not adding too much code", do you mean "without adding too much code"? \$\endgroup\$ – sawa Jun 13 '16 at 13:11

My implementation is very similar to yours, with only a few small tweaks:

module Enumerable
  def map_with(accessor, default_value: nil)
    map { |e| e.respond_to?(accessor) ? e.send(accessor) : default_value }
  • Use Enumerable, not Array, so that the method can be applied to other objects such as instances of Hash, Set, etc.
  • Use respond_to? rather than a type-check against nil, for better duck-typing robustness.
  • Use a named parameter, for improved code clarity.
  • 2
    \$\begingroup\$ That is a method with a different feature. \$\endgroup\$ – sawa Jun 13 '16 at 13:09
  • 1
    \$\begingroup\$ Yes, it is. But I consider this to be a more "correct" implementation than simply checking for nil. \$\endgroup\$ – Tom Lord Jun 13 '16 at 13:21
  • 3
    \$\begingroup\$ Your method is completely different: e.g. the OP's method [1, nil, 2].map_with(:to_s, 'Not available') # => ['1', 'Not available', '2'] and your method [1, nil, 2].map_with(:to_s, 'Not available') # => ['1', '', '2']. This completely subverts the whole purpose of the method, namely avoiding strange corner cases with nil values. (Also: Enumerable is a module, not a class.) \$\endgroup\$ – Jörg W Mittag Jun 13 '16 at 13:33
  • 1
    \$\begingroup\$ This completely subverts the whole purpose of the method, namely avoiding strange corner cases with nil values --- Debatable.... OP never actually stated the intended use case for the method. What happens if OP tries something like ["foo", "bar", 123].map_with(:reverse)? I'd need more context to know which behaviour is more desirable. \$\endgroup\$ – Tom Lord Jun 13 '16 at 13:41
  • \$\begingroup\$ It is correct, that @TomLord solved a somewhat different problem than I have stated, but it is close enough to let me rethink my implementation. The difference is mainly in accessors, which are understood by NilClass (such as to_s), and in my concrete application, it seems that either solution would work. \$\endgroup\$ – user1934428 Jun 13 '16 at 16:12

Function Wrapping

Here is an alternative implementation, just for contrast with the other solutions. It needs a little work to be more robust, but the concept is clear.

This version wraps the target method with a new method that checks the arguments. If all are nil then the default value is returned, otherwise the arguments are passed to the inner method. This approach's benefits include being purely functional and not being limited to just mapping; it could be used on any method. The disadvantage is that usage is somewhat ugly and not as ruby-like as one might wish.

# Wrap function in a new function that checks for nil arguments
# If arguments are nil, return default value, otherwise return result
# of original function
def apply_default(default, function)
  fn = function.to_proc
  Proc.new { |*args| args.all?(&:nil?) ? default : fn.call(*args) }

a = ['abc', nil, 'defg']

# Usage Examples:
puts '====1===='
f = apply_default(-1, :size)
puts a.map(&f)

puts '====2===='
puts a.map(&apply_default(-1, :size))

puts '====3===='
puts a.map { |e| apply_default(-1, :size).call(e) }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy