Dimensional (Mixins Example)

How can we harmonize the types of a Square class and a Rectangle class so that it’s possible to write a scale function that works on both?

As we saw in class, both Square ⊂ Rectangle and Rectangle ⊂ Square end up violating the Liskov Substitution Principle. While there is a nice “is-a” relationship here — all squares are rectangles! — that does not translate nicely into object-oriented inheritance relationships. Inheritance can’t model this relationship well.

In Ruby, the door is open to try such nonsensical approaches even if we didn’t use inheritance. Duck typing means you can always try passing an object and see what happens. “I’m not here to stop you!” says Ruby. At least in Java, we could say, “Don’t ever try to substitute a square for a rectangle, or vice versa!” by not having either inherit from the other.

But the increased chaos of duck typing also opens up an interesting metaprogramming solution that is not even possible with Java.

A module is Ruby’s version of a mixin: a set of members that we can add to multiple different classes without actually creating an inheritance relationship. A mixin is almost like copying and pasting code between classes, but without actual duplication of code.

module Dimensional
  def self.included(target_class)  # When this module is included in a target class...
    
    # We add a new metaprogramming method, dimension_attr, to
    # that target class. It creates a normal attr_accessor, but
    # also keeps track of all the dimension-related attrs on
    # the class.
    #
    def target_class.dimension_attr(name)
      attr_accessor name

      @dimensions ||= []
      @dimensions << name
    end

    def target_class.dimensions
      @dimensions
    end

  end
end

# Here are two classes that use our new mixin:

class Rectangle
  include Dimensional

  dimension_attr :width
  dimension_attr :height
end

class Square
  include Dimensional

  dimension_attr :size
end

p Rectangle.dimensions
p Square.dimensions

# And here is a generic scale function that multiples all the dimensions
# by some factor, no matter how many or how few there are to adjust:

def scale!(shape, factor)
  shape.class.dimensions.each do |dim_name|
    # If dim_name is "width", then the following would be
    # equivalent to `self.width *= factor`:
    shape.send(
      "#{dim_name}=",
      shape.send(dim_name) * factor
    )
  end
end

r = Rectangle.new
r.width = 10
r.height = 20
scale!(r, 3)
p r

s = Square.new
s.size = 127
scale!(s, 3)
p s

# Now we can have an arbitrary number of dimensional attributes that need to be scaled!

class Cuboid
  include Dimensional

  dimension_attr :width
  dimension_attr :height
  dimension_attr :depth
end

c = Cuboid.new
c.width = 10
c.height = 50
c.depth = 300
scale!(c, 3)
p c

# Why stop there?

class SmileyFace
  include Dimensional

  dimension_attr :radius
  dimension_attr :eye_radius
  dimension_attr :eye_separation
  dimension_attr :eye_center_y
  dimension_attr :mouth_width
  dimension_attr :mouth_center_y
end
face = SmileyFace.new
face.radius = 10
face.eye_radius = 1
face.eye_separation = 5
face.eye_center_y = -2
face.mouth_width = 6
face.mouth_center_y = 5
scale!(face, 10)
p face

There is a relationship between SmileyFace and Square: they have a set of dimension-related attributes that should all scale together. That’s not a relationship that we explicitly model in Ruby’s type system, but the implicit relationship is there thanks to duck typing.

To model that as a type relationship in Java, we would have to give up on those properties being normal getter and setters backed by instance variables, and instead use, say, a Map<DimensionKeys, Double> to track them all.

Aside: more about Java

That isn’t strictly true: we could finagle something like the Ruby version using a Java feature called “reflection” which allows us to access parts of our class structure by name at runtime. We can’t create new structures on existing classes, as metaprogramming can, but we could manually write normal properties for the dimensions and then use reflection to scale them all together. This would be horrendously messy in practice. It’s technically possible. It is not, however, a solution anyone would be likely to reach for.

But in Ruby, they can be normal properties and part of an arbitrary-length data structure. We give up static type safety, but we buy some fairly wild metaprogramming flexibility.