p1 = Point.new(3, 5)
p2 = p1
p3 = Point.new(3, 5)
I asked what Ruby would say when we asked whether these different
objects were equal to each other using the == operator. We found that
p1 and p2 were equal but not p1 and p3. By default, Ruby uses the
most strict definition of equality, which is object equality (the
variables point to the same object).In the previous lecture we looked at an implementation of the spaceship operator (<=>) for the Point class. Normally we would also say:
include Comparable
I left that off of the class so that we could do it in the interpreter. This
allowed us to see how the methods changed when we added this to the class.
>> p = Point.new(3, 5)
=> #
>> m1 = p.methods
=>
[:y,
...
>> class Point
>> include Comparable
>> end
=> Point
>> m2 = p.methods
=>
[:y,
...
>> m2.length - m1.length
=> 6
>> m2 - m1
=> [:clamp, :<=, :>=, :<, :>, :between?]
But we found that it also redefined equality:
>> p1 == p2
=> true
>> p1 == p3
=> true
>> p1 == Point.new(5, 3)
=> true
Before the definition was too strict. Not it is too loose. It is saying that
the point (3, 5) is equal to the point (5, 3). That's because the spaceship
operator uses distance from the origin to determine the order of values. So we
introduced our own version of the equals operator:
def == other
return (x == other.x and y == other.y)
end
I spent a little time discussing a subclass called Point3D that would include a
z-coordinate. We started with this definition:
class Point3D < Point
def initialize(x = 0, y = 0, z = 0)
super(x, y)
@z = z
end
def to_s
super[0..-2] + ", " + z.to_s + ")"
end
protected
attr_reader :z
attr_writer :z
end
This is a well-known example of where you run into trouble with an equality
operator and an inheritance relationship. This class inherits a version of the
equals operator that looks at just the x an y coordinates. We can fix that by
having it also check the z coordinate
def == other
return (self and z == other.z)
end
This gives the right answer when you compare two Point3D objects, but it gives
the wrong answer when you have a regular Point comparing itself to a Point3D.
It can return true when it shouldn't because it doesn't check the
z-coordinate. And if you turn the comparison around and ask the Point3D
whether it equals the regular point, you can an error when it tries to access
the z component of the simple Point. There is not a great solution to this
problem other than to avoid inheritance in this case.The other common Ruby mixin in Enumerable. It defines a series of methods built on top of the each method. Remember that we defined a MyRange class with an each method:
class MyRange
def initialize(first, last)
@first = first
@last = last
end
def each
i = @first
while i <= @last
yield i
i += 1
end
end
end
By including the Enumerable mixin, we get 58 new methods. I demonstrated it
this way in the interpreter:
>> x = MyRange.new(1, 10)
=> #<MyRange:0x00007f9593d1df00 @first=1, @last=10>
>> m1 = x.methods
=>
[:first,
...
>> class MyRange
>> include Enumerable
>> end
=> MyRange
>> m2 = x.methods
=>
[:first,
...
>> m1.length
=> 69
>> m2.length
=> 127
>> m2.length - m1.length
=> 58
And we can see the list of the actual methods by saying:
>> m2 - m1
=>
[:slice_after, :slice_when, :chunk_while, :sum, :uniq, :chain, :lazy,
:to_h, :include?, :max, :min, :to_set, :find, :to_a, :entries, :sort,
:sort_by, :grep, :grep_v, :count, :detect, :find_index, :find_all,
:select, :filter_map, :reject, :collect, :map, :flat_map,
:collect_concat, :inject, :reduce, :partition, :group_by, :tally,
:all?, :any?, :one?, :none?, :minmax, :min_by, :max_by, :minmax_by,
:member?, :each_with_index, :reverse_each, :each_entry, :each_slice,
:each_cons, :each_with_object, :zip, :take, :take_while, :drop,
:drop_while, :cycle, :chunk, :slice_before]
>>
So by defining a single method (each) and using the mixin, we can get 58 very
useful methods. This is certainly easier from a programming point of view and
it is easier to create consistency across different types of objects when they
all refer to the same mixin.Then I switched to a topic we didn't have time for earlier when we discussed Scheme. I wanted to explore how mutation works in Scheme. We've seen that you can call define to bind and rebind a value:
> (define x 34)
> x
34
> (define x 18.9)
> x
18.9
But this all takes place at the top level environment. For example, if I have
a function that tries to rebind x, it doesn't work:
> (define (foo) (define x 13) x)
> (foo)
13
> x
18.9
In this case it defines a local x that has value 13, but that has no effect on
the global x in the top-level environment.There is an alternative. You need to use define to introduce new identifiers into the environment, but once introduced, you can call set! to change their values. The convention in Scheme is to have an exclamation mark at the end of the name of any function that is a mutating function. So if we had written our local function using set! instead, then we end up changing the actual global variable:
> (define (foo) (set! x 13) x)
> (foo)
13
> x
13
I then talked about how the mutating functions can be used to create a local
variable that only certain functions have access to:
(define incr null)
(define get null)
(define m 3)
(let ((n 0))
(set! incr (lambda (i) (set! n (+ n i m))))
(set! get (lambda () n)))
We saw that the get function would report the value of n and the incr
function was able to increment n, but we can't access n from the top-level
environment:
> (get)
0
> (incr 3)
> (get)
6
> (incr 5)
> (get)
14
> n
* reference to undefined identifier: n
The call on let introduces a local scope in which n is declared. Each lambda
that appears inside the let causes Scheme to set up a closure that has a
reference to that inner environment that has the variable n.We then spent time discussing a technique known as memoization in which we remember what values were returned by various calls on a function. The idea is similar to the idea of caching. As a function is called, we keep track of what values were returned for each call, memoizing the result. This is a useful technique that can be used in any programming language. It is particularly helpful for speeding up recursive definitions that compute the same value multiple times.
In a previous lecture we wrote an inefficient version of the fib function for computing Fibonacci numbers:
(define (fib n)
(if (<= n 1)
1
(+ (fib (- n 1)) (fib (- n 2)))))
The complexity of this function is exponential. We were able to rewrite it
using a more iterative approach, but we can use memoization instead. We set up
a global variable called answers with the first two values:
(define answers '((1 . 1) (0 . 1)))
Then in writing the function, we first did a lookup against our list of
answers. If we've already computed that Fibonacci number, then we just return
its value. Otherwise we compute an answer and store it in our list of answers
so that we never compute it again:
(define (fib n)
(let ([match (assoc n answers)])
(if match
(cdr match)
(let ([new-answer (+ (fib (- n 1)) (fib (- n 2)))])
(set! answers (cons (cons n new-answer) answers))
new-answer))))
We saw that as we asked for higher values of fib, our global variable ended up
with more memoized results:
> answers
((1 . 1) (0 . 1))
> (fib 5)
8
> answers
((5 . 8) (4 . 5) (3 . 3) (2 . 2) (1 . 1) (0 . 1))
> (fib 20)
10946
> answers
((20 . 10946)
(19 . 6765)
(18 . 4181)
(17 . 2584)
(16 . 1597)
(15 . 987)
(14 . 610)
(13 . 377)
(12 . 233)
(11 . 144)
(10 . 89)
(9 . 55)
(8 . 34)
(7 . 21)
(6 . 13)
(5 . 8)
(4 . 5)
(3 . 3)
(2 . 2)
(1 . 1)
(0 . 1))
We then looked at how to localize answers. Instead of using a global variable,
we can use a let inside the function and define a helper function that has
access to it:
(define (fib n)
(let ([answers '((1 . 1) (0 . 1))])
(define (helper n)
(let ((match (assoc n answers)))
(if match
(cdr match)
(let ((new-answer (+ (helper (- n 1)) (helper (- n 2)))))
(set! answers (cons (cons n new-answer) answers))
new-answer))))
(helper n)))
This is a pretty good answer in that every time you call fib, it sets up a
variable called answers that memoizes the results. But we can do even better.
Why reconstruct answers every time you call fib? If we instead want to
construct the answers just once, then we don't want fib to be defined as a
normal function as we have done above. Instead, we want to set it up just
once and make it a function that has access to answers. But we've already
done that. Our helper function is the function we want fib to be, so all we
have to do is assign fib to be the helper:
(define fib
(let ([answers '((1 . 1) (0 . 1))])
(define (helper n)
(let ([match (assoc n answers)])
(if match
(cdr match)
(let ([new-answer (+ (helper (- n 1)) (helper (- n 2)))])
(set! answers (cons (cons n new-answer) answers))
new-answer))))
helper))
This assignment happens just once, so that we construct the answers just once.
That means that we'll never end up computing the same value of fib more than
once.