DEV Community

Brandon Weaver
Brandon Weaver

Posted on

Let's Read - Eloquent Ruby - Ch 16

Perhaps my personal favorite recommendation for learning to program Ruby like a Rubyist, Eloquent Ruby is a book I recommend frequently to this day. That said, it was released in 2011 and things have changed a bit since then.

This series will focus on reading over Eloquent Ruby, noting things that may have changed or been updated since 2011 (around Ruby 1.9.2) to today (2024 — Ruby 3.3.x).

Chapter 16. Use Modules as Mixins

Modules are good for a lot more than just namespacing in Ruby. They're containers for behaviors, certainly, but what if we could also use that to share behavior across different classes? Well that's exactly what a mixin is in Ruby, and it's how we create reusable chunks of code.

Quick Aside: Hanging Indentation

The book tends to use this hanging style for some multi-line constructs:

class Document
  CLICHES = [ /play fast and loose/,
              /make no mistake/,
              /does the trick/,
              /off and running/,
              /my way or the highway/ ]
end
Enter fullscreen mode Exit fullscreen mode

While this is a personal preference I tend to really dislike hanging indentation for a few reasons:

  • Diffs - If that variable name changes or you move the construct you now have to reindent the entire construct relative to wherever it ends up.
  • Left to Right - The more distance my eye has to travel to follow code the more my ADHD self loses track of things.
  • Two Space - Consistent indentation makes it far easier to visually group code and not lose track of ends and other bounds

Instead I prefer line-breaked normally indented constructs:

class Document
  CLICHES = [
    /play fast and loose/,
    /make no mistake/,
    /does the trick/,
    /off and running/,
    /my way or the highway/
  ]
end
Enter fullscreen mode Exit fullscreen mode

To me it's just easier to read and work with later, and the only reason I know why some default to this is JetBrains IDEs like RubyMine suggesting it for some strange reason because Java is notorious for it. Anyways, one of my personal biggest annoyances in reading Ruby code, however petty it might be.

Better Books with Modules

The book starts with an example of improving the Document class further with the ability to judge the quality of the content's writing. As the book mentions cliches can generally be frowned upon, so it suggests we create a way of counting how many of them there are in a document's content:

class Document
  CLICHES = [
    /play fast and loose/,
    /make no mistake/,
    /does the trick/,
    /off and running/,
    /my way or the highway/
  ]

  def number_of_cliches
    # Rather than the `inject(0)` the `count` method would
    # make more sense here. Also `match?` is clearer than
    # `=~` is.
    #
    # If you wanted to speed this up `Regexp.union(CLICHES)`
    # as a new constant would be a fair bit faster and remove
    # an entire loop.
    CLICHES.count { |phrase| phrase.match?(content) }
  end
end
Enter fullscreen mode Exit fullscreen mode

Now I do deviate from the book a bit here in using count and match?, but I believe that demonstrates that there can be methods that more clearly indicate intent while also making the code more succinct. Typically nowadays count and sum have replaced most usages of reduce and its alias inject, and are more clear to readers in general. If there's a more specific named method available you can use in general prefer that.

The book continues by suggesting that there may be an extended ebook system we need to support:

class ElectronicBok < ElectronicText
end
Enter fullscreen mode Exit fullscreen mode

Do we copy and paste the behavior from Document, even if it would mostly be duplicated? The book mentions most folks first instinct is to create a base class, but in Ruby we also have a way to group similar behaviors outside of inheritance using mixins.

Mixin Modules to the Rescue

We can take that bit of potentially shared code and wrap it in a module like so:

module WritingQuality
  CLICHES = [
    /play fast and loose/,
    /make no mistake/,
    /does the trick/,
    /off and running/,
    /my way or the highway/
  ]

  def number_of_cliches
    # Rather than the `inject(0)` the `count` method would
    # make more sense here. Also `match?` is clearer than
    # `=~` is.
    CLICHES.count { |phrase| phrase.match?(content) }
  end
end
Enter fullscreen mode Exit fullscreen mode

...and elsewhere in the code we can directly include it in our classes using include:

class Document
  include WritingQuality
end

class ElectronicBook < ElectronicText
  include WritingQuality
end
Enter fullscreen mode Exit fullscreen mode

Giving it a test you can see we get the same results:

my_tome = Document.new(
  title: "Hackneyed",
  author: "Russ",
  content: "my way or the highway does the trick"
)

my_time.number_of_cliches
# => 2

my_ebook = ElectronicBook.new(
  title: "Hackneyed",
  author: "Russ",
  content: "my way or the highway does the trick"
)

my_ebook.number_of_cliches
# => 2
Enter fullscreen mode Exit fullscreen mode

One of the reasons a lot of folks use mixins is because you can only inherit from one class, but there's no real limit on how many modules you can mixin until your RAM catches fire.

The book suggests maybe there are a few more shared behaviors that we could add:

module ProjectManagement; end
module AuthorAccountTracking; end

class ElectronicBook < ElectronicText
  include WritingQuality
  include ProjectManagement
  include AuthorAccountTracking
end
Enter fullscreen mode Exit fullscreen mode

The danger, of course, is that too many modules and it can be very hard to follow what's going on in the code without opening up 5+ files on the biggest widescreen you can fit on your desk or monitor arm. Generally if you need a 48" monstrosity of an ultra wide just to read your code chances are some things might need to be simplified a bit.

Extending a Module

In all of those above cases we're mixing behavior into the instance of a class, does that mean we could also mixin behavior at a class level? We can certainly give it a try:

module Finders
  def find_by_name(name); end
  def find_by_id(doc_id); end
end
Enter fullscreen mode Exit fullscreen mode

Perhaps your first instinct, if you only know include, is to do something like this:

class Document
  class << self
    include Finders
  end
end
Enter fullscreen mode Exit fullscreen mode

...but as with most things Ruby also has a keyword for that idea which is a bit more succinct:

class Document
  extend Finders
end
Enter fullscreen mode Exit fullscreen mode

Both of which could be called like so:

Document.find_by_name("War and Peace")
Enter fullscreen mode Exit fullscreen mode

...though a lot of find_by and where style methods tend to accept several keyword arguments, very similarly to what ActiveRecord does.

So what's the difference between include and extend? You use include for internals or instances, and extend for externals, or at least that's the pneumonic I tend to use in my head to remember. Either that or I swap the two if I really blank out on it.

There's also prepend, but that's a subject for another article.

Staying Out of Trouble

For every module we mixin we're also changing the hierarchy of the inheritance chain behind it. The book uses an illustration here, but in essence it says:

Document -> WritingQuality -> Object
Enter fullscreen mode Exit fullscreen mode

In a lot of cases this is transparent, but you can tell what modules a class has loaded either through ancestors or by asking it using kind_of?(ModuleNameHere). For Document the ancestry chain looks something like this:

[Document, WritingQuality, Object, Kernel, BasicObject]
Enter fullscreen mode Exit fullscreen mode

include inserts modules between a class and its superclass, so if Document defined its own variant of a method it would win. prepend, conversely, puts it before the class and any methods on it come first. The book gives an example of this overriding for include here:

class Document
  include WritingQuality
end

class PoliticalBook < Document
  def number_of_cliches = 0
end
Enter fullscreen mode Exit fullscreen mode

Since PoliticalBook comes first in the ancestry chain it'll return 0 any time we call that method. Whatever's higher up the chain wins, unless it doesn't respond to that method. If you happened to include multiple modules with the same behavior that contradict each other that'd mean whichever one loads first will "win" and the other will be ignored if they share method names.

In general you want to keep mixin modules short and focused, because the larger they become the more risk you run of running into each other while coding.

Wrap Up

Mixins are extremely common in Ruby, as are include and extend, with prepend being a bit rarer. Enumerable especially is a great example if you want to read and explore.

Top comments (1)

Collapse
 
primetarget profile image
Ethan Anderson

Interesting post! I'm curious, do you think the shift away from reduce to more specific methods like count has also influenced how Rubyists think about performance or readability over time?