Comp sition
James Dabbs
jamesdabbs
Composition
- Object composition (not to be confused with function composition) is a way to combine simple objects or data types into more complex ones
- Function composition (not to be confused with object composition) is an act or mechanism to combine simple functions to build more complicated ones
— Wikipedia
Composition
Combining pieces cohesively
Composition
Goals for this talk
- illuminate object and functional composition
- confuse object and functional composition
- develop a phrasebook for those interested in spending more time in functional country
- Legos
- Topology
- Category Theory
- Haskell
- Today
Haskell infected
Functional core
"Haskell is easy! You just need to grok category theory and then you can starting printing stuff."
— A. Strawman
Category Theory
Lessons Learned
A category is
- a collection of objects
- functions between those objects
- the means to compose functions
Category Theory is the study of composition
Category Theory
Lessons Learned
Surjection
Monoid
Isomorphism
Category Theory
Lessons Learned
Isomorphic
- Literally: same shape
- Two things that may appear different, but function identically
The Curry-Howard isomorphism says that proving theorems and writing programs are the same activity
a, b, ab, ba, aaa, aba, bab, bbb, ...
Category Theory
Lessons Learned
See e.g. the Nullstellensatz in Algebraic Geometry, the Monstrous Moonshine theorem, Homotopy Type Theory, ...
When you arrive at the same idea from different directions, pay attention
Category Theory
Lessons Learned
Commutative Diagrams
Category Theory
Lessons Learned
Abstraction
- Abstraction is the process of unifying concepts by ignoring their differences
- You often know you're at the right level of abstraction when there's only one thing that could possibly work (and that thing works)
- Function composition is a surprisingly robust abstraction
Object Composition
- In OO, composition refers to an object holding a reference to one or more other objects that it collaborates with to fulfill its responsibilities
- has-a relationship (as opposed to inheritance's is-a)
Favor object composition over class inheritance
— Gang of Four
Object Composition
class Topologist
attr_reader :jokes
def initialize jokes:
@jokes = jokes
end
def tell_joke
@jokes.sample
end
end
emmy = Topologist.new jokes: Topologist::JOKES
puts emmy.tell_joke
"Klein bottle for sale, enquire within"
a Klein bottle
Object Composition
class AlgebraicTopologist < Topologist
def tell_joke
@jokes.select { |joke| gets? joke }.sample
end
private
def gets? joke
joke.length < 200 || joke.include?("group")
end
end
hopf = AlgebraicTopologist.new jokes: Topologist::JOKES
puts hopf.tell_joke
"Q: What's purple and commutes?
A: An abelian grape."
Object Composition
class LoudTopologist < Topologist
def tell_joke
super.upcase + "!!!"
end
end
lej = LoudTopologist.new jokes: Topologist::JOKES
puts lej.tell_joke
"A COMATHEMATICIAN IS A DEVICE FOR TURNING COTHEOREMS INTO FFEE!!!"
Object Composition
class LoudAlgebraicTopologist < AlgebraicTopologist
def tell_joke
super.upcase + "!!!"
end
end
or
class LoudAlgebraicTopologist < LoudTopologist
def jokes
@jokes.select { |joke| gets? joke }.sample
end
private
def gets? joke; ...; end
end
class LoudAlgebraicTopologist < ??
end
Object Composition
class Algebraist
def initialize topologist
@topologist = topologist
end
def tell_joke
@topologist.jokes.shuffle.find { |joke| gets? joke }
end
private
def gets?(joke); ...; end
end
emmy = Topologist.new jokes: Topologist::JOKES
hopf = Algebraist.new emmy
puts hopf.tell_joke
"I have more jokes but they are too small for this margin"
Object Composition
class Loudspeaker
def initialize jokester
@jokester = jokester
end
def tell_joke
@jokester.tell_joke.upcase + "!!!"
end
end
topologist = Topologist.new jokes: Topologist::JOKES
algebraic_topologist = Algebraist.new topologist
loud_topologist = Loudspeaker.new topologist
loud_algebraic_topologist = Loudspeaker.new algebraic_topologist
- Collaborating objects allow us to parameterize behavior
- We do not need a distinct class for each combination of behaviors
topologist = Topologist.new jokes: Topologist::JOKES
algebraic_topologist = Algebraic.new topologist
loud_topologist = Loudspeaker.new topologist
loud_algebraic_topologist = Loudspeaker.new algebraic_topologist
Observations
- This is still brittle because of the inconsistent API between objects
- This is far from the only way to design the API of these objects
topologist = Topologist.new jokes: Topologist::JOKES
error = Algebraic.new Loudspeaker.new topologist
puts error.tell_joke
# => ...
Observations
Function Composition
Function Composition
f :: Int -> Int
f x = x + 2
g :: Int -> Int
g x = x**2
(f . g) 3 = f (g 3) = f 9 = 11
is itself a function
f . g :: Int -> Int
. :: (B -> C) -> (A -> B) -> (A -> C)
is a function too!
newFunction = f . g
newFunction 3 = 11
map newFunction [1,2,3] = [3,6,11]
Consider
-- WAI (2.0)
type App = Request -> IO Response
type Middleware = App -> App
-- Rack
type App = Env -> IO (Status, Headers, Body)
type Middleware = App -> App
serveStatic :: String -> Middleware
:: String -> (Request -> IO Response) -> (Request -> IO Response)
middlewareChain = allowOrigin "localhost" . serveStatic "/build" . logStdoutDev
appDev = middlewareChain appBase
Advantages
Pure
Advantages
Pure Data-in, data-out
- No implicit dependencies
- Easy to test and trust
- Easy to parallelize
Advantages
Equational reasoning
f x = x + 2
g (f x) = g (x + 2)
Compare with
max x y = if x > y then x else y
inc x = x ++
max (inc x) (inc y) = if x++ > y++ then x++ else y++
Advantages
Tansparent composition
f :: A -> B
g :: B -> C
g . f :: A -> C
f >> g :: A -> C
>> :: (A -> B) -> (B -> C) -> (A -> C)
- Functions always have one input type and one output type
- Functions are composable whenever their output and input types match up
Topologists in Haskell
topologist :: [String] -> String
topologist jokes = head (shuffle jokes)
topologist :: [Joke] -> Joke
topologist :: [String] -> String
topologist = shuffle >> head
topologist jokes = (head . shuffle) jokes
-- >> = flip .
topologist jokes = (shuffle >> head) jokes
Pointless
Topologists in Haskell
algebraicTopologist :: [String] -> String
algebraicTopologist jokes = (filter gets >> shuffle >> head) jokes
where
gets joke = length joke < 200 || isInfixOf "group" joke
But wait ...
topologist = shuffle >> head
algebraicTopologist = filter gets >> shuffle >> head
algebraicTopologist = filter gets >> shuffle >> head
where
gets joke = ...
Topologists in Haskell
topologist = filter (\_ -> True) >> shuffle >> head
algebraicTopologist = filter gets >> shuffle >> head
algebraicTopologist :: [String] -> String
algebraicTopologist jokes = (filter gets >> shuffle >> head) jokes
where
gets joke = length joke < 200 || isInfixOf "group" joke
algebraicTopologist = filter gets >> shuffle >> head
where
gets joke = ...
But wait ...
Topologists in Haskell
filter isFunny >> shuffle >> head
makeTopologist :: SenseOfHumor -> Topologist
makeTopologist :: (String -> Bool) -> ([String] -> String)
topologist = makeTopologist (\_ -> True)
algebraicTopologist = makeTopologist (\joke -> length joke < 200
|| isInfixOf "group" joke)
makeTopologist isFunny = filter isFunny >> shuffle >> head
Topologists in Haskell
loudTopologist = shuffle >> head >> shout
where
shout str = upcase str ++ "!!!"
-- or, equivalently
loudTopologist = topologist >> shout
filter criteria >> shuffle >> head >> deliver
Topologists in Haskell
makeTopologist :: SenseOfHumor -> Delivery -> Topologist
makeTopologist isFunny delivery = filter isFunny >> shuffle >> head >> delivery
makeTopologist :: (String -> Bool) -> (String -> String) -> ([String] -> String)
topologist = makeTopologist (\_ -> True) (\j -> j)
= makeTopologist (const True) id
loudAlgebraic = makeTopologist (\j -> length j < 200) (\j -> upcase j ++ "!!!")
Topologists in Haskell in Ruby
double :: Int -> Int
double x = x * 2
double = ->(x) { x * 2 }
double.call 5 # => 10
addThree :: Int -> Int -> Int -> Int
addThree a b c = a + b + c
addThree = -> a do
-> b do
-> c do
a + b + c
end
end
end
addThree.call(1).call(2).call 3 # => 6
addThree = ->(a,b,c) { a + b + c }.curry
Topologists in Haskell in Ruby
makeTopologist :: (String -> Bool) -> (String -> String) -> [String] -> String
makeTopologist isFunny delivery = filter isFunny >> shuffle >> head >> delivery
makeTopologist = -> isFunny do
-> delivery do
-> jokes do
delivery.call jokes.select { |j| isFunny.call j }.sample
end
end
end
Topologists in Haskell in Ruby
makeTopologist :: SenseOfHumor -> Delivery -> Topologist
class Topologist
attr_reader :sense_of_humor, :delivery
def initialize sense_of_humor:, delivery:
@sense_of_humor, @delivery = sense_of_humor, delivery
freeze
end
end
Topologists in Haskell in Ruby
makeTopologist :: SenseOfHumor -> Delivery -> Topologist
class Topologist
attr_reader :sense_of_humor, :delivery
def initialize sense_of_humor:, delivery:
@sense_of_humor, @delivery = sense_of_humor, delivery
freeze
end
def call jokes
delivery.call jokes.select { |joke| sense_of_humor.call joke }.sample
end
end
topologist = Topologist.new(
sense_of_humor: ->(joke) { true },
delivery: ->(joke) { joke }
)
loud_topologist = Topologist.new(
sense_of_humor: ->(joke) { true },
delivery: ->(joke) { joke.upcase + "!!!" }
)
Topologists in Haskell in Ruby
makeTopologist :: SenseOfHumor -> Delivery -> Topologist
class Topologist
def with overrides={}
self.class.new({
sense_of_humor: sense_of_humor,
delivery: delivery
}.merge overrides)
end
end
loud_topologist = topologist.with(
delivery: ->(joke) { joke.upcase + "!!!" }
)
Topologists in Haskell in Ruby
class SenseOfHumor
def initialize word:, max_length:
@word, @max_length = word, max_length
freeze
end
def call joke
joke.length < @max_length || joke.include?(@word)
end
end
algebraic = SenseOfHumor.new max_length: 200, word: "group"
topologist.with sense_of_humor: algebraic
loud_topologist.with sense_of_humor: algebraic
Embedded Haskell
Rules Guidelines for functional Ruby
- Objects should have a defined collection of fields. An object's state should be expressible entirely in terms of those fields.
- Fields should be set at initialization and then frozen. "Updates" are performed by creating a new object.
- Objects should respond to
call
to perform their (primary) responsibility.
Embedded Haskell
Advantages
In this system, our objects
- have a clearly defined responsibility
- have a predictable interface
- are immutable
- have injectable dependencies
- are easy to configure and re-configure
- are easy to test
- are easy to mock in tests
Functions are like Legos
In Ruby
you already think about types and composition
def create_post request
post = parse_input request
id = persist post
success_with_id id
end
parse_input :: Request -> Post
persist :: Post -> Int
success_with_id :: Int -> String
create_post = parse_input >> persist_post >> success_with_id
In Ruby
you already think about duck types and composition
def create_post request
post = parse_input request
id = persist post
success_with_id id
end
def persist obj
obj.save!
obj.id
end
parse_input :: Request -> Post
persist :: Persistable -> Int
success_with_id :: Int -> String
create_post = parse_input >> persist >> success_with_id
In Ruby
you already think about duck types and composition
def create_post request
post = parse_input request
id = persist post
success_with_id id
end
def persist obj
if valid? obj
obj.save
obj.id
end
end
parse_input :: Request -> Post
persist :: Persistable -> Int?
success_with_id :: Int -> String
create_post = parse_input >> persist >> success_with_id
Embedded Haskell
Testing
class Multiples
fields :factor
def call count
1.upto(count).map { |i| i * factor }
end
end
m = Multiples.new factor: 7
cases = {
1 => [7],
2 => [7,14],
3 => [7,14,21],
-1 => []
}
cases.each do |input, output|
expect(m.call input).to eq output
end
Embedded Haskell
Testing
class Bot
fields :handlers, # :: [Handler]
:dispatcher, # :: [Handler] -> Message -> Handler
:http # :: Request -> Nil
def call request; ...; end
end
let(:bot) { Bot.new ... } # or a "live" production Bot configuration
let(:message) { Message.new text: "hello", user: ... }
Embedded Haskell
Testing
Possible considerations
- Given an input, expect an output
- Need a dependency to produce a particular value
- Need a dependency to receive a particular value
Embedded Haskell
Testing
# 1) Input => Output
response = bot.with(
http: ->(req) { }
).call message
# 2) Dependecy returns a particular value
response = bot.with(
dispatcher: ->(reqs, msg) { TestHandler.new }
).call message
# 3) Dependency receives a particular value
http = double "HTTPClient"
expect(http).to receive(...)
response = bot.with(http: http).call message
expect(response.text).to eq "..."
Example - Medlink
class SMS::OrderPlacer
def initialize sms
@sms = sms
end
def run!
parse_message
build_order
validate_order
record
send_response
end
private
# ...
end
Example - Medlink
class SMS::OrderPlacer
def initialize sms
@sms = sms
end
def run!
parse_message
build_order
validate_order
record
send_response
end
private
def parse_message
@user = something_involving @sms
@text = something_else_involving @sms
end
def send_response
Medlink.notifier.notify "#{@user} has ordered #{@order.supplies.to_sentence}"
end
# ...
end
When composition is implicit, so are boundaries
Prefer explicit boundaries with explicit values
parse_message :: SMS -> ParseResult
build_order :: ParseResult -> Order
validate_order :: Order -> Order -- ??
record :: Order -> Order -- ??
send_response :: Order -> String
parse_message >> build_order >> validate_order >> record >> send_response
Example - Composed
class SMS::OrderPlacer
fields :notifier, :db
def call sms
send_response( record( validate_order( build_order( parse_message( sms )))))
end
private
def parse_message sms
...
end
Example - Composed
class SMS::OrderPlacer
fields :notifier, :db
def call sms
compose(
:parse_message,
:build_order,
:validate_order,
:record,
:send_response
).call sms
end
def compose *methods
->(val) do
result = val
methods.each do |name|
result = method(name).call result
end
result
end
end
end
Example - Composed
class SMS::OrderPlacer
fields :notifier, :db
def call sms
compose(
:parse_message,
:build_order,
:validate_order,
:record,
:send_response
).call sms
end
def compose *methods
->(val) do
methods.reduce(val) do |result, name|
method(name).call result
end
end
end
end
Example - Composed
class SMS::OrderPlacer
fields :notifier, :db
def call sms
compose(:parse_message, :build_order, :validate_order,
:record, :send_response).call sms
end
def parse_message sms
ParseResult.new user: ..., text: ...
end
def record order
db.save_order order
order
end
def send_response order
notifier.call "#{order.user} has ordered #{order.supplies.to_sentence}"
"Got it! Your #{order.supplies.to_sentence} are on the way"
end
# ...
end
Example - Composed
placer = Medlink.container.sms_order_placer.with(
db: instance_double(Medlink::Repository),
notifier: ->(_) { nil }
)
expect(placer.db).to receive(:save_order)
message = Message.new(
text: "Order bandages, mosquito netting - please and thank you!",
user: ...
)
response = placer.call message
expect(response).to eq \
"Got it! Your bandages and mosquito netting are on the way!"
Example - Extracted
class SMS::OrderPlacer
fields :notifier, :db, :order_placer
def call sms
compose(:parse_message, :build_order, order_placer).call sms
end
def compose *methods
->(val) do
methods.reduce(val) do |result, m|
func = m.respond_to?(:call) ? m : method(m)
func.call result
end
end
end
end
let(:placer) { Medlink.container.sms_order_placer.with order_placer: id }
order = placer.call Message.new(
text: "Order bandages, mosquito netting",
user: ...
)
expect(order.supplies).to eq ["bandages", "mosquito netting"]
Example - Extracted
class OrderPlacer
fields :notifier, :db
def call order
compose(:validate_order, :record, :send_response).call sms
end
private
# other methods extracted from SMS::OrderPlacer ...
end
Methods are a service object in waiting
SMS
parse_message :: SMS -> ParseResult
build_order :: ParseResult -> Order
validate_order :: Order -> Order
record :: Order -> Order
send_response :: Order -> String
sms_order_placer :: SMS -> String
ParseResult
Order
String
Order
Order
Example - Maybe
SMS
-- Maybe Type ~> Type | nil
parse_message :: SMS -> Maybe ParseResult
build_order :: ParseResult -> Maybe Order
validate_order :: Order -> Maybe Order
record :: Order -> Maybe Order
send_response :: Order -> Maybe String
sms_order_placer :: SMS -> Maybe String
ParseResult
Order
Maybe ParseResult
Maybe Order
Order
Order
Maybe Order
Maybe Order
Maybe String
Example - Maybe
Example - Maybe
class SMS::OrderPlacer
def call sms
compose(:parse_message, :build_order, :validate_order,
:record, :send_response).call sms
end
def compose *methods
->(val) do
methods.reduce(val) do |result, me|
# call me, maybe
if result
func = me.respond_to?(:call) ? me : method(me)
func.call result
end
end
end
end
end
-- Maybe Type ~> Type | nil
parse_message :: SMS -> Maybe ParseResult
build_order :: ParseResult -> Maybe Order
validate_order :: Order -> Maybe Order
record :: Order -> Maybe Order
send_response :: Order -> Maybe String
sms_order_placer :: SMS -> Maybe String
Example - Maybe
class SMS::OrderPlacer
def validate order
return if order.supplies.none?
return if ...
end
end
db = instance_double Medlink::Repository
expect(db).not_to receive(:save_order)
response = sms_order_placer.call Message.new(text: "Order invalid", user: ...)
expect(response).to eq nil
-- Maybe Type ~> Type | nil
parse_message :: SMS -> Maybe ParseResult
build_order :: ParseResult -> Maybe Order
validate_order :: Order -> Maybe Order
record :: Order -> Maybe Order
send_response :: Order -> Maybe String
sms_order_placer :: SMS -> Maybe String
SMS
ParseResult
Order
E[ParseResult]
E[Order]
Order
Order
E[Order]
E[Order]
E[String]
Example - Either
-- E[Type] = Either Type String
parse_message :: SMS -> E[ParseResult]
build_order :: ParseResult -> E[Order]
validate_order :: Order -> E[Order]
record :: Order -> E[Order]
send_response :: Order -> E[String]
sms_order_placer :: SMS -> E[String]
Example - Either
-- E[Type] = Either Type String
parse_message :: SMS -> E[ParseResult]
build_order :: ParseResult -> E[Order]
validate_order :: Order -> E[Order]
record :: Order -> E[Order]
send_response :: Order -> E[String]
sms_order_placer :: SMS -> E[String]
Either = Struct.new :value, :error
class SMS::OrderPlacer
def success value; Either.new value, nil; end
def error err; Either.new nil, err ; end
# N.B. This just uses regular old return values
def validate order
return error("Can't order none many supplies") if order.supplies.none?
return error("Something else") if ...
success order
end
end
Example - Either
-- E[Type] = Either Type String
parse_message :: SMS -> E[ParseResult]
build_order :: ParseResult -> E[Order]
validate_order :: Order -> E[Order]
record :: Order -> E[Order]
send_response :: Order -> E[String]
sms_order_placer :: SMS -> E[String]
class SMS::OrderPlacer
def compose *methods
->(val) do
methods.reduce(val) do |result, m|
if result.error
result
else
func = m.respond_to?(:call) ? m : method(m)
func.call result.value
end
end
end
end
end
Example - Logging
class SMS::OrderPlacer
fields :notifier, :db, :logger
# call :: SMS -> Either Order String
def call sms; ...; end
end
class SMS::OrderPlacer
fields :notifier, :db
# call :: SMS -> (Result, Logs)
def call sms; ...; end
end
response, logs = sms_order_placer.call message
expect(logs.count).to be > 2
expect(logs.first).to eq "Received new order from ..."
SMS
ParseResult
Order
LE[ParseResult]
LE[Order]
Order
Order
LE[Order]
LE[Order]
LE[String]
Example - Logging
-- LE[Type] = (Either Type String, [String])
parse_message :: SMS -> LE[ParseResult]
build_order :: ParseResult -> LE[Order]
validate_order :: Order -> LE[Order]
record :: Order -> LE[Order]
send_response :: Order -> LE[String]
sms_order_placer :: SMS -> LE[String]
Example - Logging
class SMS::OrderPlacer
def success value, *messages
[Either.new(value, nil), messages]
end
# record :: Order -> [Either Order String, [String]]
def record order
if db.save_order(order)
success order, "Saved order with #{order.supplies.count} supplies"
else
error ...
end
end
end
Example - Logging
class SMS::OrderPlacer
def compose *methods
->(val) do
methods.reduce([val, []]) do |result, m|
either, logs = result[0], result[1]
if either.error
result
else
func = m.respond_to?(:call) ? m : method(m)
new_value, new_logs = func.call either.value
[new_value, logs + new_logs]
end
end
end
end
end
- We can build error handling, logging, and more using pure functions and extended composition strategies
- None of the compose implementations used instance methods * ; each is a generic and reusable strategy
-
Maybe.compose
,Either.compose
, ...
Observations
Extending Composition
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
- an A or an error object
- a list of A's
- a promise returning an A
- a probability distribution of A's
- writing to a stream and returning an A
- reading from a repo and returning an A
Extending Composition
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
- M is a functor
- "Moving up" is lift or map
- Exercise: convince yourself that List is a functor (and map is map)
Extending Composition
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
- Vertical lines are return or pure
- Diagram is commutative
Extending Composition
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
- M is a monad
- "Moving up" is bind or >>=
- Exercise: List is a monad (what is bind?)
A monad is just a monoid in the category of endofunctors, what's the problem?
A monad is like an assembly line of snails eating burritos
A monad is a way to extend a computation composably
Parting Thoughts
- Sorry for writing another monad tutorial
- If you're interested in trying this style out on e.g. your service layer check out
pzol/deterministic
anddry-rb/dry-container
- Blog post and references to follow
How you compose is as important as what you're composing
Be explicit and intensional and it will be extensible
James Dabbs
@jamesdabbs
github/jamesdabbs
Composition
By James Dabbs
Composition
RubyConf 2016 talk on OO and FP and the nature of composition
- 2,825