
Abstraction
A numerator and a denominator.
Two variables: a numerator and a denominator.
Yes, but we’ll need a data structure. How about… a pair of numbers, wrapped in an array!
one_ninth = [1, 9]
By creating an array, using
[
and]
.
By multiplying the numerator. We leave the denominator as-is.
def fraction_scale(fraction, scale) new_numerator = fraction[0] * scale new_denominator = fraction[1] [new_numerator, new_denominator] end
By creating an array, using
[
and]
.
With array indexing operations, using
[0]
and[1]
.
Because our underlying data type is an array.
We multiply the numerators and the denominators.
def fraction_multiply(a, b) new_numerator = a[0] * b[0] new_denominator = a[1] * b[1] [new_numerator, new_denominator] end
Same as before: by creating an array, using
[
and]
.
Same as before: with the array indexing operations
[0]
and[1]
.
We are merely reinforcing our mental model of fractions.
Phew.
This one’s trickier. We need to do a bit of arithmetic. It all starts with getting a common denominator.
def fraction_add(a, b) new_numerator = a[0] * b[1] + b[0] * a[1] new_denominator = a[1] * b[1] [new_numerator, new_denominator] end
Two ninths!
Well, sort of.
p fraction_add([1, 9], [1, 9]) # => [18, 81]
There are two buried in there. We just need to simplify by finding the great common divisor.
def fraction_add(a, b) new_numerator = a[0] * b[1] + b[0] * a[1] new_denominator = a[1] * b[1] # NEW: simplify gcd = new_numerator.gcd(new_denominator) [new_numerator / gcd, new_denominator / gcd] end p fraction_add([1, 9], [1, 9]) # => [2, 9]
One ninth!
Sigh, almost.
p fraction_multiply([1, 2], [2, 9]) # => [2, 18]
It’s there, we just need to simplify.
def fraction_multiply(a, b) new_numerator = a[0] * b[0] new_denominator = a[1] * b[1] # NEW: simplify gcd = new_numerator.gcd(new_denominator) [new_numerator / gcd, new_denominator / gcd] end p fraction_multiply([1, 2], [2, 9]) # => [1, 9]
We are merely reinforcing our mental model of fractions.
We should get [1, 1], but… we don’t.
p fraction_scale(one_ninth, 9) # => [9, 9]
We need to simplify.
def fraction_scale(fraction, scale) new_numerator = fraction[0] * scale new_denominator = fraction[1] # NEW: simplify gcd = new_numerator.gcd(new_denominator) [new_numerator / gcd, new_denominator / gcd] end p fraction_scale(one_ninth, 9) # => [1, 1]
By creating an array, using
[
and]
. But each time, we gather the greatest common divisor to simplify it.
We have reinforced our mental model of fractions.
But… we can make mistakes.
We can try!
def create_fraction(numerator, denominator) gcd = numerator.gcd(denominator) [numerator / gcd, denominator / gcd] end p create_fraction(9, 9) # => [1, 1]
We can try!
def fraction_scale(fraction, scale) new_numerator = fraction[0] * scale new_denominator = fraction[1] # NEW: use our abstraction create_fraction(new_numerator, new_denominator) end p fraction_scale(one_ninth, 9) # => [1, 1]
By calling create_fraction with a numerator and a denominator.
We can try!
def fraction_add(a, b) new_numerator = a[0] * b[1] + b[0] * a[1] new_denominator = a[1] * b[1] create_fraction(new_numerator, new_denominator) end p fraction_add([1, 9], [1, 9]) # => [2, 9] def fraction_multiply(a, b) new_numerator = a[0] * b[0] new_denominator = a[1] * b[1] create_fraction(new_numerator, new_denominator) end p fraction_multiply([1, 2], [2, 9]) # => [1, 9]
And the code is much cleaner!
By calling create_fraction with a numerator and a denominator.
Oh, we’re calling fraction_add and fraction_multiply with arrays as arguments. We can clean that up.
# OLD fraction_multiply([1, 2], [2, 9]) # NEW fraction_multiply( create_fraction(1, 2), create_fraction(2, 9) )
It’s a lot of characters, though.
By calling create_fraction with a numerator and a denominator.
The arrays are there, but we are not creating them by hand.
With array indexing operations, using
[0]
and[1]
.def fraction_multiply(a, b) new_numerator = a[0] * b[0] new_denominator = a[1] * b[1] create_fraction(new_numerator, new_denominator) end
A fraction is a numerator and a divider. It’s somewhat obvious to use
[0]
and[1]
respectively.
We can continue to change our code! How should we represent them?
Great idea, that syntax is quite nice! Our fields are nicely labeled as well.
one_ninth = { numerator: 1, denominator: 9, }
By creating a Hash using
{
,}
, and listing our fields such asnumerator:
.
Oh! We can update our constructor.
def create_fraction(numerator, denominator) gcd = numerator.gcd(denominator) # OLD # [numerator / gcd, denominator / gcd] # NEW { numerator: numerator / gcd, denominator: denominator / gcd } end one_ninth = create_fraction(1, 9) p one_ninth[:numerator] # => 1
By accessing the fields of the hash such as
one_ninth[:numerator]
.
Because our underlying data type is a hash.
No. The act of writing create_fraction(a, b) has not changed. We do not need to update any call sites.
No, our hashes are nicely abstracted away. Our code is nice and clean.
def fraction_multiply(a, b) new_numerator = a[0] * b[0] new_denominator = a[1] * b[1] create_fraction(new_numerator, new_denominator) end p fraction_multiply( create_fraction(1, 2), create_fraction(2, 9) )
It… sadly does not.
data.rb:29:in 'Object#fraction_multiply': undefined method '*' for nil (NoMethodError) new_numerator = a[0] * b[0] ^ from data.rb:34:in '<main>'
They are trying to access the numerator and denominator, but we’ve changed how the numerator and denominator and stored internally.
a[0]
no longer makes sense. We need the:numerator
field, not the0
th one.
By accessing the named fields of the hash with
[
and a field such as:numerator
.def fraction_multiply(a, b) # OLD # new_numerator = a[0] * b[0] # new_denominator = a[1] * b[1] # NEW new_numerator = a[:numerator] * b[:numerator] new_denominator = a[:denominator] * b[:denominator] create_fraction(new_numerator, new_denominator) end p fraction_multiply( create_fraction(1, 2), create_fraction(2, 9) ) # {numerator: 1, denominator: 9}
In all of our functions, just as we did before we introduced create_fraction.
We’ve abstracted away how we create fractions.
We certainly know how to do that!
def create_fraction(numerator, denominator) gcd = numerator.gcd(denominator) { numerator: numerator / gcd, denominator: denominator / gcd } end # NEW: Get the numerator def numer(fraction) fraction[:numerator] end # NEW: Get the denominator def denom(fraction) fraction[:denominator] end
We can! It looks quite nice next to our creator_fraction constructor.
one_ninth = create_fraction(1, 9) p numer(one_ninth) # => 1 p denom(one_ninth) # => 9
We can!
def fraction_multiply(a, b) # OLD # new_numerator = a[:numerator] * b[:numerator] # new_denominator = a[:denominator] * b[:denominator] # NEW new_numerator = numer(a) * numer(b) new_denominator = denom(a) * denom(b) create_fraction(new_numerator, new_denominator) end p fraction_multiply( create_fraction(1, 2), create_fraction(2, 9) ) # => {numerator: 1, denominator: 9}
We can use our numerator and denominator getters like so:
def fraction_scale(fraction, scale) new_numerator = numer(fraction) * scale new_denominator = denom(fraction) create_fraction(new_numerator, new_denominator) end p fraction_scale(one_ninth, 9) # => { numerator: 1, denominator: 1 } def fraction_add(a, b) new_numerator = numer(a) * denom(b) + numer(b) * denom(a) new_denominator = denom(a) * denom(b) create_fraction(new_numerator, new_denominator) end p fraction_add(one_ninth, one_ninth) # => { numerator: 2, denominator: 9 }
By using create_fraction(a, b).
By using the getter functions numer and denom.
The hashes are there, but they are abstracted away from us.
I’m ready.
Shall I undo all of our changes?
We’ll start with create_fraction
def create_fraction(numerator, denominator) gcd = numerator.gcd(denominator) [numerator / gcd, denominator / gcd] end
…And our new getter functions can be updated as follows.
def numer(fraction) fraction[0] end def denom(fraction) fraction[1] end
They still work!
p fraction_scale(one_ninth, 9) # => [1, 1] p fraction_add(one_ninth, one_ninth) # => [2, 9] p fraction_multiply( create_fraction(1, 2), create_fraction(2, 9) ) # => [1, 9]
We do not need to update them.
By using create_fraction(a, b).
By using the getter functions numer and denom.
These functions are fully abstracted from the internal representation of fractions.
đź”— Notes
