函数式 Ruby 编程

image.jpg

欧阳继超

oyanglulu@gmail.com

Table of Contents

Agenda

  • 什么是函数式编程
  • Ruby 的一些函数式特性
  • 使用 Monad 纯化/简化控制流

FBI Warning

functional-ruby-qr.jpg

用 Nokia 的同学请自己手动输入 git.io/fprb

我是…

image.png

我是…

image.png 6336168ecbbf4fbdc46e.png?username=jcouyang&width=400&height=53;.png

我还是

s25996532.jpg s28861278.jpg

啊?

00e33c72-6b15-11e6-8bdf-a1a58a3631b4.jpg

所以是…一个会点 Scala 的 JavaScript 程序员来教 Rubyist 函数式编程?

我在 JavaScript 社区讲函数式的时候, 观众是这样的…

00e33c72-6b15-11e6-8bdf-a1a58a3631b4.jpg

Ruby 函数式 ?

321ff7d2-6a46-11e6-903e-8cc84f3acc78.JPG

You might be surprised to see Ruby in the list of functional languages because they generally count as object oriented languages. – Martin Odersky

你可能奇怪我把Ruby也放到了函数式语言的列表, 这些语言通常会被归到面向对象语言. – Scala 之父

什么是函数式

data-port.gif

  • 一等函数 first class function / 入 lambda
  • 纯 purity
  • 引用透明性 referential transparency
  • 无副作用 side effectless
  • 不可变 immutability
  • 持久化数据结构 persistent data structures

…当纯到一定程度可能就需要

  • 范畴论 Catergory Theory

好处呢?

  • 好组合 composible
  • 好推理 easy to reason about
  • 好测试 easy to test
  • 好多线程 Multi-thread
  • 好玩 fun
  • 高逼格 high bigger elegant

你可能不知道的Ruby

020c3f88-6a50-11e6-88bd-6c6ad6815495.png

lambda aka 匿名函数

[多选题] 请选出所有的 lambda

A: {}/do end # such as =[1,2,3].map{|x| x+1 }=
B: plus1 = lambda {|x| x + 1 }
C: plus1 = -> (x) { x + 1 }
D: plus1 = Proc.new { |x| x + 1 }

万物皆对象, lambda 也不例外

lambda 也就是一个正常的对象

plus1 = ->(x) { x + 1 }
#<Proc:0x007fbaea988030@-:3 (lambda)>

如果给这个lambda一个引用,我们可以跟用method一样用

plus1 = ->(x) { x + 1 }
plus1.call(3)
plus1.(3)
plus1[3]
4

三等公民

def plus1 x
  x + 1
end
[1,2,3,4].map &plus1
`plus1': wrong number of arguments (0 for 1) (ArgumentError)

一等 vs 三等

image.png

一等公民 Proc

plus1 = ->(x) { x + 1 }
[1,2,3,4].map &plus1
[2, 3, 4, 5]

给三等座升个舱

def plus1 x
  x + 1
end
first_class_plus1 = method(:plus1)
[1,2,3,4].map &first_class_plus1
[2, 3, 4, 5]

升舱的魔法 #to_proc

method(:plus1)
# => #<Method: Object#plus1>
class Method
  def to_proc
    lambda{|*args|
      self.call(*args)
    }
  end
end

升舱实例2 - Symbol

%w(ouyang jichao).map &:capitalize 
# ===
%w(ouyang jichao).map { |x| x.capitalize}
["Ouyang", "Jichao"]

来 🍬 Desugar &

%w(ouyang jichao).map &:capitalize.to_proc
%w(ouyang jichao).map &Proc.new(|x| x.send(:capitalize))
["Ouyang", "Jichao"]

模式匹配 pattern matching

3o6MbdPcxvF7Hb5G3S.gif

destructure - 数组

first, *middle_and_last = ['Phillip', 'Jay', 'Fry']
"first: #{first}, middle_and_last: #{middle_and_last}"
"first: Phillip, middle_and_last: [\"Jay\", \"Fry\"]"

destructure - 哈希

方法的参数会自带 destructure 哈希的功能 aka keyword arguments:

fry = {first: 'Phillip', middle: 'Jay', last: 'Fry'}
def printFirstName first:, **rest
  p first, rest
end
printFirstName fry
["Phillip", {:middle=>"Jay", :last=>"Fry"}]

case when

ruby 中的 case 可以搞定这几种模式匹配

  • 值/表达式
  • 类型
  • Proc
  • 正则

这个很简单,应该都有用过

me = 'ouyang'
case me
when 'ouyang' 
  "hehe #{me}"
else 'hehe jichao'
end
hehe ouyang

类型

class Me
  def initialize name
    @name = name
  end

  def heheda
    "呵呵哒 #{@name}"
  end
end

me = Me.new 'ouyang'

case me
when Me
  me.heheda
else
  '呵呵哒了'
end

lambda (aka guard)

require 'ostruct'
me = OpenStruct.new(name: 'jichao', first_name: 'ouyang')
case me
when ->(who){who.name=='jichao'}
  "hehe #{me}"
end
hehe #<OpenStruct name="jichao", first_name="ouyang">

正则

case 'jichao ouyang'
when /ouyang/
"呵呵哒"
end
"呵呵哒"

但其实只是个简单的语法糖

case when 并不是magic,其实只是 if else 的语法糖, 比如上面说的正则

if(/ouyang/ === 'jichao')
  "heheda"
end

所以 magic 则是所有 when 的对象都实现了 === 方法而已

  • 值: object.=== 会代理到 ==
  • 类型: Module.=== 会看是否是其 instance
  • 正则: regex.=== 如果匹配返回 true
  • 表达式:取决于表达式返回的值的 === 方法
  • lambda: proc.=== 会运行 lambda 或者 proc

55xWvUIMb51mw.gif

说了这么些奇技淫巧, 逼格还是不够高呀 除了花式一些有什么用呢?

纯 pure

Category Theory

Monad - 自函子范畴上的含幺半群

一个简单 🌰

把大象放冰箱里需要几步

image.png

命令式放大象

opened_fridge = open_fridge
if opened_fridge
  fridge_w_elephent = put_elephent_in opened_fridge
  if fridge_w_elephent
    closed_fridge = close_fridge
    if closed_fridge
      'yay'
    else
      'fail to close fridge'
    end
  else
    'fail to put elephent in'
  end
else
  'fail to open fridge'
end

监控

opened_fridge = open_fridge
if opened_fridge
  Monitoring.logger.info('fridge opened')
  fridge_w_elephent = put_elephent_in opened_fridge
  if fridge_w_elephent
    Monitoring.logger.info('puted a elephent into fridge')
    closed_fridge = close_fridge
    if closed_fridge
      Monitoring.logger.info('fridge closed')
      'yay'
    else
      Monitoring.logger.error('no able to close fridge')
      'fail to close fridge'
    end
  else
    Monitoring.logger.error('elephent put failed')
    'fail to put elephent in'
  end
else
  Monitoring.logger.error('fail to open fridge')
  'fail to open fridge'
end

或者用更极端的抛异常方式

begin
  close(put_elephent_in open_fridge)
rescue A=>e
 ...
rescue B=>e
 ...
rescue C=>e
 ...
end

广告时间

😹 ➡️ 😼
⬇️ ↘️ ⬇️
🙀 ➡️ 😻

猫呢?

https://git.io/cats.rb

让我们用一个简单的 Either Monad

gem install data.either
require 'data.either'
Right.new(1).flat_map do |x| 
  if x < 1
    Left.new('meh')
  else
    Right.new(x+1)
  end
end
# => #<Right 2>

来简化控制流

open_fridge.flat_map do |fridge|  # <= 1
  put_elephent_in fridge          # <= 2
end.flat_map do |fridge|
  close fridge                    # <= 3
end

这样可以专心构造控制逻辑,而不需要关心上一步如果错误该怎么办

怎么做到的

12dBjCf9NclhBe.gif

image.png

Either 魔法

def flat_map
  case self
  when Right
    yield @v
  else
    self
  end
end

一个更实际的 🌰

用 microservices 组合成新的 service

image.png

上图有几次 IO

  • 总共4个IO, 每一步骤都可能出错
  • 但程序猿不希望漏掉任何错误信息
  • 但是又不能为了监控,影响了这个简单的工作流

控制流不关心失败和监控

do
  a <- fetchA
  b <- fetchB
  c <- put $ blah a ++ b

IO自挂东南枝

image.jpg

def fetch(endpoint, decoder)
  response = self.class.get(endpoint, format: :json)
  case response.code
  when 410
    Left.new(Exceptions::DataFailure.new("Resource #{endpoint} was deleted"))
  when 404
    Left.new(Exceptions::DataFailure.new("Resource #{endpoint} not exist"))
  when 200
    Right.new decoder.from_json(response.body)
  else
    Left.new(Exceptions::RepositoryError.new("Fetching #{endpoint} with Error:\n#{endpoint}, response code: #{response.code}"))
  end
end

failure_processed, success_processed = Either.partition Mapinator.run

Monitoring.send_processed success_processed.length
Monitoring.logger.info("Processed successful #{success_processed.length} listings: #{success_processed}")
Monitoring.logger.error("Processed FAILURE #{failure_processed.length} with Exceptions:") unless failure_processed.empty?
...

还可不可以在纯一些

haskell.png

Free Monad aka Interpreter Pattern

image.png

有些像 Cons

image.png

还有…

  • Coyoneda
  • Free Monoid
  • State
  • EitherT
  • MaybeT

这些我都不会讲…

因为我不会讲…

希望不久之后可以…

gem install control.monad.free

Q/A

性能

image.png

你TM都选 Ruby 了还在乎性能?

并发多线程 made easy

require "celluloid/autostart"

module Enumerable
  def pmap(&block)
    futures = map { |elem| Celluloid::Future.new(elem, &block) }
    futures.map(&:value)
  end
end

多谢

/

函数式 Ruby 编程 - 欧阳继超