some Ruby things I learned today 💭

If you've been following my posts, I mentioned that following the #100DaysOfCode challenge I wanted to spend more time doing practice problems on codewars, leetcode, HackerRank, etc. This is because I want to spend more time developing my problem-solving skills and to become more familiar of the "ruby-way" of programming.

Here are some things I learned about today that feel worth noting:

Inject / Reduce

These methods can be used to basically combine a bunch of things down to one thing (or at least that's how I've come to understand it so far). Usually, we can do this by iterating through the data structure and applying some sort of aggregation technique, but the reduce method lets us do it in one line! According to ruby-doc, the inject and reduce methods are aliases and they combine all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator.

Example of inject and reduce

[1,2,3,4].inject( :+ )
=> 10

# A method that generates the full name of people given their first name
# followed by some variable number of middle names, and finally their last name.
def full_name(name, *other_names)
  other_names.reduce(name) { |n, o| n + " " o)
end

full_name('John', 'Jacob', 'Jingleheimer', 'Schmidt')
=> "John Jacob Jingleheimer Schmidt"

Note: A colon : before a sequence of characters is a Symbol literal. The symbol you pass to reduce or inject will be interpreted as a name of a method to call on each element.

Defining Methods

Positional arguments, Optional parameters, and Keyword arguments

There are several ways to setup the way arguments are passed to methods in Ruby. The first way to do so is to simply use positional arguments. For example, here is a class Coffee with positional arguments to initialize the coffee object's size, flavor, and roast.

class Coffee
  attr_accessor :size, :roast, :flavor, :ice

  def initialize(size, roast, flavor, ice)
    @size = size
    @flavor = flavor
    @roast = roast
    @ice = ice
  end
end

cuppa = Coffee.new('small', 'dark', 'vanilla', true)

This is a straightforward way of setting up our class, but there is one major drawback to using positional arguments which is that the arguments are order specific. Let's pretend that instead of the three arguments we have to initialize the object, we had 20. If we wanted to apply a default value to one of the arguments, we would now need to re-order the arguments in the initialize method call and in all the objects that have been instantiated previously. This becomes really cumbersome if you have a lot of arguments because you now have to ensure that the argument order is corrected. Technically, the method invocation will still work if you don't place the argument with a default value at the end (for example, you can define a method like def initialize(size, roast='Medium', flavor), but the convention is to place arguments with default values at the end because it's less confusing.

Here is an example of adding a default value to our argumentroast in our Coffee class.

class Coffee
  attr_accessor :size, :roast, :flavor, :ice

  def initialize(size, flavor, ice, roast='Medium')
    @size = size
    @flavor = flavor
    @roast = roast
    @ice = ice
  end
end

cuppa = Coffee.new('small', 'dark', 'vanilla', true)
# The values are now incorrect with no indication of the error
cuppa.size # small
cuppa.flavor # dark
cuppa.ice # vanilla
cuppa.roast # true

Luckily, we can refactor our class to use keyword arguments to make future changes a lot easier! With keyword arguments, we explicitly state every argument with a keyword (just as the naming implies). This will make things easier such that the object instantiation will no longer rely on order-specific argument calls. Then, if we need to add a default parameter later on we don't need to change every invocation of our Coffee class.

class Coffee
  attr_accessor :size, :roast, :flavor, :ice

  def initialize(size:, roast: 'Medium', flavor, ice: 'false')
    @size = size
    @flavor = flavor
    @roast = roast
    @ice = ice
  end
end

cuppa = Coffee.new(
  size: 'large',
  roast: 'dark',
  flavor: 'hazelnut',
)
cuppa.size # large
cuppa.roast # dark
cuppa.flavor # hazelnut
cuppa.ice # false

Awesome, now we can see with the method invocation exactly what arguments the class is expecting!