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
endemmy = 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
endor
class LoudAlgebraicTopologist < LoudTopologist
def jokes
@jokes.select { |joke| gets? joke }.sample
end
private
def gets? joke; ...; end
endclass LoudAlgebraicTopologist < ??
endObject 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
endemmy = 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
endtopologist = 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_topologistObservations
- 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 = 11is 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 -> AppserveStatic :: String -> Middleware
:: String -> (Request -> IO Response) -> (Request -> IO Response)middlewareChain = allowOrigin "localhost" . serveStatic "/build" . logStdoutDev
appDev = middlewareChain appBaseAdvantages
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] -> Joketopologist :: [String] -> Stringtopologist = shuffle >> headtopologist jokes = (head . shuffle) jokes-- >> = flip .
topologist jokes = (shuffle >> head) jokesPointless
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 >> headmakeTopologist :: SenseOfHumor -> TopologistmakeTopologist :: (String -> Bool) -> ([String] -> String)topologist = makeTopologist (\_ -> True)
algebraicTopologist = makeTopologist (\joke -> length joke < 200
|| isInfixOf "group" joke)makeTopologist isFunny = filter isFunny >> shuffle >> headTopologists in Haskell
loudTopologist = shuffle >> head >> shout
where
shout str = upcase str ++ "!!!"
-- or, equivalently
loudTopologist = topologist >> shoutfilter criteria >> shuffle >> head >> deliverTopologists in Haskell
makeTopologist :: SenseOfHumor -> Delivery -> TopologistmakeTopologist 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 # => 10addThree :: Int -> Int -> Int -> Int
addThree a b c = a + b + caddThree = -> a do
-> b do
-> c do
a + b + c
end
end
end
addThree.call(1).call(2).call 3 # => 6addThree = ->(a,b,c) { a + b + c }.curryTopologists in Haskell in Ruby
makeTopologist :: (String -> Bool) -> (String -> String) -> [String] -> String
makeTopologist isFunny delivery = filter isFunny >> shuffle >> head >> deliverymakeTopologist = -> isFunny do
-> delivery do
-> jokes do
delivery.call jokes.select { |j| isFunny.call j }.sample
end
end
endTopologists in Haskell in Ruby
makeTopologist :: SenseOfHumor -> Delivery -> Topologistclass Topologist
attr_reader :sense_of_humor, :delivery
def initialize sense_of_humor:, delivery:
@sense_of_humor, @delivery = sense_of_humor, delivery
freeze
end
endTopologists in Haskell in Ruby
makeTopologist :: SenseOfHumor -> Delivery -> Topologistclass 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
endtopologist = 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 -> Topologistclass Topologist
def with overrides={}
self.class.new({
sense_of_humor: sense_of_humor,
delivery: delivery
}.merge overrides)
end
endloud_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
endalgebraic = SenseOfHumor.new max_length: 200, word: "group"
topologist.with sense_of_humor: algebraic
loud_topologist.with sense_of_humor: algebraicEmbedded 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
callto 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
endparse_input :: Request -> Post
persist :: Post -> Int
success_with_id :: Int -> String
create_post = parse_input >> persist_post >> success_with_idIn 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
endparse_input :: Request -> Post
persist :: Persistable -> Int
success_with_id :: Int -> String
create_post = parse_input >> persist >> success_with_idIn 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
endparse_input :: Request -> Post
persist :: Persistable -> Int?
success_with_id :: Int -> String
create_post = parse_input >> persist >> success_with_idEmbedded Haskell
Testing
class Multiples
fields :factor
def call count
1.upto(count).map { |i| i * factor }
end
endm = 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
endEmbedded 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
# ...
endExample - 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
# ...
endWhen 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_responseExample - 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
...
endExample - 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
endExample - 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
endExample - 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
# ...
endExample - 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
endlet(: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 ...
endMethods 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 -> StringParseResult
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 StringParseResult
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 StringExample - Maybe
class SMS::OrderPlacer
def validate order
return if order.supplies.none?
return if ...
end
enddb = 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 StringSMS
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
endExample - 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
endExample - Logging
class SMS::OrderPlacer
fields :notifier, :db, :logger
# call :: SMS -> Either Order String
def call sms; ...; end
endclass SMS::OrderPlacer
fields :notifier, :db
# call :: SMS -> (Result, Logs)
def call sms; ...; end
endresponse, 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
endExample - 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/deterministicanddry-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
- 3,105