James Dabbs
jamesdabbs
— Wikipedia
Goals for this talk
"Haskell is easy! You just need to grok category theory and then you can starting printing stuff."
— A. Strawman
Lessons Learned
A category is
Category Theory is the study of composition
Lessons Learned
Lessons Learned
The Curry-Howard isomorphism says that proving theorems and writing programs are the same activity
a, b, ab, ba, aaa, aba, bab, bbb, ...
Lessons Learned
See e.g. the Nullstellensatz in Algebraic Geometry, the Monstrous Moonshine theorem, Homotopy Type Theory, ...
Lessons Learned
Lessons Learned
Favor object composition over class inheritance
— Gang of Four
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
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."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!!!"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 < ??
endclass 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"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_topologisttopologist = Topologist.new jokes: Topologist::JOKES
algebraic_topologist = Algebraic.new topologist
loud_topologist = Loudspeaker.new topologist
loud_algebraic_topologist = Loudspeaker.new algebraic_topologisttopologist = Topologist.new jokes: Topologist::JOKES
error = Algebraic.new Loudspeaker.new topologist
puts error.tell_joke
# => ...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]-- 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 appBasePure
Pure Data-in, data-out
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)
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
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 >> headmakeTopologist :: SenseOfHumor -> TopologistmakeTopologist :: (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 >> shoutfilter criteria >> shuffle >> head >> deliver
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 ++ "!!!")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 }.currymakeTopologist :: (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
endmakeTopologist :: 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
endmakeTopologist :: 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 + "!!!" }
)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 + "!!!" }
)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: algebraic
Rules Guidelines for functional Ruby
call to perform their (primary) responsibility.
Advantages
In this system, our objects
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_idyou 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_idyou 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_idTesting
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
endTesting
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
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 "..."class SMS::OrderPlacer
def initialize sms
@sms = sms
end
def run!
parse_message
build_order
validate_order
record
send_response
end
private
# ...
endclass 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
# ...
endPrefer 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_responseclass SMS::OrderPlacer
fields :notifier, :db
def call sms
send_response( record( validate_order( build_order( parse_message( sms )))))
end
private
def parse_message sms
...
endclass 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
endclass 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
endclass 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
# ...
endplacer = 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!"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"]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
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
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 Stringclass 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]
-- 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]-- 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-- 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
endclass 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]
-- 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]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
endclass 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
endMaybe.compose, Either.compose, ...
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
A
B
C
D
E
M[A]
M[B]
M[C]
M[D]
M[E]
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
pzol/deterministic and dry-rb/dry-container
How you compose is as important as what you're composing
Be explicit and intensional and it will be extensible
@jamesdabbs
github/jamesdabbs