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

f(x) = x + 2
f(x)=x+2f(x) = x + 2
g(x) = x^2
g(x)=x2g(x) = x^2
(f \circ g)(3)
(fg)(3)(f \circ g)(3)
f(1) = 1 + 2 = 3
f(1)=1+2=3f(1) = 1 + 2 = 3
g(5) = 25
g(5)=25g(5) = 25
= f(g(3))
=f(g(3))= f(g(3))
= f(9)
=f(9)= f(9)
= 11
=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
fgf \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

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

Made with Slides.com