Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Last active December 18, 2018 16:53
Show Gist options
  • Save JoshCheek/f2f4fed0c184ad9dbe98 to your computer and use it in GitHub Desktop.
Save JoshCheek/f2f4fed0c184ad9dbe98 to your computer and use it in GitHub Desktop.
Object Model Notes for 1503

Object Model

Bindings

  • are nodes in the stack
  • store local_variables
  • have a return value
  • have a self

Classes

  • are a node in a linked list called "inheritance"
  • they store methods

Instances (aka Objects)

  • Has a link to the head of the list
  • The "class" is the head of the list
  • Store @instance_variables

Code to execute

This code is somewhere out there in Ruby

class Class
  def new(*args)
    instance = allocate()
    instance.__send__ :initialize, *args
    instance
  end
end

Here is our code:

class User
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name  = last_name
  end

  def full_name
    "#{first_name} #{last_name}"
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

user = User.new('Bozo', 'Clown')
user.full_name # => "Bozo Clown"

What happens?

  • Start with one binding on the stack, its self is pointing at an object called "main"
  • We hit class User
    • Create a new class named User, it has no methods
    • push a binding to the stack, with self pointing at User
  • We hit def initialize
    • Add initialize to User's methods
  • We hit def full_name
    • Add full_name to User's methods
  • We hit def first_name
    • Add first_name to User's methods
  • We hit def last_name
    • Add last_name to User's methods
  • We hit the end of the User class
    • Pop the second binding off the stack, we're back in main
  • We hit User.new('Bozo', 'Clown')
    • Create the string 'Bozo', an object whose class is String
    • Create the string 'Clown', an object whose class is String
    • Invoke new on User
      • Go searching for it at the class pointer
      • User's class is Class, and we find the method there
      • Push a binding onto the stack, with self set to User, the class
      • Add a local variable args which is technically pointing at an array containing 'Bozo' and 'Clown'
      • We'll magic allocate away, what it does is gives us a new object
        • That object's class is our self, which is User
      • Store the object in a local variable instance, in the current binding
      • We hit instance.initialize(*args)
      • Look up the local variable instance, and the local variable args
      • Find the method:
        • get classy on instance to locate User
        • Add a binding to the stack with self set to the user instance was pointing at
        • Add the local variable first_name to the third binding, and point at 'Bozo', the first value in the args
        • Add the local variable last_name to the third binding, and point at 'Clown', the last value in the args
        • We hit @first_name = first_name
          • From our binding, look up first_name to find 'Bozo'
          • From our binding, look up self to find our user
          • Add the instance variable @first_name to our user, it points at 'Bozo', the value it got from the local variable.
        • We hit @last_name = last_name
          • From our binding, look up last_name to find 'Clown'
          • From our binding, look up self to find our user
          • Add the instance variable @last_name to our user, it points at 'Clown'.
        • We leave the initialize method
          • Pop the third binding off the stack (erase it)
          • We are now back in the second binding, in the new method
      • We hit instance and look up the local variable to find our user
    • We leave the new method
      • Pop the second binding off the stack (erase it)
      • We return to the first binding, at user = User.new('Bozo', 'Clown')
      • We now set the user that was returned into a local variable user in the first binding
  • We hit user.full_name
    • Look up user to find our instance
    • Call full_name
      • From our user, get classy to find the User class
      • We find its full_name method here
      • Add a binding onto the stack, with self pointing at our user (because that's the object we called the method on)
        • We hit "#{first_name} #{last_name}"
        • first_name is not a local, so it must be a method, call it:
          • From the binding, find self, then get classy, and we find the method
          • Add a binding to the stack with self set to the second binding's self (the user)
          • We hit @first_name
            • From the binding, follow self to find the user
            • Look up its @first_name instance variable to find 'Bozo'
          • We return the to the caller
            • pop the binding off the stack
        • last_name is not a local, so it must be a method, call it:
          • From the binding, find self, then get classy, and we find the method
          • Add a binding to the stack with self set to the second binding's self (the user)
          • We hit @flast_name
            • From the binding, follow self to find the user
            • Look up its @last_name instance variable to find 'Clown'
          • We return the to the caller
            • pop the binding off the stack
          • We now put 'Bozo' and 'Clown' into a new string
            • Create a new object for "Bozo Clown", its class is String
      • We leave the full_name method
        • Pop the second binding off the stack
        • Return "Bozo Clown" to the user.full_name
  • Our program ends

Modules

Modules are bags of methods that can be included in another class's inheritance hierarchy. When you include the module, Ruby creates an invisible class whose method table is pointing at the module's method table, and inserts it at the superclass.

Here is the code example we made up in class:

module Talkative
  def speak
    "I said: #{@sound}"
  end
end

module Communicative
  def greet
    "Hi, my name is #{@name}"
  end
end

class Firetruck
  Firetruck.ancestors # => [Firetruck, Object, Kernel, BasicObject]
  include Talkative
  Firetruck.ancestors # => [Firetruck, Talkative, Object, Kernel, BasicObject]

  def initialize(sound)
    @sound = sound
  end
end

class Animal
  include Communicative
  include Talkative

  def initialize(name, sound)
    @name = name
    @sound = sound
  end
end

class Person
  include Communicative
  def initialize(name)
    @name = name
  end
end

Person.new("Bill").greet           # => "Hi, my name is Bill"
Animal.new("Rusty", "woof").greet  # => "Hi, my name is Rusty"
Animal.new("Rusty", "woof").speak  # => "I said: woof"
Firetruck.new("wroooowoooo").speak # => "I said: wroooowoooo"

There is an attached picture of the class structure it creates.

Challenges

Here are some challenges to exercise this knowledge.

# Run these with `command + option + n`, NOT `b`


# =====  Toplevel methods are defined where?  =====
def rawr!
  "#{self} says: rawr!"
end
public :rawr!

# *****
# What class is rawr! defined in
# method(:rawr!).owner # =>

# *****
# Think of some objects that inherit from this class
# and show you can call it on them
# <your example here>


# =====  What do bindings tell us?  =====

# Here we have a method that returns an object wrapping the binding it executed in
# We can evaluate code within the context of that binding to find out about it
def get_binding
  a = 123
  binding
end
b = get_binding

# *****
# What is self in that binding?
# b.eval 'self' # =>

# *****
# What are its local variables?
# b.eval 'local_variables' # =>

# *****
# What is the value of a?
a = 99
# b.eval 'a' # =>

# *****
# The binding tracks what `self` is, why does this matter?
# What will we see the second time we run this?
b.eval 'instance_variables'
@abc = 123
# b.eval 'instance_variables' # =>


# =====  Calling methods pushes bindings onto the callstack  =====
# We can see the callstack with the `caller` method.
def you_rang?
  # *****
  # How many bindings are on the callstack?
  # caller.size # =>

  # *****
  # Where did we call it from?
  # caller # =>
end
you_rang?

# *****
# What will we see, before and after the calls of each of these lines below?
def call1
  caller.size
  call2
  caller.size
end

def call2
  caller.size
  call3
  caller.size
end

def call3
  caller.size
  "zomg".call4
  caller.size
end

class String
  def call4
    caller.size
  end
end

caller.size
call1
caller.size

# =====  The last line of a method is returned to the caller  =====

# *****
# What will we see returned from call1?
def call1
  call2
  call3
end

def call2
  222
end

def call3
  call4
  333
end

def call4
  444
end

call1


# =====  Arguments are evaluated first  =====
# Each of the expressions below will evaluate its argument first,
# then it will evaluate itself. What will we see on eac line?

def call1(n)
  # n # =>
  1 + n
end

def call2(n)
  # n # =>
  n + 2
end

def call3(n)
  # n # =>
  n + 3
end

# call3(
#   call2(
#     call1(0)  # =>
#   )           # =>
# )             # =>


# =====  Instance Variables  =====
# An instance is a collection of instance variables with a pointer to its class,
# it is like the base of a linked list, pointing at the first node in the list
# (typically named "head")

# What will we see returned from 159?
class Fruit
  def initialize(banana)
    @apple = banana
    @pear  = "#{banana} boat"
  end

  def pear
    @apple
  end
end

fruit = Fruit.new('orange')
# fruit.pear # =>


# =====  Attr Whatevers  =====
# The attr_accessor (et all) define define methods that get/set the instance variables of the same name
class Fruit
  attr_accessor :apple

  def initialize(banana)
    @apple = banana
    self.apple = "#{banana} boat"
  end

  def pear
    @apple
  end
end

#*****
# What will we see on these two lines?
fruit = Fruit.new('orange')
# fruit.pear   # =>
# fruit.apple  # =>

# *****
# We can punch the object in the face and rearrange its guts with metaprogramming
# Here, I go into it and set @apple = 'pineapple'
# What will we see in the following expressions?
fruit.instance_variable_set '@apple', 'pineapple'
# fruit.pear  # =>
# fruit.apple # =>

fruit.apple = 'mango'
# fruit.pear  # =>
# fruit.apple # =>


# ===== Classes are a linked list called inheritance  =====
# Get classy, stay super

class A
  def zomg
    'a'
  end
end

class B < A
  def zomg
    'b'
  end
end

class C < B
end

class D < A
end

# *****
# What will we see on each of these lines?
# A.new.zomg # =>
# B.new.zomg # =>
# C.new.zomg # =>
# D.new.zomg # =>

# =====  We can use super to access the definition in the superclass chain  =====
class C1
  def m
    '1'
  end
end

class C2 < C1
  def m
    super + '2'
  end
end

class C3 < C2
  def m
    super + '3'
  end
end

# *****
# What will we see on each of these lines?
# C1.new.m # =>
# C2.new.m # =>
# C3.new.m # =>


# =====  Once again, but with malice  =====
class W
  def zomg() '1' + wtf  end
  def wtf()  '2'        end
  def bbq()  '3'        end
end

class X < W
  def zomg() super      end
  def wtf()  '4' + bbq  end
  def bbq()  super      end
end

class Y < X
  def zomg() '6' + super  end
  def wtf()  '7' + super  end
  def bbq()  '8' + super  end
end

#*****
# W.new.zomg # =>
# X.new.zomg # =>
# Y.new.zomg # =>


# =====  Chaining method calls  =====
# When we call a method, we call it on whatever the expression evaluates to
# This means that chaining methods leads to methods called on the return value of the previous expression

#*****
# What will this expression evaluate to?
# 'abc'.upcase.reverse.downcase.chars.first # =>

#*****
# It doesn't matter if you split the expression across lines, what will we see?
# 'abc'       # =>
#   .upcase   # =>
#   .reverse  # =>
#   .downcase # =>
#   .chars    # =>
#   .first    # =>

#*****
# The dot can go on the preceeding line, or the current line
# 'abc'.      # =>
#   upcase.   # =>
#   reverse.  # =>
#   downcase. # =>
#   chars.    # =>
#   first     # =>

#*****
# We can get all funky with the dot (best practices, ya know?)
# 'abc'.              # =>
#   upcase  .reverse  # =>
#   .downcase.        # =>
#   chars             # =>
# .  first            # =>


# =====  Mixing and matching args and chaining  =====
#*****
# Uncomment each of the following lines, what will we see?
def z(a)
  # a + a # =>
end

def w(a)
  # a         # =>
  #  .reverse # =>
end

# (w (z 'abc').upcase).chars # =>


# =====  Modules  =====
# When you include a module, it makes a class and inserts it into the hierarchy
class A
  def wat
    'a'
  end
end

module WatInTheWorld
  def wat
    '!?' + super
  end
end

class B < A
  include WatInTheWorld
  def wat
    'b' + super
  end
end

#*****
# What will we see here?
# B.new.wat # =>


# =====  Silence!! DEstroy him!!  =====
# Dew! Bew! Dew-dew-dew! Bew!

module InSpace
  attr_reader :current_status
  def initialize(current_status, *whatevz)
    @current_status = current_status
    super(*whatevz)
  end
end

class Human
  attr_reader :name
  def initialize(name)
    @name = name
  end
end

class Student < Human
  include InSpace
  attr_reader :lesson
  def initialize(lesson, *o_O)
    @lesson = lesson
    super *o_O
  end
end

students_in_space = Student.new(
  "The future is quite different to the present",
  "Though one thing we have in common with the present is we still call it the present, even though its the future",
  "What you call 'the present', we call 'the past', so... you guys are way behind"
)

#*****
# We used poisonous gasses, (with traces of lead)
# and we poisoned their asses (actually, their lungs)
# students_in_space.current_status # =>
# students_in_space.name           # =>
# students_in_space.lesson         # =>

Singleton Classes

We can define a method on some particlar object

a = "a"
def a.x
  "I am: #{self.inspect}"
end
a               # => "a"
a.x             # => "I am: \"a\""
'a'.x rescue $! # => #<NoMethodError: undefined method `x' for "a":String>

This places it on the object's singleton class

a.singleton_class.instance_methods(false) # => [:x]

The singleton class is a class specifically for that object, The object's class pointer points at it, and its superclass points to the original class So the object can call those methods, and all the old ones, but no other object can call its methods.

a.singleton_class # => #<Class:#<String:0x007fc9cd06a9c0>>
 .superclass      # => String

The most common way you'll see this done defining methods on self within a class.

class String
  def self.empty
    ''
  end
end

String.empty # => ""

Extending a module onto an object

If we include a module into the singleton class, that will bet placed between it and the original class Here, we have to use ancestors, because superclass lies.

module M
  def zomg
    'zomg!'
  end
end

a = "a"
a.singleton_class.class_eval { include M }
a.singleton_class   # => #<Class:#<String:0x007fc9cd0699d0>>
 .ancestors.take(3) # => [#<Class:#<String:0x007fc9cd0699d0>>, M, String]

A common way to open the singleton class is with class << ...

a.singleton_class # => #<Class:#<String:0x007fc9cd0699d0>>
class << a
  self # => #<Class:#<String:0x007fc9cd0699d0>>
  include M
end

This pattern is common enough that there is a method to do it for you: extend.

a = "a"
a.zomg rescue $! # => #<NoMethodError: undefined method `zomg' for "a":String>
a.extend M
a.zomg # => "zomg!"

Meandering around the object model

Not something you should do in your code, but we can test our understanding of the object model by anticipating what each of these methods will return.

a = 'a'

a.singleton_class # => #<Class:#<String:0x007fdac19dc738>>
 .singleton_class # => #<Class:#<Class:#<String:0x007fdac19dc738>>>
 .singleton_class # => #<Class:#<Class:#<Class:#<String:0x007fdac19dc738>>>>
 .singleton_class # => #<Class:#<Class:#<Class:#<Class:#<String:0x007fdac19dc738>>>>>
 .class           # => Class
 .superclass      # => Module
 .superclass      # => Object
 .class           # => Class
 .new             # => #<Class:0x007fdac19d7878>
 .new             # => #<#<Class:0x007fdac19d7878>:0x007fdac19d75f8>
 .class           # => #<Class:0x007fdac19d7878>
 .class           # => Class
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment