Skip to content

Instantly share code, notes, and snippets.

@nicokosi
Last active August 29, 2015 14:09
Show Gist options
  • Save nicokosi/8daf1017de9b062360e6 to your computer and use it in GitHub Desktop.
Save nicokosi/8daf1017de9b062360e6 to your computer and use it in GitHub Desktop.
Notes about Coursera "Programming Languages": https://www.coursera.org/course/proglang - part 3 (Ruby)
Part 3
Introduction to Ruby {
Our focus {
Pure Object-Oriented
no primitives (even numbers)
Class-based
Every object has a class that determines behavior (like Java)
Mixins
"enhanced interfaces"
Dynamically-typed (like Racket)
Reflection
Has closures
A good scripting language
(because of syntax, semantics and scoping rules)
}
NOT out focus {
string manipulation and regular expressions
web framework (Ruby On Rails)
}
Note: we could have used Smalltalk (many similaritie, less modern), Racket (which has classes)
Basics:
# this is a comment
class MyClass ... end # class definition
def my_method(x,y) ... end # method definition
myObject = MyClass.new # object creation
myObject.my_method(3,4) # method call, parens are optional (but recommended)
self.my_method # call method on current object (like 'this' in Java) / self is implicit (optional)
x=3 # local mutable variable
new lines # either end-of-line or semi-colon ';''
We ll use 'irb' (REPL) / 'ruby' (interpreter)
}
Classes and Objects {
Rules of class-based OOP:
all values are objects
objects communicate via method calls aka messages
all objects have a class and a private state
Code {
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Classes and Objects
class A
def m1
34
end
def m2 (x,y)
z = 7
if x > y
false
else
x + y * z
end
end
end
class B
def m1
4
end
def m3 x
x.abs * 2 + self.m1
end
end
# returning self is convenient for "stringing method calls" (chaining calls)
class C
def m1
print "hi "
self
end
def m2
print "bye "
self
end
def m3
print "\n"
self
end
end
# example uses (can type into irb)
# here in a multiline comment, which is not well-known
=begin
a = A.new
thirty_four = a.m1
b = B.new
four = b.m1
forty_seven = B.new.m3 -17
thirty_one = a.m2(3,four)
c = C.new
c.m1.m2.m3.m1.m1.m3
=end
}
}
Object state {
method calls access / update object state (variables)
Instance variables
- are accessed via prefix '@'
- contain nil if not initialized
- can be set via 'initialize' method (that can have default argument) which is called after call to 'new'
NB: it s a good style to initialize instance variables in 'initialize'
Aliasing:
x=y # creates an alias => aliases refer to the same object with same state
Class variables:
shared by all class instances
syntax: prefixed by '@@'
Class constants:
public, should not be mutated
syntax:
- name start with capital letter
- outer access: MyClass::MyConstant
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Object State
class A
def m1
@foo = 0
end
def m2 x
@foo += x
end
def foo
@foo
end
end
class B
# uses initialize method, which is better than m1
# initialize can take arguments too (here providing defaults)
def initialize(f=0)
@foo = f
end
def m2 x
@foo += x
end
def foo
@foo
end
end
class C
# we now add in a class-variable, class-constant, and class-method
Dans_Age = 38
def self.reset_bar
@@bar = 0
end
def initialize(f=0)
@foo = f
end
def m2 x
@foo += x
@@bar += 1
end
def foo
@foo
end
def bar
@@bar
end
end
# example uses
=begin
x = A.new
y = A.new # different object than x
z = x # alias to x
x.foo # get back nil because instance variable not initialized
x.m2 3 # error because try to add with nil object
x.m1 # creates @foo in object x refers to
z.foo # remember, x and z are aliases
z.m2 3
x.foo
y.m1
y.m2 4
y.foo
x.foo
w = B.new 3
v = B.new
w.foo + v.foo
d = C.new 17
d.m2 5
e = C.new
e.m2 6
d.bar
forty = C::Dans_Age + d.bar
=end
}
Visibility {
Visibility of instance variables:
Hiding things is essential for modularity and abstraction
- implementation can change later without external impact
- allows "duck typing"
Object state is always private
(cannot write 'instance.@foo')
Getters/setters:
Name convention:
# convention: getter has the same name as instance variable
def foo
@foo
end
# convention: setter has the same name as instance variable + '=' suffix
def foo= x
@foo = x
end
Shorthand:
# generate 'foo' getter
attr_reader :foo
# generate 'bar' getter and setter
attr_accessor :bar
Visibility of methods:
3 visibilities:
- 'private'
- 'protected': for current class ans sub-classes
- 'public': anyone (= default visiblity)
It applies to subsequent method definitions.
example:
def foo # public
protected def bar ... # protected
def baz ... # protected
public def hello ... # public
}
A longer example {
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: A Longer Example
# Note: Ruby 1.9 has its built-in rational ("Rational"), let's build our own
class MyRational
def initialize(num,den=1) # second argument has a default
if den == 0
raise "MyRational received an inappropriate argument"
elsif den < 0 # notice non-english word elsif
@num = - num # fields created when you assign to them
@den = - den
else
@num = num # semicolons optional to separate expressions on different lines
@den = den
end
reduce # i.e., self.reduce() but private so must write reduce or reduce()
end
# first implementation of "to string"
def to_s
ans = @num.to_s
if @den != 1 # everything true except false _and_ nil objects
ans += "/"
ans += @den.to_s
end
ans
end
# second implementation of "to string" (with funny method + if syntax)
def to_s2 # using some unimportant syntax and a slightly different algorithm
dens = ""
dens = "/" + @den.to_s if @den != 1
@num.to_s + dens
end
# third implementation of "to string" (using '${}' aka interpolation)
def to_s3 # using things like Racket's quasiquote and unquote
"#{@num}#{if @den==1 then "" else "/" + @den.to_s end}"
end
# bang suffix is a conventional way to indicate mutating methods
def add! r # mutate self in-place
a = r.num # only works b/c of protected methods below
b = r.den # only works b/c of protected methods below
c = @num
d = @den
@num = (a * d) + (b * c)
@den = b * d
reduce
self # convenient for stringing calls
end
# a functional addition, so we can write r1.+ r2 to make a new rational
# and built-in syntactic sugar will work: can write r1 + r2
def + r
ans = MyRational.new(@num,@den)
ans.add! r
ans
end
protected
# there is very common sugar for this (attr_reader)
# the better way:
# attr_reader :num, :den
# protected :num, :den
# we do not want these methods public, but we cannot make them private
# because of the add! method above
def num
@num
end
def den
@den
end
private
def gcd(x,y) # recursive method calls work as expected
if x == y
x
elsif x < y
gcd(x,y-x)
else
gcd(y,x)
end
end
def reduce
if @num == 0
@den = 1
else
d = gcd(@num.abs, @den) # notice method call on number
@num = @num / d
@den = @den / d
end
end
end
# can have a top-level method (just part of Object class) for testing, etc.
def use_rationals
r1 = MyRational.new(3,4)
r2 = r1 + r1 + MyRational.new(-5,2)
puts r2.to_s
(r2.add! r1).add! (MyRational.new(1,-4))
puts r2.to_s
puts r2.to_s2
puts r2.to_s3
end
}
Everything is an Object {
Everything is an object (even 'nil')
All code are methods (top-level methods are in 'Object' class which is inherited from all classes)
Can call method on any object: will fail at runtime if not exists (dynamic "undefine error")
Examples:
3 + 4 # i.e. 3.+(4)
-5.abs # => 5
1.nonzero? # => 1
0.nonzero? # => nil
0.nil? # => false
nil.nil? # => true
nil == false # => true
Reflection:
Is often useful in REPL (and sometimes in production code)
Examples:
# list int methods:
3.methods # [:to_s, ... :even ...]
3.methods - nil.methods # list int methods that do not exist in 'nil'
# class of instance/class:
3.class # => Fixnum
MyRational.class # => Class
}
Class Definitions are Dynamic {
Classes can change while the program is running!
=> can break abstractions / make programs hard to analyze
=> can be useful for scripting / small programs (examples: modify default "to_s" for everything, add double method on integers)
Note: adding/updating/deleting a method to a class immediately adds/updates/deletes it to all existing instances!
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Class Definitions are Dynamic
require_relative './127_example' # similar to ML's use
# above line defines the MyRational class, but now we can change it
class MyRational
def double
self + self
end
end
class Fixnum
def double # add double to all integers
self + self
end
end
def m
42
end
class Object
def m
43
end
end
# this one will crash irb
# class Fixnum
# def + x
# 42
# end
# end
}
Duck Typing {
"If it walks like a duck and quacks like a dug, it's a duck!"
(or don't worry, it's like a duck)
=> assume that object has method X, not that its class is Y
+: more code reuse
-: no code equivalence, callers may asume a lot about how callees are implemented
examples:
# update coordinates for mirror effect (not a very good usage of duck typing)
# It can be applied to any object that has 'x=' and 'x' methods where result of 'x' has a '*' method
# note: documentation does not matter, only code does!
def mirror_update pt
pt.x = pt.x * (-1)
end
# a better example that can be applied to numbers, strings or our rationals
def double x
x + x
end
}
Arrays {
More flexible but less performant than in other languages
cf. Array class
a = [1,2,3,4]
a[2] # get element at index i
a[1] = 42 # set element at index to value
a[1000] # => nil (does not fail!)
a.size # => 4
a[-1] # => 4 (negative index => start from the end)
a[6] = 666 # a is now [1,2,3,4,nil,nil,666] pno problem! increment array size / set nil values if necessary
a[7] + [true, false] # concatenate arrays
a[7] | [true, false, 666] # concatenate arrays and remove duplicates
# Array size can be dynamic
s = if ... then 10 else 20 end
Array.new(s) # size is dynamic, default value is nil
Array.new(s) { 1 } # size is dynamic, default value is 1
# Arrays can be used as stacks / queues via push/pop + shift/unshift
a.push "foo" # [1,2,3,4,"foo"]
a.pop # => "foo"
# Aliases
e = a + [] # a is different from e
# Slicing
a[1,2] # => [2,3]
# map
a.each { |i| puts (i*i)}
}
Blocks {
Almost like closures (the strangest Ruby feature, but a very convenient one!)
Like closures, blocks:
- easy way to pass anonymous functions
- can take 0 or more arguments
- use lexical scope (block uses environement where it was defined)
But, they have strange things:
- any method call can have 0 or 1 block (callee might ignore it / return a error / use it...)
Syntax:
{ e } # block with no param
do e end # alternative (equivalent)
{ |x| e } # block with 1 param
{ |x,y| e } # block with 2 params
Usage:
used everywhere since standard lib use them intensivelly
ex.:
Array.new, Arrays.each, Array.map (equivalent to Array.collect), Array.any?, Array.all?, Arrray.inject ("reduce"), Array.select ("filter") etc...
('for' loops are rarly used, BTW)
sequences: (0..4).each
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Passing Blocks
3.times { puts "hello" }
[4,6,8].each { puts "hi" }
i = 7
[4,6,8].each {|x| if i > x then puts (x+1) end }
a = Array.new(5) {|i| 4*(i+1)}
a.each { puts "hi" }
a.each {|x| puts (x * 2) }
a.map {|x| x * 2 } #synonym: collect
a.any? {|x| x > 7 }
a.all? {|x| x > 7 }
a.all? # implicit are elements "true" (i.e., neither false nor nil)
a.inject(0) {|acc,elt| acc+elt }
a.select {|x| x > 7 } #non-synonym: filter
def t i
(0..i).each do |j|
print " " * j
(j..i).each {|k| print k; print " "}
print "\n"
end
end
t 9
}
Using blocks {
Let s implement our function that use blocks
Syntax:
'yield' / 'yield(arg)' # call block (fails if block is missing)
'block_given?' # check if block exists
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Using Blocks
class Foo
def initialize(max)
@max = max
end
def silly
yield(4,5) + yield(@max,@max)
end
def count base
if base > @max
raise "reached max"
elsif yield base
1
else
1 + (count(base+1) {|i| yield i}) # passing block requires re-wrapping it within {}
end
end
end
f = Foo.new(1000)
f.silly {|a,b| 2*a - b}
f.count(10) {|i| (i * i) == (34 * i)}
}
Procs {
Objects similar to blocks
Blocks are second-class expression (you cannot return them) => limited, but very convenient
Procs are first-class expression (full closure) => full power, but less easy to use (and not alwways needed)
2 ways to make Procs objects:
- (Object.)lambda
Use 'call' method to call lambdas
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Procs
a = [3,5,7,9]
# no need for Procs here
b = a.map {|x| x + 1}
i = b.count {|x| x >= 6}
# need Procs here: want an array of functions
c = a.map {|x| lambda {|y| x >= y} }
# elements of c are Proc objects with a call method
c[2].call 17
j = c.count {|x| x.call(5) }
}
Hashes and ranges {
Similar to arrays, very common.
Hashes:
Like arrays, but:
- have keys
- no natural ordering
We often use a hash arguement instead of several arguments
h = { "key1" => "hello", false => 42 }
h["key1"] # "hello"
h["not-exists"] # nil
h.keys # ["key1",false]
h.values # ["hello",42]
h.each { |k,v| ... }
Ranges:
Like arrays, for continuous numbers
0..1000 # lazy sequence
(0..1000) # [0,1,2...,1000]
Good style to:
- use ranges when possible
- use hashes if non-numeric keys better represent data
Similar methods:
- Many methods exist only for one class (e.h. 'keys' only exists for hashes) => great for duck typing
- Some methods are similar (e.g 'each', 'count', etc...)
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Hashes and Ranges
h1 = {}
h1["a"] = "Found A"
h1[false] = "Found false"
h1["a"]
h1[false]
h1[42]
h1.keys
h1.values
h1.delete("a")
h2 = {"SML"=>1, "Racket"=>2, "Ruby"=>3}
h2["SML"]
# Symbols are like strings, but cheaper. Often used with hashes.
h3 = {:sml => 1, :racket => 2, :ruby => 3}
# each for hashes best with 2-argument block
h2.each {|k,v| print k; print ": "; puts v}
# ranges
(1..100).inject {|acc,elt| acc + elt}
def foo a
a.count {|x| x*x < 50}
end
# duck typing in foo
foo [3,5,7,9]
foo (3..9)
}
Subclassing {
Subclasses, inheritance and overriding
Subclassing:
syntax:
class ColorPoint < Point
super(...) # call superclass' initialize
subclass
- inherits from all methods from superclass
- can override (part of or all of) them
- does not "inherit" fields (like in Java)
warning: subclassing has nothing to do with "type inheritance", in Ruby
reflection:
Foo.class.superclass # returns superclass
foo.is_a? Clazz # returns true if foo is an instance of Clazz (or a subclass)
foo.instance_of? Clazz # returns true if foo is an exact instance of Clazz (and not of a subclass of Clazz)
Beware: using class/is_a/instance_of is not very OOP / "duck-typing"-oriented
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Subclassing
class Point
attr_accessor :x, :y
def initialize(x,y)
@x = x
@y = y
end
def distFromOrigin
Math.sqrt(@x * @x + @y * @y) # Uses instance variables. why a module method? Less OOP :-(
end
def distFromOrigin2
Math.sqrt(x * x + y * y) # uses getter methods
end
end
class ColorPoint < Point
attr_accessor :color
# replace initialiazer of superclass
def initialize(x,y,c="clear") # or could skip this and color starts unset
super(x,y) # keyword super calls same method in superclass
@color = c
end
end
# example uses with reflection
p = Point.new(0,0)
cp = ColorPoint.new(0,0,"red")
p.class # Point
p.class.superclass # Object
cp.class # ColorPoint
cp.class.superclass # Point
cp.class.superclass.superclass # Object
cp.is_a? Point # true
cp.instance_of? Point # false
cp.is_a? ColorPoint # true
cp.instance_of? ColorPoint # true
Sometimes, subclassing is overused
alternatives:
1. add method in base class with a default value in constructor (e.g. directly add 'color' field to 'Point')
OK, but not modular: impacts all callers (it s a problem for a lib)
2. copy/paste base class implem (e.g. copy 'Point' methods into 'ColorPoint')
+: seperation, -: no code reuse
3. use delegation (e.g. 'ColorPint' has a 'Color' field)
+: encapsulation, code reuse, -: not very convenient, coupling
}
Overriding and Dynamic Dispatch {
Overriding can make a method defined in superclass call a method in the subclass
e.g. PolarPoint.distFromOrigin2() -> ThreeDPoint.distFromOrigin2 -> ThreeDPoint.z
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 7: Overriding and Dynamic Dispatch
class Point
attr_accessor :x, :y
def initialize(x,y)
@x = x
@y = y
end
def distFromOrigin
Math.sqrt(@x * @x + @y * @y) # uses instance variables directly (poor OOP style)
end
def distFromOrigin2
Math.sqrt(x * x + y * y) # uses getter methods (better OOP style)
end
end
# design question: "Is a 3D-point a 2D-point?"
# [arguably poor style here, especially in statically typed OOP languages]
class ThreeDPoint < Point
attr_accessor :z
def initialize(x,y,z)
super(x,y)
@z = z
end
def distFromOrigin
d = super
Math.sqrt(d * d + @z * @z)
end
def distFromOrigin2
d = super
Math.sqrt(d * d + z * z)
end
end
class PolarPoint < Point
# Interesting: by not calling super constructor, no x and y instance vars
# In Java/C#/Smalltalk would just have unused x and y fields
def initialize(r,theta)
@r = r
@theta = theta
end
def x
@r * Math.cos(@theta)
end
def y
@r * Math.sin(@theta)
end
def x= a
b = y # avoids multiple calls to y method
@theta = Math.atan2(b, a)
@r = Math.sqrt(a*a + b*b)
self
end
def y= b
a = x # avoid multiple calls to y method
@theta = Math.atan2(b, a)
@r = Math.sqrt(a*a + b*b)
self
end
def distFromOrigin # must override since inherited method does wrong thing
@r
end
# inherited distFromOrigin2 already works!!
end
# the key example
pp = PolarPoint.new(4,Math::PI/4)
pp.x
pp.y
pp.distFromOrigin2
}
Method-Lookup Rules, Precisely {
Dynamic dispatch:
(aka virtual methods)
= call self.method can call a method defined in subclass
Most unique characterisitic of OOP
Variable lookup:
in ML: cf. lexical scope for closures
in Racket: like ML + let/letrec
in Ruby:
similiar for local variables
but different for instance variables / class variables / methods => louk up in term of 'self'
'self' lookup:
For Ruby instance variables / methods / classes
'self' maps to current object
Lookup for instance variable '@x' => lookup to self
Lookup for class variable '@@x' => lookup to self.class
Lookup for method => dynamic dispatch
Dynamic dispatch for methods:
Rules for evaluation of e0.m(e1,...en)
1. evaluate args e0,...en to objects obj0,...,objn (e0 is special, it s called the "receiver")
2. let C be the class of obj0
3. if m is defined in C => FOUND
else recur with superclasses of C (until class Object)
else call 'method_missing' (default one in Object raises an error)
4. evaluate body of method
with arguments bound to obj1,...,objn
with 'self' bound to obj0 => DYNAMIC 'DYNAMIC DISPATCH'
Similar in Java, C# etc...
}
Dynamic dispatch vs closures {
In Ruby, we can use dynamic dispatch to change external libs => more flexible / can be dangerous! (double edged sword) :)
Example: override badly implemented 'even' function.
ML Code:
(* Dan Grossman, Programming Languages, Jan-Mar 2013 *)
(* Section 7: Dynamic Dispatch Versus Closures *)
(* ML functions do not use use late binding: simple example: *)
fun even x = (print "in even\n" ; if x=0 then true else odd (x-1))
and odd x = (print "in odd\n" ; if x=0 then false else even (x-1))
val a1 = odd 7
val _ = print "\n"
(* does not change behavior of odd -- which is too bad *)
fun even x = (x mod 2) = 0
val a2 = odd 7
val _ = print "\n"
(* does not change behavior of odd -- which is good *)
fun even x = false
val a3 = odd 7
val _ = print "\n"
Ruby code:
# Dan Grossman, Programming Languages, Jan-Mar 2013
# Section 7: Dynamic Dispatch Versus Closures
# Ruby methods use late binding: simple example:
class A
def even x
puts "in even A"
if x==0 then true else odd(x-1) end
end
def odd x
puts "in odd A"
if x==0 then false else even(x-1) end
end
end
a1 = A.new.odd 7
puts "a1 is " + a1.to_s + "\n\n"
class B < A
def even x # changes B's odd too! (helps)
puts "in even B"
x % 2 == 0
end
end
a2 = B.new.odd 7
puts "a2 is " + a2.to_s + "\n\n"
class C < A
def even x
puts "in even C" # changes C's odd too! (hurts)
false
end
end
a3 = C.new.odd 7
puts "a3 is " + a3.to_s + "\n\n"
OOP with dynamic dispatch:
- makes it easy to change behavior, without copying code
- harder to reason about code
}
OOP vs Functional Decomposition {
2 forms of decompositions:
In FP, we break down programs into functions
In OOP, we break down programs into classes that give behavior to some kind of data
Both forms of decomposition are opposite
Which is better? depend on personal taste + on how we expect to change/extend
The expression example:
different variants of expressions: ints, additions, negations...
different operations: eval, toString, hasZero...
Matrix of variants and expressions:
eval toString hasZero ...
--- --- ---
Int |
Add |
Negate |
...
Standard approach in FP (ex: ML):
define a datatype with one constructor for each variant
define one function per expression: each function has one branch for each variant (branches can be combined via wildcard patterns, for example)
=> program organized "by columns"
Standard approach in OOP:
One abstract class with one abstract method for each operation
Define a subclass for each variant
=> program organized "by rows"
Ruby code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 8: OOP vs. Functional Decomposition
# Note: If Exp and Value are empty classes, we do not need them in a
# dynamically typed language, but they help show the structure and they
# can be useful places for code that applies to multiple subclasses.
class Exp
# could put default implementations or helper methods here
end
class Value < Exp
# this is overkill here, but is useful if you have multiple kinds of
# /values/ in your language that can share methods that do not make sense
# for non-value expressions
end
class Int < Value
attr_reader :i
def initialize i
@i = i
end
def eval # no argument because no environment
self
end
def toString
@i.to_s
end
def hasZero
i==0
end
end
class Negate < Exp
attr_reader :e
def initialize e
@e = e
end
def eval
Int.new(-e.eval.i) # error if e.eval has no i method
end
def toString
"-(" + e.toString + ")"
end
def hasZero
e.hasZero
end
end
class Add < Exp
attr_reader :e1, :e2
def initialize(e1,e2)
@e1 = e1
@e2 = e2
end
def eval
Int.new(e1.eval.i + e2.eval.i) # error if e1.eval or e2.eval has no i method
end
def toString
"(" + e1.toString + " + " + e2.toString + ")"
end
def hasZero
e1.hasZero || e2.hasZero
end
end
ML code:
(* Programming Languages, Dan Grossman, Jan-Mar 2013 *)
(* Section 8: OOP vs. Functional Decomposition *)
datatype exp =
Int of int
| Negate of exp
| Add of exp * exp
exception BadResult of string
(* this helper function is overkill here but will provide a more
direct contrast with more complicated examples soon *)
fun add_values (v1,v2) =
case (v1,v2) of
(Int i, Int j) => Int (i+j)
| _ => raise BadResult "non-values passed to add_values"
fun eval e = (* no environment because we don't have variables *)
case e of
Int _ => e
| Negate e1 => (case eval e1 of
Int i => Int (~i)
| _ => raise BadResult "non-int in negation")
| Add(e1,e2) => add_values (eval e1, eval e2)
fun toString e =
case e of
Int i => Int.toString i
| Negate e1 => "-(" ^ (toString e1) ^ ")"
| Add(e1,e2) => "(" ^ (toString e1) ^ " + " ^ (toString e2) ^ ")"
fun hasZero e =
case e of
Int i => i=0
| Negate e1 => hasZero e1
| Add(e1,e2) => (hasZero e1) orelse (hasZero e2)
}
Adding Operations or Variants {
About extensibility
In our "expression example", what if we add a new operation (e.g. "noNegConstants")?
- in FP: easier, just write a new function
- in OOP: less easy, write n methods
And what if we add a new variant (e.g. "Mult")?
- in FP: less easy, update n functions
- in OOP: easier, create a new class
Note: type checker (in ML, Java...) helps us to find unimplemented methods
The other way:
FP allows "plan ahead" (make datatypes extensible / operations take function arguments to give result)
OOP has "visitor pattern"
Sum up:
expecting new operations? use FP
expecting new variants? use OOP
beware: extensibility is a double-edged sword (code is harder to reason about)
Ruby code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 8: Adding Operations or Variants
# Note: If Exp and Value are empty classes, we do not need them in a
# dynamically typed language, but they help show the structure and they
# can be useful places for code that applies to multiple subclasses.
class Exp
# could put default implementations or helper methods here
end
class Value < Exp
# this is overkill here, but is useful if you have multiple kinds of
# /values/ in your language that can share methods that do not make sense
# for non-value expressions
end
class Int < Value
attr_reader :i
def initialize i
@i = i
end
def eval # no argument because no environment
self
end
def toString
@i.to_s
end
def hasZero
i==0
end
def noNegConstants
if i < 0
Negate.new(Int.new(-i))
else
self
end
end
end
class Negate < Exp
attr_reader :e
def initialize e
@e = e
end
def eval
Int.new(-e.eval.i) # error if e.eval has no i method
end
def toString
"-(" + e.toString + ")"
end
def hasZero
e.hasZero
end
def noNegConstants
Negate.new(e.noNegConstants)
end
end
class Add < Exp
attr_reader :e1, :e2
def initialize(e1,e2)
@e1 = e1
@e2 = e2
end
def eval
Int.new(e1.eval.i + e2.eval.i) # error if e1.eval or e2.eval has no i method
end
def toString
"(" + e1.toString + " + " + e2.toString + ")"
end
def hasZero
e1.hasZero || e2.hasZero
end
def noNegConstants
Add.new(e1.noNegConstants,e2.noNegConstants)
end
end
class Mult < Exp
attr_reader :e1, :e2
def initialize(e1,e2)
@e1 = e1
@e2 = e2
end
def eval
Int.new(e1.eval.i * e2.eval.i) # error if e1.eval or e2.eval has no i method
end
def toString
"(" + e1.toString + " * " + e2.toString + ")"
end
def hasZero
e1.hasZero || e2.hasZero
end
def noNegConstants
Mult.new(e1.noNegConstants,e2.noNegConstants)
end
end
ML code:
(* Programming Languages, Dan Grossman, Jan-Mar 2013 *)
(* Section 8: Adding Operations or Variants *)
(* we take our previous version and add noNegConstants without changing
old code and a Mult constructor, which requires changing all previous code,
with a nice to-do list due to inexhaustive pattern-matches. *)
datatype exp =
Int of int
| Negate of exp
| Add of exp * exp
| Mult of exp * exp
exception BadResult of string
(* this helper function is overkill here but will provide a more
direct contrast with more complicated examples soon *)
fun add_values (v1,v2) =
case (v1,v2) of
(Int i, Int j) => Int (i+j)
| _ => raise BadResult "non-values passed to add_values"
fun eval e = (* no environment because we don't have variables *)
case e of
Int _ => e
| Negate e1 => (case eval e1 of
Int i => Int (~i)
| _ => raise BadResult "non-int in negation")
| Add(e1,e2) => add_values (eval e1, eval e2)
| Mult(e1,e2) => (case (eval e1, eval e2) of
(Int i, Int j) => Int (i*j)
| _ => raise BadResult "non-ints in multiply")
fun toString e =
case e of
Int i => Int.toString i
| Negate e1 => "-(" ^ (toString e1) ^ ")"
| Add(e1,e2) => "(" ^ (toString e1) ^ " + " ^ (toString e2) ^ ")"
| Mult(e1,e2) => "(" ^ (toString e1) ^ " * " ^ (toString e2) ^ ")"
fun hasZero e =
case e of
Int i => i=0
| Negate e1 => hasZero e1
| Add(e1,e2) => (hasZero e1) orelse (hasZero e2)
| Mult(e1,e2) => (hasZero e1) orelse (hasZero e2)
fun noNegConstants e =
case e of
Int i => if i < 0 then Negate (Int(~i)) else e
| Negate e1 => Negate(noNegConstants e1)
| Add(e1,e2) => Add(noNegConstants e1, noNegConstants e2)
| Mult(e1,e2) => Mult(noNegConstants e1, noNegConstants e2)
}
Binary Methods with Functional Decomposition {
What if operations have multiple arguments of different variants?
ex:
- include variants "String" and "Rational"
- update "Add" to work on any pair of Int, String , Rational: concatenation if either math or string concat
=> another 3x3 grid:
Int String Rational
Int
String
Rational
In ML: 'add_values' will be a helper function with 9 cases
ML code:
(* Programming Languages, Dan Grossman, Jan-Mar 2013 *)
(* Section 8: Binary Methods With Functional Decomposition *)
datatype exp =
Int of int
| Negate of exp
| Add of exp * exp
| Mult of exp * exp
| String of string
| Rational of int * int
exception BadResult of string
fun add_values (v1,v2) =
case (v1,v2) of
(Int i, Int j) => Int (i+j)
| (Int i, String s) => String(Int.toString i ^ s)
| (Int i, Rational(j,k)) => Rational(i*k+j,k)
| (String s, Int i) => String(s ^ Int.toString i) (* not commutative *)
| (String s1, String s2) => String(s1 ^ s2)
| (String s, Rational(i,j)) => String(s ^ Int.toString i ^ "/" ^ Int.toString j)
| (Rational _, Int _) => add_values(v2,v1) (* commutative: avoid duplication *)
| (Rational(i,j), String s) => String(Int.toString i ^ "/" ^ Int.toString j ^ s)
| (Rational(a,b), Rational(c,d)) => Rational(a*d+b*c,b*d)
| _ => raise BadResult "non-values passed to add_values"
fun eval e =
case e of
Int _ => e
| Negate e1 => (case eval e1 of
Int i => Int (~i)
| _ => raise BadResult "non-int in negation")
| Add(e1,e2) => add_values (eval e1, eval e2)
| Mult(e1,e2) => (case (eval e1, eval e2) of
(Int i, Int j) => Int (i*j)
| _ => raise BadResult "non-ints in multiply")
| String _ => e
| Rational _ => e
fun toString e =
case e of
Int i => Int.toString i
| Negate e1 => "-(" ^ (toString e1) ^ ")"
| Add(e1,e2) => "(" ^ (toString e1) ^ " + " ^ (toString e2) ^ ")"
| Mult(e1,e2) => "(" ^ (toString e1) ^ " * " ^ (toString e2) ^ ")"
| String s => s
| Rational(i,j) => Int.toString i ^ "/" ^ Int.toString j
fun hasZero e =
case e of
Int i => i=0
| Negate e1 => hasZero e1
| Add(e1,e2) => (hasZero e1) orelse (hasZero e2)
| Mult(e1,e2) => (hasZero e1) orelse (hasZero e2)
| String _ => false
| Rational(i,j) => i=0
fun noNegConstants e =
case e of
Int i => if i < 0 then Negate (Int(~i)) else e
| Negate e1 => Negate(noNegConstants e1)
| Add(e1,e2) => Add(noNegConstants e1, noNegConstants e2)
| Mult(e1,e2) => Mult(noNegConstants e1, noNegConstants e2)
| String _ => e
| Rational(i,j) => if i < 0 andalso j < 0
then Rational(~i,~j)
else if j < 0
then Negate(Rational(i,~j))
else if i < 0
then Negate(Rational(~i,j))
else e
}
Binary Methods with Double Dispatch {
Double dispatch:
- each class implements 'add_values' and 3 functions: addInt(v), addString(v) and addRational(v)
- Add.eval(e1, e2) calls e1.eval.add_values e2.eval
Warning: testing type (e.g. 'v.is_a? Rational') is not really OOP
Ruby code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 8: Binary Methods with OOP: Double Dispatch
# Note: If Exp and Value are empty classes, we do not need them in a
# dynamically typed language, but they help show the structure and they
# can be useful places for code that applies to multiple subclasses.
class Exp
# could put default implementations or helper methods here
end
class Value < Exp
# this is overkill here, but is useful if you have multiple kinds of
# /values/ in your language that can share methods that do not make sense
# for non-value expressions
end
class Int < Value
attr_reader :i
def initialize i
@i = i
end
def eval # no argument because no environment
self
end
def toString
@i.to_s
end
def hasZero
i==0
end
def noNegConstants
if i < 0
Negate.new(Int.new(-i))
else
self
end
end
# double-dispatch for adding values
def add_values v # first dispatch
v.addInt self
end
def addInt v # second dispatch: other is Int
Int.new(v.i + i)
end
def addString v # second dispatch: other is MyString (notice order flipped)
MyString.new(v.s + i.to_s)
end
def addRational v # second dispatch: other is MyRational
MyRational.new(v.i+v.j*i,v.j)
end
end
# new value classes -- avoiding name-conflict with built-in String, Rational
class MyString < Value
attr_reader :s
def initialize s
@s = s
end
def eval
self
end
def toString
s
end
def hasZero
false
end
def noNegConstants
self
end
# double-dispatch for adding values
def add_values v # first dispatch
v.addString self
end
def addInt v # second dispatch: other is Int (notice order is flipped)
MyString.new(v.i.to_s + s)
end
def addString v # second dispatch: other is MyString (notice order flipped)
MyString.new(v.s + s)
end
def addRational v # second dispatch: other is MyRational (notice order flipped)
MyString.new(v.i.to_s + "/" + v.j.to_s + s)
end
end
class MyRational < Value
attr_reader :i, :j
def initialize(i,j)
@i = i
@j = j
end
def eval
self
end
def toString
i.to_s + "/" + j.to_s
end
def hasZero
i==0
end
def noNegConstants
if i < 0 && j < 0
MyRational.new(-i,-j)
elsif j < 0
Negate.new(MyRational.new(i,-j))
elsif i < 0
Negate.new(MyRational.new(-i,j))
else
self
end
end
# double-dispatch for adding values
def add_values v # first dispatch
v.addRational self
end
def addInt v # second dispatch
v.addRational self # reuse computation of commutative operation
end
def addString v # second dispatch: other is MyString (notice order flipped)
MyString.new(v.s + i.to_s + "/" + j.to_s)
end
def addRational v # second dispatch: other is MyRational (notice order flipped)
a,b,c,d = i,j,v.i,v.j
MyRational.new(a*d+b*c,b*d)
end
end
class Negate < Exp
attr_reader :e
def initialize e
@e = e
end
def eval
Int.new(-e.eval.i) # error if e.eval has no i method
end
def toString
"-(" + e.toString + ")"
end
def hasZero
e.hasZero
end
def noNegConstants
Negate.new(e.noNegConstants)
end
end
class Add < Exp
attr_reader :e1, :e2
def initialize(e1,e2)
@e1 = e1
@e2 = e2
end
def eval
e1.eval.add_values e2.eval
end
def toString
"(" + e1.toString + " + " + e2.toString + ")"
end
def hasZero
e1.hasZero || e2.hasZero
end
def noNegConstants
Add.new(e1.noNegConstants,e2.noNegConstants)
end
end
class Mult < Exp
attr_reader :e1, :e2
def initialize(e1,e2)
@e1 = e1
@e2 = e2
end
def eval
Int.new(e1.eval.i * e2.eval.i) # error if e1.eval or e2.eval has no i method
end
def toString
"(" + e1.toString + " * " + e2.toString + ")"
end
def hasZero
e1.hasZero || e2.hasZero
end
def noNegConstants
Mult.new(e1.noNegConstants,e2.noNegConstants)
end
end
}
Multiple inheritance {
= if a class can have > 1 superclass
C++ has 'multiple inheritance' (but some problems).
Ruby has 'mixins' (a bit less powerful).
Java/Csharp have 'interfaces' (less powerful)
Useful? yes, prevents pasting code (ex: Point / ColorPoint / Point3D / ColorPoint3D, etc...) but it s hard!
Vocabulary:
'immediate' subclass/superclass
'transitive' subclass/superclass
'class hierarchy':
- a tree for single inheritance
- a 'Directed-Acyclic Graph' for multiple inheritance
What s wrong?
- what does 'super' means if class hierarchy is a graph? => more complicated
- what if C inherits from A and B, and both have method m?
- what if C inherits from A, which overrides m and B, which does not overrides m?
- what if C inherits from A and B that have field F? Has C one or two copies of F?
Ruby:
Person(pocket), Artist < Person, Cowboy < Person: cowboys and artists have a distinct pocket
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 8: Multiple Inheritance
class Pt
attr_accessor :x, :y
def distToOrigin
Math.sqrt(x * x + y * y)
end
end
class ColorPt < Pt
attr_accessor :color
def darken # error if @color not already set
self.color = "dark " + self.color
end
end
class Pt3D < Pt
attr_accessor :z
def distToOrigin
Math.sqrt(x * x + y * y + z * z)
end
end
# This does not exist in Ruby (or Java/C#, it does in C++)
# class ColorPt3D_3 < ColorPt, Pt3D
# end
# two ways we could actually make 3D Color Points:
class ColorPt3D_1 < ColorPt
attr_accessor :z
def distToOrigin
Math.sqrt(x * x + y * y + z * z)
end
end
class ColorPt3D_2 < Pt3D
attr_accessor :color
def darken # error if @color not already set
self.color = "dark " + self.color
end
end
}
Mixins {
= Ruby s alternative to multiple inheritance (like Scala traits)
mixin = a collections of methods, not a class
In Ruby, a class cannot extend more that one class but can include several mixins
Syntax:
module MyMixin
def myMethod1
self + self # here we assume that any class that includes this mixin has 'x' method
end
end
class MyInt include MyMixin
Lookup rules:
1. in class 2. in mixins 3. in superclass 4. in superclass mixins
It s often bad style to use fields in mixins (avoid the 'pocket' problems)
Widely used mixins:
'Comparable' which assumes '<=>' exists (aka 'spaceship operator')
'Enumerable' which assumes 'each' exists
Mixins limitation (vs multiple inheritance):
works well for our ColorPoint/Point3D example but not for the cowboy/artist
Code:
# Programming Languages, Dan Grossman, Jan-Mar 2013
# Section 8: Mixins
module Doubler
def double
self + self # uses self's + message, not defined in Doubler
end
end
class Pt
attr_accessor :x, :y
include Doubler
def + other
ans = Pt.new
ans.x = self.x + other.x
ans.y = self.y + other.y
ans
end
end
class String
include Doubler
end
# these are probably the two most common uses in the Ruby library:
# Comparable and Enumerable
# you define <=> and you get ==, >, <, >=, <= from the mixin
# (overrides Object's ==, adds the others)
class Name
attr_accessor :first, :middle, :last
include Comparable
def initialize(first,last,middle="")
@first = first
@last = last
@middle = middle
end
def <=> other
l = @last <=> other.last # <=> defined on strings
return l if l != 0
f = @first <=> other.first
return f if f != 0
@middle <=> other.middle
end
end
# Note ranges are built in and very common
# you define each and you get map, any?, etc.
# (note map returns an array though)
class MyRange
include Enumerable
def initialize(low,high)
@low = low
@high = high
end
def each
i=@low
while i <= @high
yield i
i=i+1
end
end
end
# here is how module Enumerable could implement map:
# (but notice Enumerable's map returns an array,
# *not* another instance of the class :( )
# def map
# arr = []
# each {|x| arr.push x }
# arr
# end
# this is more questionable style because the mixin is using an
# instance variable that could clash with classes and has to be initialized
module Color
def color
@color
end
def color= c
@color = c
end
def darken
self.color = "dark " + self.color
end
end
class Pt3D < Pt
attr_accessor :z
# rest of definition omitted (not so relevant)
end
class ColorPt < Pt
include Color
end
class ColorPt3D < Pt3D
include Color
end
}
Interfaces {
How to deal with 'multiple inheritance' in statically-typed langs which do not have 'multiple inheritance'?
Statically-typed OOP has interfaces (cf. Java, Csharp, etc... but not Ruby)
If C is a transitive subclass of D, then C is a subtype of D
Interfaces:
- are a type, but not a class (interfaces cannot be instanciated).
- only contain method signatures (no definitions like in Ruby)
- classes can implement several interfaces
}
Subtyping from the beginning {
Let s study Subtyping in pseudo-code
Imagine a tiny statically typed language that has records with mutable fields (ML has records but no subtyping nor mutable fields ; Racket and Ruby have no static typing):
- record creation: { f1 = e1, f2 = e2, ..., fn = en}
- record field access: e.f
- record field update: e1.f = e2
- basic type system for records:
if e1, e2, ..., en have type t1, t2, ..., tn
record has type { f1: t1, f2: t2, ..., fn = tn }
e.f has type t
e.f = e2 is only valid if e2 has same type than e.f
Example:
fun distToOrigin (p: {x: real, y: real}) =
Math.sqrt(p.x*p.x + p.y*p.y)
val pythag : {x: real, y: real} = {x=3.0, y=4.0}
val five : real = distToOrigin(pythag) # OK
val c = {x: real, y: real, color: string} = {x=3.0, y=4.0, color="green"} # Oops, would not type check:
distToOrigin(c)
So we also want to allow extra fields:
distToOrigin({x=3.0, y=4.0, color="green"}) # It's fine
}
The subtype relation {
Add more flexibility to our type system, with 2 things:
1. add notion of subtyping
t1 <: t2 # means t1 is a subtype of t2
2. add one rule:
if e has type t1 and t1 <: t2, then e also has t2
principle of substitutability:
if t1 <: t2, then any value of type t1 must be usable in every way a t2 is
4 rules:
rule 1: "width" subtyping: a supertype can has a subset of fields with the same type
rule 2: "permutation" subtyping: a supertype can have the same fields in different order
rule 3: transitivity: if t1 <: t2 and t2 <: t3 then t1 <: t3
rule 4: reflexibility: every type is a subtype of itself
}
Depth subtyping {
We cannot add "depth" subtyping (nested) like
{ center: {x: real, y: real, z :real}, radius: real }
<:
{ center: {x: real, y: real}, radius: real }
Rule we would like to add:
if ta <: tb, then { f1: t1, ..., f: ta, ..., fn: tn } <: { f1: t1, ..., f: tb, ..., fn: tn }
But it is unsound because of mutation (sphere could be transformed into circle)
(note: it would be OK if our language was immutable)
}
Function Subtyping {
subtyping for functions!
When a function type is a subtype of another...
Example:
(* distance between a point and f(point) *)
fun distMoved(
f : {x: real, y: real} -> { x: real, y: real },
point: { x: real, y: real}) =
let val p2 : { x: real, y: real } = f p
val dx : real = p2.x - p.x
val dy : real = p2.y - p.y
in
Math.sqrt(dx*dx + dy*dy)
end
fun flip p = { x = ~p.x, y=~p.y }
val d = distMoved(flip, {x=3.0, y=4.0}) (* it s OK, no subtyping here *)
If ta <: tb, then t->ta <: t->tb
"return types are covariant" i.e. a function can return "more than it needs" to
Example:
fun flipGreen p = { x = ~p.x, y=~p.y, color="green" }
val d = distMoved(flipGreen, {x=3.0, y=4.0})
(* OK *)
ta <: tb does NOT allow ta->t <: tb->t
Example:
fun flipIfGreen p =
if p.color = "green" (* kaboom! *)
then { x = ~p.x, y=~p.y }
else { x = p.x, y=p.y }
val d = distMoved(flipIfGreen, {x=3.0, y=4.0})
(* not OK, unsound! *)
If tb <: ta, then ta->t <: tb->t
"Argument types are contravariant" i.e. a function can assume "less that it needs to" about arguments
Example:
fun flipX_Y0 = { x = ~p.x, y=0.0 }
val d = distMoved(flipX_Y0, {x=3.0, y=4.0})
You can do both:
Example:
fun flipXMakeGreen p = { x=~p.x, y=0.0, color="green"} (* {x:real} -> {x:real, y:real, c:string} *)
val d = distMoved(flipXMakeGreen, {x=.3.0, y=4.0})
General rule:
if t3 <: t1 and t2 <: t4, then t1->t2 <: t3->t4
"function subtyping" is contravariant in arguments and covariant in results
}
Subtyping for OOP {
Like in Java, C#, etc...
Recall:
- classes are types
- subclasses are subtypes
- substitution principle: instance of subclass should be usable on place of instance of superclass
Objects are like records
- with mutable fieds (generally)
- with methods which are immutable functions that have access to 'self'
In theory:
- Subtypes could have extra fields and methods
- Overriding methods could have contravariant arguments and covariant results (compared to overridden methods)
In practice, in Java:
- subtyping are explicit subclasses
- a subclass can add fields and methods
- a subclass can override a method with a covariant return type
- a subclass CANNOT override a method with contravariant arguments
Classes vs Types
class: defines object s behaviour
type: defines object s fields/methods
Both are often confused! :)
Self/this is a special argument!
It is covariant, not like other arguments
}
Generics Versus Subtyping {
Generics are good at:
- types for functions that compose (i.e. 'compose(g,h)')
- types for functions that operate over generic collections (i.e. 'length', 'map' etc...)
(when types can be anything but multiple things need to be in the same type)
Subtyping is NOT good:
- for containers (we would need to downcast => can fail, run-time cost...)
Subtyping is good:
- code that "needs a Foo but have more than a Foo" (i.e. geometry points for colored points, GUI widgets...)
ML has no subtyping (alternative: pass getter functions)
}
Bounded Polymorphism {
When to combine generics and subtyping
Some langs have both generics and subtyping (i.e. Java)
"bounded polymorphism": combining generics and subtyping
Example:
// Filter points that are inside a circle (cannot send / get back List<ColorPoint>)
List<Point> inCircle(List<Point> pts, Point circleCenter, double circleRadius)
// With generics, we could now send / get back List<ColorPoint> but we cannot implement inCircle() for any type:
List<T> inCircle(List<T> pts, Point circleCenter, double circleRadius)
// With generics, we could now send / get back List<ColorPoint> but we cannot implement inCircle() for any type:
List<T> inCircle(List<T> pts, Point circleCenter, double circleRadius)
// Correct syntax for bounded polymorphism in Java:
List<T extends Point> inCircle(List<T> pts, Point circleCenter, double circleRadius)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment