Andrew McCluskey
@ajmacca
It's programming with functions
Mapping of inputs to outputs
Consequences: referential transparency / purity
Replacing any call to a function with the function's return value results in a program with identical behavior.
def add(a, b)
a + b
end
def lyf
2 * add(18, 3)
end
def add(a, b)
a + b
end
def lyf
2 * 21
end
def add(a, b)
# HERE BE EFFECTS!
puts "Adding #{a} and #{b} and #{@c}"
a + b + @c
end
def lyf
2 * add(18, 3)
end
Why is referential transparency desirable?
What am I giving up with referential transparency?
What am I not giving up?
Our programs don't execute side effects, they produce computations as pure values that are executed by a runtime system.
def main(args)
<<-MAIN
num_echoes = #{args[0]}
loop do
s = $stdin.gets
break if s.chomp == 'q'
puts(s * num_echoes)
end
MAIN
end
eval(main(ARGV))
main = do
args <- getArgs
let
e s =
error $ "Unable to parse '" ++ s ++ "' as Int"
go' =
case args of
(s:_) -> either (const (e s)) go $ readEither s
_ -> error "Expected at least one arg"
go _ "q" = pure ()
go n s = do
(() <$) . replicateM n . putStrLn $ s
getLine >>= go n
getLine >>= go'
Why are types desirable?
data Vehicle = Car
| Bicycle
| MotorBike
describeVehicle :: Vehicle -> String
describeVehicle Car =
"Closed in people container on 3 or more wheels"
describeVehicle Bicycle =
"Person powers two wheels with pedals"
describeVehicle MotorBike =
"Two wheels powered by a motor"
def describe_vehicle(v)
case v
when :car
"Closed in people container on 3 or more wheels"
when :bicycle
"Person powers two wheels with pedals"
when :motorbike
"Two wheels powered by a motor"
else
raise "I don't know how to describe a '#{v}'"
end
end
Types aren't bad - type systems are sometimes bad.
Higher order functions are essential to FP. Ruby doesn't quite support them, but we can approximate them with blocks/Proc
.
def map_block(a)
b = []
for x in a
b << (yield x)
end
b
end
a = [1,2,3]
b = map(a) { |n| n * 3 }
# b == [3,6,9]
def map_block(a)
b = []
for x in a
b << (yield x)
end
b
end
a = [1,2,3]
f = Proc.new { |n| n * 3 }
b = map(a, &f)
# b == [3,6,9]
def map_block_arg(a, &f)
b = []
for x in a
b << f.call(x)
end
b
end
a = [1,2,3]
b = map(a) { |n| n * 3 }
# b == [3,6,9]
def map_proc(a, f)
b = []
for x in a
b << f.call(x)
end
b
end
a = [1,2,3]
f = Proc.new { |n| n * 3 }
b = map_proc(a, f)
# b = [3,6,9]
Symbol#to_proc
for instance methods
# You wrote:
[1,2,3].map { |n| n.to_s }
# Why not:
[1,2,3].map(&:to_s)
&
prefix calls #to_proc
#to_proc
on symbols returns a Proc
that executes the named instance methodAvoid methods with !
suffix.
# Unneccessary mutation
things = ['keyboard', 'mouse', 'display']
things.map! { |s| s.upcase }
# things == ['KEYBOARD', 'MOUSE', 'DISPLAY']
# Immutability
things2 = ['keyboard', 'mouse', 'display']
up_things2 = things.map { |s| s.upcase }
# things2 == ['keyboard', 'mouse', 'display']
# up_things2 == ['KEYBOARD', 'MOUSE', 'DISPLAY']
Avoid each
and for
loops. Instead use functions like:
select
map
inject
zip
select
nums = []
(1..10).each { |n| nums << n if n.even? }
# Better
nums = (1..10).select { |n| n.even? }
# Best
nums = (1..10).select(&:even?)
# => [2,4,6,8,10]
map
nums = [1,2,3]
squares = []
nums.each { |n| squares << n ** 2 }
# Better
squares = nums.map { |n| n ** 2 }
# => [1,4,9]
inject
# Bad
nums = [1,2,3]
total = 0
nums.each { |n| total += n }
# Good
total = nums.inject(0) { |sum, n| sum + n }
# Best
total = nums.inject(0, &:+)
# this...
nums = [1,2,3]
nums_to_squares = {}
nums.each { |n| nums_to_squares[n] = n ** 2 }
# ...or this
nums_to_squares = nums.inject({}) do |h, n|
h[n] = n ** 2
h
end
zip
odds = [1,3]
evens = [2,4]
pairs = []
(0..[odds.length, evens.length].min).each { |i|
pairs << [odds[i], evens[i]]
}
# pairs == [[1,2], [3,4]]
zip_pairs = odds.zip(evens)
# zip_pairs == [[1,2], [3,4]]
odds = [1,3,5]
evens = [2,4,6]
foo = odds.zip(evens) { |(a,b)| a * b }
foo == nil
!!!odds = [1,3,5]
evens = [2,4,6]
foo = odds.zip(evens) { |(a,b)| a * b }
odds = [1,3,5]
evens = [2,4,6]
foo = []
odds.zip(evens) { |(a,b)| foo << a * b }
# foo == [2,12,30]
self
methodsclass Foo
def self.do_the_thing
# can't access instance variables or methods
end
end
Struct
Foo = Struct.new(:bar, :baz, :whoozitz)
foo = Foo.new(1,2,3)
# => true
foo.bar + foo.baz == foo.whoozitz
# => Error!
foo.quux.nil?
Foo = Struct.new(:bar, :baz) do
def with_bar(bar)
self.new(bar, self.baz)
end
def total
self.bar + self.baz
end
end
module TicTacToe
Game = Struct.new(:board, :next_symbol, :winner)
def self.finished?(game)
!game.winner.nil? || board_full?(game.board)
end
def self.with_winner(game, winner)
self.new(game.board, game.next_symbol, winner)
end
end
addThreeMaybes h =
liftA3 (\a b c -> a + b + c)
(lookup h "foo")
(lookup h "bar")
(lookup h "baz")
-- List of keys can be arbitrarily long.
-- If any aren't present, we get `Nothing`.
addThreeMaybes h keys =
fmap sum . traverse (`M.lookup` h) $ keys
if h[:foo]
if h[:bar]
if h[:baz]
h[:foo] + h[:bar] + h[:baz]
else
nil
end
else
nil
end
else
nil
end
def self.add_three_failures(h)
a = h["a"]
b = h["b"]
c = h["c"]
unless a.nil? || b.nil? || c.nil?
a + b + c
else
nil
end
end
def self.add_three_failures_list(h, keys)
keys.inject(0) { |a, k|
n = h[k]
(a.nil? || n.nil?) ? nil : a + n
}
end
What if the computation to get each value changes?
addThreeValidations h keys =
let
f k = maybe (AccFailure $ ["Couldn't find key: " <> k])
AccSuccess
(M.lookup k h)
in
fmap sum . traverse f $ keys
-- "foo" and "baz" aren't keys in the map
addThreeValidations someMap ["foo", "bar", "baz"]
-- => AccFailure [ "Couldn't find key: foo",
-- , "Couldn't find key: baz"
-- ]
addMaybes h keys =
let
f = (`M.lookup` h)
in
fmap sum . traverse f $ keys
addValidation h keys =
let
f k = maybe (AccFailure $ ["Couldn't find key: " <> k])
AccSuccess
(M.lookup k h)
in
fmap sum . traverse f $ keys
fmap sum . traverse f $ keys
fmap sum . traverse f $ keys
addThings f xs =
fmap sum . traverse f $ xs
addMaybes h keys =
addThings (`M.lookup` h) keys
addValidations h keys =
let
validatedLookup k =
maybe (AccFailure $ ["Couldn't find key: " <> k])
AccSuccess
(M.lookup k h)
in
addThings validatedLookup keys
addMultiplesOf n ns =
addThings (*) ns n
-- > addMultiplesOf 5 [1,2,3]
-- 30
def self.add_things(fmap, pure, lifta2, f, keys)
fas = keys.inject(pure.call([])) { |fas, k|
fa = f.call(k)
lifta2.call(fa, fas) { |a, as| as << a }
}
fmap.call(fas) { |as| as.inject(0, &:+) }
end
def self.add_things_nil(h, keys)
pure = Optional.method(:new)
fmap = ->(o, &f){
o.and_then { |x| pure.call(f.call(x)) }}
lifta2 = ->(fa, fb, &g){
fa.and_then { |a|
fb.and_then { |b|
pure.call(g.call(a, b))}}}
f = ->(k){pure.call(h[k])}
add_things(fmap, pure, lifta2, f, keys)
end
Proc
Try these ideas on your Ruby
Homework: write tic-tac-toe using these principles
Data61 course in Brisbane 06-08 Feb
Register by 26 Jan (notify.qfpl.io)
#qfpl on Freenode
&:
explanation: https://stackoverflow.com/a/1961118/510722