Tip:
Highlight text to annotate it
X
So, let's talk about duct typing and mix ins. This is kind of picking up where we
left off from cool Ruby features that you're going to get to know and love. The
popular notion of duck typing, or where the name probably came from, Is that
there's this sort of off the cup saying that if you've got something that looks
like a duck and quacks like a duck, then you might as well treat it like a duck.
And this is actually more than just things like operator overloading. If you know
java which of course you all do because it's required for this class, technically
It?s a little bit more like java interfaces. So, here's a really simple
example. We saw some examples in a previous lecture about methods like sort
and reverse, things that appear to operate on collections. But what's interesting is
I can use the sort method on a list something that's like an array. I can use
it on. You know these two collections I can also use it on a ray of symbols. So
the fact is sword somehow be figuring it out what the classes of the different
[inaudible] of the ray are in order to sort them. And with the knowledge you
already have, you should immediately sort of think oh! Of course, As long as each
one of the classes so you know in this cases you've got. An array of [inaudible].
We've got an array of stings and we've got an array of symbols. And, as long as those
classes implement how to compare members of the class to each other. Sort doesn't
really have to do anything. Right. So that's the sense in which it's like
interface in java. The sense in which it is different. Is you don't have to declare
any of this. Ruby really doesn't care what the types of things are. The only question
is. Do the objects in question respond to the method that you want them to respond
to? So we talked about this idea of the innumerable. You know, methods that
operate on innumerable collections. And it might surprise you that those aren't
really members of the, or the, they're not methods of the array class at all, right?
The, the common reaction is you see oh, okay those are all arrays and you're
calling sort on them. Sort must be an instance method of the array class. But in
fact, you can also do, let's see. Whoops. Sorry. You can also, do something like I O
dot read lines. Read lines is a method that will return each line of the file one
at a time, and I could call sort on that too. And that isn't really an array,
right; it's a method that's doing some stuff. So, what's actually going on here
is that Ruby has a separate mechanism for dealing with this kind of reuse, called a
module, and you can think of a module as a collection of class and/or instance
methods, but they're not actually a class. So you can't instantiate it, right? You
can't make a thing that is an instance of a module in the way that you could make an
object that's an instance of a class. Modules are designed to be mixed into
something else, hence the name. Now some modules are just really used for name
spacing. So, you know there's functions that are in the Floating Point Math
Library and constants like pi and just prefix in the module name with two colons
means we're just gonna see them as name space. If you see Python, Python does
something kind of like this. But the more interesting kind of module is the ones
whose mess, whose methods are designed to be mixed into another class. So, when you
see something like. Here's a class A, never mind the fact that it inherits from
B for the moment, include my module that means that whatever file contains my
module, all of the methods of my module will be in lined as if you'd put them
right in the class. So any instance method defined in my module effectively becomes
available as instance method in class A any class methods defined in my module
become available as class methods of class A. Now, because ruby has inherited, so you
might ask, what does this mean in terms of the inheritance hierarchy because usually,
if you call a method and it's not found in the receiver's class, you go to the super
class. Well, you call a method missing and then you go to the super class. So yes, in
fact this changes things a little. We've been lying slightly masking the truth. In
fact when you call the method on a. If the if A itself doesn't have the method then
it'll check any modules that have been mixed into the class. If that doesn't
work, it'll actually check method missing and then it'll go up then chain of command
from there. So the chain of command is, you get to handle stuff. Any modules that
you have mixed in get to handle stuff. Method missing if you've defined it gets
to handle stuff. And otherwise then you really do punt up to the super class. So,
with this context, the sort method we've been seeing is actually a module method.
It's defined in a module called innumerable, and innumerable is mixed into
the array class, but it's also mixed into a bunch of other classes. So let's see
kind of what that means. So what do you. You know, when I say a mix in is a
contract, it means if a numerable is going to provide these extra methods which are
effectively going to become methods of the class I'm mixing into, are there any
assumptions the innumerable is making about that class in order for this to all
work? And the answer is yes. Innumerable assumes that every. Target object in that
class can respond to the each method. We've seen each before, it's one of the
integrator methods but here's the idea right? If you give me a collection
whatever kind of collection it is, and I'm allowed to use each to pull elements out
of that collection one at a time, then you can imagine given that ability I could
build a whole bunch of other methods. For example, I could ask is it the case that.
All elements of this collection have some property or which one true for elevation
voice and case any of them I have collect the honest matters of [sound] I have ask
of the collection include particular kinds of elements and again if you can obvious
right if you can ability to numeric the collection you can do all of this things.
Now in a job interface so more would declaration it's as re-implemented, I
numeral or something like that no sustain hurry and no sustain numeral. This is a
radical concept, but you read the documentation for innumerable. And the
documentation says, innumerable [inaudible] that objects of the class
you're mixing it into has the [inaudible] method. And further. There are some
methods in the numerable. Like the sort method. That also assumed elements of the
class can compare themselves to each other. So we'll come back to that in a
minute. Here it is. It's a minute later. Another module called 'Comparable' assumes
that if your class implements the Space Ship Operator, so-called because some
people think it looks like a spaceship from the 80's video games and that's when,
I guess, these names were invented. You can easily see that if you have this
operate. By the way, has everybody seen spaceship before? If you compare two
things with spaceship. It's supposed to return -one if the thing on the left side
is smaller, +one if the thing on right side is smaller. Or zero if the things are
equal. Right, and the notions of equality and greater than or less than are up to
the class. As with everything else. It would be good if the class obeyed the
[inaudible] equality. Right so if a is less than b and b is less than c. It would
be good if a is less than c. Otherwise, sorting becomes really interesting. But,
assuming your class implements this. You can see how you get all these others
right. The first four or five are kind of obvious. But you also get between. Right,
between is really just a combination of it's. If it's less than or equal than one
thing and greater than or equal to another thing. So, if your class implements the
spaceship operator, and it implements each, then you can also take, advantage of
the sort method enumerable. Which just means the things in the collection have to
respond to spaceship. Otherwise, how could you compare them? And the collection
itself has to have an each operator. So that you can pull out elements of that
collection one at a time. And it's a really nice way to reuse behaviors because
for imagine, imagine for a moment that you wanted to make a new data structure like
let's say a binary tree or something like that. You can easily imagine if you have
an each operator defined on your collection that returns elements one at a
time, now you get all this other stuff for free. Right, by mixing innumerable and
comparable, I can now sort your class. I can do [inaudible] over it. I can do you
know things like between. I can do all kinds of other great tricks. So that's the
kind of reuse that modules give you. So the, the, kind of key method here, or the
key idea here is that the class of the objects doesn't matter, right? It doesn't
matter if the collection is an array or a list of lines in a file. As long as it
responds to each, that's all Ruby cares about. And for things like sorting when
you mix in comparable, it doesn't matter what class or what type the collection's
elements are as long as the implements spaceship can be compared to each other.
In fact, although I don't necessarily recommend this, you could actually create
a collection that had objects of a bunch of different classes as long as the
receiver of the spaceship operator does something sensible. Right? So for example
you could imitate things like suppose, what happens if you try to compare a
string with nil? Well, if you just try it, you'll get an error because you can't
compare a string to nil, but if you change the spaceship operator implementation in
string and just say, if the receipt, if the other argument is nil, nil is always
less than any string, then, now you can sort an array and you won't run into any
problems. So what's kind of, here's a you know, more dramatic example. Suppose I
want to sort an entire file. Well, file that open that gives me back an I/O object
which is the sort of basic object of doing any kind of I/O in ruby. I/o objects
implement that each method and in the case of opening a file each returns the lines
of the file one at a time and for those of you who've done other languages what, you
know, what does a line mean? By default it means a new line at the end but you can
change that. So. I could say, file.open pass a file name and I can sort the result
right, because what comes back from file name.open is an object that responds to
each. And happily, the file IO collection mixes in the innumerable class so I can do
all these good stuff. And again, what are lines of a file? Well, they're strings. No
problem because string definitely implements spaceship. And the collection
itself implements each. Obviously a file is not an array but that doesn't matter
right? What this says is, if I can call each and get lines of a file out then it
quacks like an array. And that's kinda where the name duck typing comes from.
Here's another example, which lines of a file begin with a vowel? In case you're
writing you know wheel of fortune or something like that for, for you know in
ruby. I can open the file and I can call this is one of the many collection methods
that are in that table in chapter three. All it does is select out the elements of
the collection that for which some predicate is true. And in this case the
predicate is doesn't match the regular expression a, e, I, o, u at the beginning
of a line. Alright, so which lines of the file begin with a vowel? Let us see what
it would like if we modified the account class to implement spaceship. Okay, let me
make the text a little bigger so it's, legible on the screen. Okay. So what did I
do? I did two key things, right?. First, I defined the space ship operator. Right?
And remember, this is kind of where the knowledge that everything is intact in
sugar for, for a method call in Ruby is really important. So when I write, A space
ship B, remember that's just syntactic sugar for calling the space ship operator
on A as a receiver passing B as an argument. Okay, so that means that in the
context of an account, the thing that's going to be receiving the call is an
instance of account, so I'm defining. Space ship as an instance method and the
thing that's gonna be passed to it is the other account that I want to compare it
with. And of course the comparison itself is trivial, I am just comparing it, the
balance of myself the left hand side of spaceship is greater than equal to or less
than the balance on the other side right and of course you think or a naive person
but not your, cuz you are so smart. We think wait a minute. Isn't this like
infinite recursion? But it's not, right, because in self.balance
spaceshipother.balance, remember that balance is actually just a numeric value.
So in this line we're calling the spaceship operator on integers where it's
already defined as opposed to this which is spaceship operator that we're defining
on accounts. The other thing I did here is I included Comparable. For just this
particular example with my only goal was to be able to do the example on the
question slide. I wouldn't necessarily have to do this. But generally speaking,
if you're gonna define the spaceship operator in your class with the goal of
making it comparable, you might as well mix in Comparable. Because you get these
other methods for free and people sort of expect that if your things can compare
themselves to each other, that you would've had the courtesy to provide the,
the method to do so. So, by virtue of doing that relatively simple hack An
account now quacks like a numeric. Right? You can compare them and the comparison
does exactly what it would do it they were numbers. So, given that we've sort of seen
two ways that you can reuse things, right? There's modules and there's classes and
they have this kind of weird orthogonal second cousins behavior. So, when do use a
module for something? When do you put things in a module vs. When do you put
things in a class? So again we're going to see many examples over the course of the
class but here's kind of the higher order bit. Models, modules re use behaviors. So
if it's a high level behavior that logically can apply to many classes. Like
the idea of being comparable or the idea of being able to numerate Every element in
the collection. Then you use a module. The mechanism is a mix in. And the keyword
include is how you trigger it. Because what it's saying is. I'm going to assume A
few things about The base class, and I'm gonna build a whole bunch of behaviors on
top of that without worrying about how the base class implements its behavior. Right,
so innumerable just cares that the base class provides each. It doesn't care at
all how it implements each. So this is about behavior sharing. In contrast,
inheritance, as most people practice it, is about implementation sharing. All
right, when you subclass from another class. The intention is, you're going to
reuse a lot of the super class's methods, maybe you can override a few of them or,
or add some new methods of your own, But, if you find that you're sub-classing and
reusing very little of the methods and overriding a lot of them. Then you need to
ask yourself whether inheritance is the right mechanism because the goal is reuse
and if you're not reusing stuff, then what are you doing? So remarkably often, as
we'll see, composition which means here's a class, but I'm gonna mix behaviors in,
turns out to give you a cleaner architecture than inheritance. Inheritance
is sort of over used and I would say, although Ruby has inheritance, you see it
much less often. And as we do examples, you'll come to see why that's the case.