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

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
• Today

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

Lessons Learned

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, ...

Lessons Learned

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

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

# => ...

Function Composition

f(x) = x + 2
$f(x) = x + 2$
g(x) = x^2
$g(x) = x^2$
(f \circ g)(3)
$(f \circ g)(3)$
f(1) = 1 + 2 = 3
$f(1) = 1 + 2 = 3$
g(5) = 25
$g(5) = 25$
= f(g(3))
$= f(g(3))$
= f(9)
$= f(9)$
= 11
$= 11$

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
f \circ g
$f \circ g$

is itself a function

f . g :: Int -> Int
. :: (B -> C) -> (A -> B) -> (A -> C)
\circ
$\circ$

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

Pure

Pure Data-in, data-out

• No implicit dependencies
• Easy to test and trust
• Easy to parallelize

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++

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

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

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 = ...

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 ...

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

loudTopologist = shuffle >> head >> shout
where
shout str = upcase str ++ "!!!"

-- or, equivalently
loudTopologist = topologist >> shout
filter criteria >> shuffle >> head >> deliver

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 ++ "!!!")

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

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

makeTopologist :: SenseOfHumor -> Delivery -> Topologist
class Topologist

def initialize sense_of_humor:, delivery:
@sense_of_humor, @delivery = sense_of_humor, delivery
freeze
end
end

makeTopologist :: SenseOfHumor -> Delivery -> Topologist
class Topologist

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 + "!!!" }
)

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 + "!!!" }
)

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

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.

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

In Ruby

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

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

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

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

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: ... }

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

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"

response = bot.with(http: http).call message

expect(response.text).to eq "..."

class SMS::OrderPlacer
def initialize sms
@sms = sms
end

def run!
parse_message
build_order
validate_order
record
send_response
end

private

# ...
end

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
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(
notifier: ->(_) { nil }
)

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

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

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, ...

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]

• "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 and dry-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

By James Dabbs

Composition

RubyConf 2016 talk on OO and FP and the nature of composition

• 2,688