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
end
emmy = 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
end
or
class LoudAlgebraicTopologist < LoudTopologist
def jokes
@jokes.select { |joke| gets? joke }.sample
end
private
def gets? joke; ...; end
end
class LoudAlgebraicTopologist < ??
end
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"
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
topologist = Topologist.new jokes: Topologist::JOKES
algebraic_topologist = Algebraic.new topologist
loud_topologist = Loudspeaker.new topologist
loud_algebraic_topologist = Loudspeaker.new algebraic_topologist
topologist = 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 = 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]
-- 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
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] -> 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
attr_reader :sense_of_humor, :delivery
def initialize sense_of_humor:, delivery:
@sense_of_humor, @delivery = sense_of_humor, delivery
freeze
end
end
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 + "!!!" }
)
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
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
end
parse_input :: Request -> Post
persist :: Post -> Int
success_with_id :: Int -> String
create_post = parse_input >> persist_post >> success_with_id
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
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
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
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
# ...
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
Medlink.notifier.notify "#{@user} has ordered #{@order.supplies.to_sentence}"
end
# ...
end
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
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
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
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
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
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!"
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"]
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
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
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
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]
-- 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
end
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]
-- 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
end
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
Maybe.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