Last active
December 8, 2023 20:35
-
-
Save pcantrell/33a9b2f88ac7927e243c9ff5ab80475e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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