Professional Documents
Culture Documents
o n
S Better Ruby Through
^
Functional
Programming
@deanwampler
dean@deanwampler.com
polyglotprogramming.com/papers
All images are 2008-2009, Dean Wampler, except where otherwise noted.
This is the second version of the RubyConf2008 talk I did. Here, Ill summarize FP more
quickly and dive into some specific application areas for FP.
Im not just about Ruby...
Co-author,
Programming
Scala
programmingscala.com
Available now.
Why
Functional
Programming?
3
Why, after 50 years in academic and some industry labs is FP going mainstream??
4 2006 Clayton Hallmark
Weve hit the end of scaling through Moores Law.
5 2006 Intel
so, were scaling horizontally.
Single-
Threaded
Code
Wont
Do...
6
Functional
Programming
8
No mutable values: There is changing state in a functional program; its in the stack or new
values returned by functions. Individual bits of memory are immutable.
FP is breaking out of the academic world, because it offers a better way to approach
concurrency, which is becoming ubiquitous.
When you
share mutable
values...
Hic sunt dracones
(Here be dragons)
13
Your alternative, multi-threaded programming, which is very hard and few people can do it
well.
A client wont
mess up an
immutable object.
Another benefit...
14
You can feel confident passing an immutable object to a client without fear that the client
will modify the object in some undesirable way.
Immutable
Types in Ruby?
15
One way to be pure FP is to make your types immutable. (You can also freeze individual
objects, but you must also freeze the attributes, recursively!)
Read-only attributes
class Person
attr_reader :first_name,
Array :last_name,
:addresses,
def initialize fn, ln, ads
@first_name = fn.clone.freeze
@last_name = ln.clone.freeze
@addresses = ads.clone.freeze
end
end
Clone and freeze subobjects
16
First, we can support immutable types by using attr_reader only.
Dont forget to clone and freeze collections and other subobjects. The string attributes have
to be frozen, too. (We should actually clone and freeze each address itself, recursively.)
dean = Person.new(
"Dean", "Wampler", [addr1, ]
p dean.first_name # => "Dean"
p dean.addresses # => ["addr1"]
dean.first_name = "Bubba"
# => undefined method
dean.addresses[0] = "new addr"
# => can't modify frozen
17
Second, dont allow self-modifying methods! (Here we pass in a hash with fields that might
change. We update the field if a corresponding hash value is present.) Note that this method
would be allowed despite the clone and freeze calls we make in the initializer.
Immutable
class Person is better
def update vals
Person.new(
(vals[:first_name]||@first_name),
(vals[:last_name] || @last_name),
(vals[:addresses] || @addresses))
end
end
Returns a new Person
19
Here we merge the has vals with the existing Persons values to create a new Person.
p1 = Person.new Dean,
Wampler, [a1, a2]
Example
20
Here we merge the has vals with the existing Persons values to create a new Person.
Immutable
Objects in Ruby?
Use #freeze
21
You may want immutable instances, but not types. Use #freeze, but to do it properly youll
need to override #freeze and also call freeze on the attributes, recursively!
Drawback:
Overhead of
Object Creation.
Hash#merge
vs.
Hash#merge!
22
Youll create lots of copies of objects if they are immutable. Big objects are expensive to
copy. General Note: As always, any design choice has pluses and minuses.
Functions are
First Class Values
Characteristics of FP
23
Compose functions of
other functions
first-class values
24
Function are first-class values; you can assign them to variables and pass them to other
(higher-order) functions, giving you composable behavior.
First-Class
Functions
in Ruby?
25
Is this a function?
def even?(n); n % 2 == 0; end
You cant just pass the method name, even the proc-ized form, because of Rubys parse
rules. Is it supposed to first invoke even? with no arguments and no parentheses and pass
the result to find_all. Or, is it supposed to pass even? as a value? Ruby wants to call even?
first.
Blocks, not methods, are
first-class functions
in Ruby.
[1,2,3,4].select {|n| even? n}
# => [2, 4]
27
[].select(&method(:even?)) #1.8
[].select(&:even?) #1.9
# => [2, 4]
3rd characteristic.
y = sin(x)
Functions dont
change state
side-effect free
30
A math function doesnt change any object or global state. All the work it does is returned
by the function.
Side-effect free:
easier to
understand the
behavior. 31
A side-effect free function is much easier to understand. You dont have to understand global
or object state to understand its behavior. Functions that modify state are much trickier.
Side-effect free:
easy to call it
concurrently.
32
Therefore, you can call it anywhere and the behavior is the same for the same inputs. You can
also replace the call with the value it would return (for a give set of inputs), i.e., in
memoization (i.e., caching).
Side-Effect
Free Functions
in Ruby?
34
class Person
attr_reader :first_name, ,
attr_accessor :addresses
Theres no mechanism to verify that a given method or block is side-effect free. You have to
do it manually, if you can. Note that construction of objects isnt side-effect free, but its an
exception we can live with, if were careful! In a concurrent environment, select is side-
effect free, but what if the array is modified by another thread? What if the implicit block
has side effects?
Recursion
Characteristics of FP
36
Fibonacci Sequence
{ 0, if n = 0
F(n) = 1, if n = 1
F(n-1) + F(n-2), if n>1
37
An elegant definition. Note that we are using a case statement. Pattern matching is an
integral part of most functional languages.
Warning:
Recursion can
blow the stack.
Ruby 1.9.1 has tail-call optimization
39
Active Record
43
Ruby DSLs also have this quality. You say what you want and let ActiveRecord decide how to
do it.
We all know that
DSLs are good...
Functional programming
has many of
the same qualities.
44
Fear not the
mathematics...
45
47
In many mature code bases, I see ill-defined, hard-to-maintain object models. Can functional
thinking help?
Consider an
algebraic type:
Point
For 2-dimensional points
48
def + p2
Point.new(@x + p2.x, @y + p2.y)
end
def - p2
Point.new(@x - p2.x, @y - p2.y)
end
end
49
A point class with immutable instances (assuming floats for x & y). To add them, you add the
x values and the y values (similarly for subtraction). Also, note that they are immutable. Math
operations create new instances.
describe "Point" do
before :all do
@samples =
(-3..3).inject([]) do |l1,i|
l1 +=
(-3..3).inject([]) do |l2,i|
l2 << Point.new(1.1*i,1.1*j)
end
end
end
50
Before all, create a list of samples, combinations of x and y values between (1.1 * -3) and (1.1
* +3).
describe "Point" do
it "should support + ..." do
@samples.each do |p1|
@samples.each do |p2|
p12 = p1 + p2
p12.x.should == p1.x + p2.x
p12.y.should == p1.y + p2.y
end
end
end // - is similar
51
Then we test all the permutations to ensure that the algebraic properties hold for all
instances (at least all that we try).
For all instances,
certain properties
are true.
52
FP makes us think about how well defined our objects are. Do we always know the states they
can be in? Are the state transitions (behaviors) well defined?
Example #2
Money
is also
algebraic.
54
For simple things, its tempting to use primitive types like Strings. Theres only one
attribute (the value), right? But, is that the best idea?
class Money
Should do error
def initialize v
checking on v...
@value = v
end
Ignores currency conversions, checking that the passed-in initial value is a float, accuracy -
what should the type of @value be, etc.
Note that the value reader method is protected. We dont want the implementation to leak.
(tests can use money.send(:value) )
# TODO: proper rounding...
def + other
Money.new(value + other.value)
end
def - amount
Money.new(value - other.value)
end
def * factor
Money.new(value * factor)
end
57
58
Completes the minimum requirements for algebraic behavior. (We omitted this method from
Point earlier, but it should be there.)
The specs are
similar to those
for Point.
59
Other Examples
A Zip Code?
A persons Age?
An email or street Address?
...
60
For simple things, its tempting to use primitive types like Strings. Is that the best idea?
Compose
domain models
from precise
building blocks. 61
Start with the building blocks, get them precise, then build up.
Bloated, Rigid
Object Models
62
In many mature code bases, I see bloated, hard-to-maintain object models? Why and can
functional thinking help?
Your Person
My Person
63
How many apps/components in your systems have a different notion of Person (or any
other common type)?
Use one, big
Person type with
all possible fields?
64
Does defining a type (or more than one) for Person deliver enough value to justify its
existence? This may seem to contradict what I was just saying about well-defined types and
behaviors, but if a type like person is really just a struct of fields, then maybe it isnt
carrying its weight.
But first...
67
To consider answers to these questions, lets first look at one more aspect of functional
programming...
Functional
Data Types
68
my_hash = {
:addr1 => "1 Memory Lane",
:addr2 => "1 Hope Drive",
:addr3 => "1 Infinite Loop"}
69
Lists and (optionally) Maps are the most common functional data types. Some FP languages
have others, like Sets.
Classic Operations on
Functional Data Types
list map
filter
fold/
reduce
70
List is the most commonly-used data type. Note that maps and sets can be represented as
lists, too (ignoring performance issues from this implementation choice...).
In Ruby?
select/
grep
inject
71
Ruby equivalents.
Back to:
Do we need
a Person type,
at all?
72
Maybe were
better off
without a type!
73
If we have a lot of volatility in the definition of Person, maybe trying to define a single or
multiple Person classes is not really that beneficial. This is especially true if were exchanging
person data between components (as well see).
dean = {
:first_name => "Dean",
:last_name => "Wampler",
:addresses => [
"1 Memory Lane",
"1 Hope Drive",
"1 Infinite Loop"]}
dean[:addresses].each do |addr|
puts "Address: #{addr}"
end
Use a Hash
74
Use a hash instead. Note the synergy with JSON. Could also use a Struct in a similar way.
Other components/systems might use a different hash of values.
Person = Struct.new(
:first_name, :last_name,
:addresses)
dean = Person.new("Dean",
"Wampler", [
"1 Memory Lane",
"1 Hope Drive",
"1 Infinite Loop"])
puts dean
dean.addresses.each do |addr|
puts "Address: #{addr}"
end
Use a Struct
75
Use a hash instead. Note the synergy with JSON. Could also use a Struct in a similar way.
Other components/systems might use a different hash of values.
Software Components?
76
80 2002 electronics-labs.com
With 2^N on/off signals, all the data and complex behavior of digital electronics has been
possible.
HTTP
Get Connect
Post Delete
Put Head
Trace Options
WiFi/Ethernet, IP,TCP, HTTP, REST
81
HTTP - Very simple protocol. 8 messages, two of which are used for almost everything. Yet,
that was enough to create the explosive growth of the Internet. (Of course, there are other
protocols over Internet, like SMTP, IMAP, and POP, but HTTP drove adoption.)
Common Features
82
The richness and sophistication of objects has paradoxically undermined their utility as a
component model!
A
Functional
Component
Model 84
I suggest (and actually the FP community has said this for a long time) that a functional-
style model has more of the qualities I just described.
Data Is Represented by
Lists, Maps (and Structs?)
dean = {
:first_name => "Dean",
:last_name => "Wampler",
:addresses => [
"1 Memory Lane",
"1 Hope Drive",
"1 Infinite Loop"]}
Like JSON??
85
A very simple example from the core library. The File component provides customization
hooks through Enumerable and other methods. Note that it provides very generic, reusable
behavior, but controlled ways to exploit those primitives.
Other Examples
in Our Industry
JSON, REST
Caching, Messaging
systems.
Erlang-based NoSQL
DBs (e.g., CouchDB).
87
Finally, lets see an example of writing concurrent applications using actors. Actors dont
share any state. They communicate only through messages. However, the actor might have
its own self-contained state.
class Shape; end
draw
draw
??? error!
exit exit
91
Omnibus Concurrency
require "rubygems"
require "concurrent/actors"
require "case"
require "shapes"
include Concurrent::Actors
This example uses MenTaLguYs Omnibus Concurrency gem. See also Revactor (Ruby 1.9)
and Dramatis. The Greeting and RequestGreeting objects will be used as the messages sent
between actors.
Actor sending
commands
Message =
Struct.new :reply_to,:body
This actor
def make_msg body
current = Actor.current
Message.new current, body
end
93
This example uses MenTaLguYs Omnibus Concurrency gem. See also Revactor (Ruby 1.9)
and Dramatis. The Greeting and RequestGreeting objects will be used as the messages sent
between actors.
Send messages
94
Here is the main Actor code. An actor is spawned (e.g., in a separate Thread). It loops
forever waiting to receive messages. It matches each message by type, using MenTaLguYs
Case gem, which enhances #===. Note the combination of pattern matching and
polymorphism (Shape.draw).
display = Actor.spawn do
loop do
Actor.receive do |msg|
msg.when Message[Object,Shape] do |m|
s = m.body.draw
m.reply_to << "drew: #{s}"
end
msg.when Message[Object,:exit] do|m|
m.reply_to << "exiting"
end
msg.when Message[Object,Object] do|m|
m.reply_to << "Error! #{m.body}"
end; end; end; end
97
Here is the main Actor code. An actor is spawned (e.g., in a separate Thread). It loops
forever waiting to receive messages. It matches each message by type, using MenTaLguYs
Case gem, which enhances #===. Note the combination of pattern matching and
polymorphism (Shape.draw).
Receive replies
loop do
Actor.receive do |reply|
reply.when "exiting" do |s|
p "exiting..."
exit
end
reply.when String do |s|
p "reply: #{s}"
end
end
Back to the driver script
98
99
100
The actor example shows functional-style pattern matching, which is used heavily in FP in an
analogous way that polymorphism is used in OOP.
display = Actor.spawn do
loop do
Actor.receive do |msg|
FP pattern matching...
msg.when Message[Object,Shape] do |m|
s = m.body.draw
end with OO
polymorphism
A powerful combination!
Note the combination of pattern matching and polymorphism (Shape.draw) is very powerful.
Finally...
102
Finally...
Learn a
Functional
Language.
103
Thanks!
dean@deanwampler.com
@deanwampler
polyglotprogramming.com/papers
programmingscala.com
104