Skip to content

Instantly share code, notes, and snippets.

@pcantrell
Last active December 8, 2023 20:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pcantrell/33a9b2f88ac7927e243c9ff5ab80475e to your computer and use it in GitHub Desktop.
Save pcantrell/33a9b2f88ac7927e243c9ff5ab80475e to your computer and use it in GitHub Desktop.
# How can we harmonize the types of a Square class and a Rectangle class
# so that it's possible to write a rescale function that works on both?
#
# As we saw in class Wed, if we’re doing this in Java, inheritance cannot
# solve this problem in a simple way: both `Square extends Rectangle`
# and `Rectangle extends 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 Java’s type system.
#
# In class today, we saw that having one inherit from the other doesn’t
# really work in Ruby either. Furthermore, in Ruby we can _still_ pass one
# in place of the other and get bad results even if _neither_ inherits from
# the other! Duck typing lets us substitute one class for another and
# just see what happens, even if the substitution makes no sense at all.
#
# At least in Java, we could say “Don’t ever try to substitute squares for
# rectangles, or vice versa!” by not having either inherit from the other.
# Ruby allows us to try substituting _any_ type for another. “I’m not here
# to stop you!” says Ruby.
#
# But the increased chaos of duck typing also opens up an interesting
# metaprogramming solution that is not even possible with Java.
# `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 rescale function that multiples all the dimensions
# by some factor, no matter how many or how few there are to adjust:
def rescale!(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
rescale!(r, 3)
p r
s = Square.new
s.size = 127
rescale!(s, 3)
p s
# Now we can have an arbitrary number of dimensional attributes that need to be scaled!
class Cube
include Dimensional
dimension_attr :width
dimension_attr :height
dimension_attr :depth
end
c = Cube.new
c.width = 10
c.height = 50
c.depth = 300
rescale!(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
rescale!(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. 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.[1]
#
# 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.
#
# [1] 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment