入門

関数型-ish プログラミング
on Ruby

片月亭 黒曜
(@kokuyouwind)

$ whoami

  • 森 俊介 / 黒曜

  • @kokuyouwind

  • 株式会社Misoca

  • 一応Rubyエンジニア

  • 最近はTerraformとかDockerをよく触っている

片月亭

片月(へんげつ)= 弦月

片月亭(へんげつてい)?

片月見(かたつきみ)

中秋の名月(八月十五夜)と、
豆名月(九月十三夜)の
片方だけ見ること

片月亭(かたつきてい)

片月亭(かたつてい)

型付けてぇ!!!

本日のお題

  • 関数型プログラミングとは

  • Rubyの「関数」っぽいものの話

  • Rubyで「関数型プログラミング」っぽく書くとどうなるか

  • まとめ

本日のお題

  • 関数型プログラミングとは

  • Rubyの「関数」っぽいものの整理

  • Rubyで「関数型プログラミング」っぽく書くとどうなるか

  • まとめ

入門

関数型-ish プログラミング
on Ruby

片月亭 黒曜
(@kokuyouwind)

🔥

※個人の見解です

関数型プログラミング

入力と出力の対応を定義する「関数」と、
それに基づく変換処理を行う「関数適用」とを
基本単位とするプログラミングパラダイム

3

incr

4

sqrt

2

関数型プログラミング

「関数」自体も、
「関数」の入力や出力になったり、
変数に束縛したりできる
(第一級関数)

[1, 4, 9]

sqrt

map

[1, 2, 3]

ちなみに

関数型プログラミング
Functional Programing

型システム
Type System

日本語だと「型」が入るが、
型システムとは関係ない

入門

関数型-ish プログラミング
on Ruby

片月亭 黒曜
(@kokuyouwind)

ish = 〜のような、〜っぽい

Rubyに「関数」はない

関数「っぽいもの」はある

関数「っぽいもの」を使った
プログラミングの話

本日のお題

  • 関数型プログラミングとは

  • Rubyの「関数」っぽいものの話

  • Rubyで「関数型プログラミング」っぽく書くとどうなるか

  • まとめ

Ruby

「レシーバ」オブジェクトに対する
「メソッド呼び出し」が
処理の基本単位

-1

abs

1

関数との違い

関数は変数に束縛して、後から適用できる
オブジェクトは変数に束縛できるが、
メソッド呼び出しするには名前が必要

let abs2 = abs
abs2(-2) (* => 2 *)
abs2 = Math.abs # Error!!!

abs2 = Math
abs2.abs(-2)
# 大事なことなので2回(ry

関数っぽいものを作る際の問題点

関数適用→関数+引数
function(arg1, arg2, ...)

メソッド呼び出し→レシーバ+メソッド+引数
receiver.method(arg1, arg2, ...)

そもそも要素の数があわない!

解1. レシーバを固定

モジュール関数
Math.sqrt(4)

自身のメソッド呼び出し
[self.]private_method(1,2,3)

レシーバを意識せず、入力と出力にフォーカスできる
このままだと結局持ち運びはできない

解2. メソッド名を固定

Proc, Lambda
proc { | n | n * n }.call(2)
lambda { | n | n * n }.call(2)

Method
Math.method(:sqrt).call(4)

オブジェクトなので受け渡せる!

関数絡みの糖衣構文

  • call

    • proc.()

    • proc[]

  • lambda

    • ->(x, y) { x + y }

  • Method

    • Math.:sqrt (Ruby 2.7から導入)

関数絡みのメソッド

  • カリー化

add = ->(x, y) { x + y }.curry

# 引数を分けて適用できる
add.(2).(3) # => 5

# 引数を一部だけ適用して使い回せる
# (部分適用)
add10 = add.(10)
add10.(5) # => 15
add10.(100) #=> 110

関数絡みのメソッド

  • カリー化

add = ->(x, y) { x + y }.curry

# なぜか元と同じ呼び方もできる
add.(2, 3) # => 5

# なぜか空callを何回も呼べる
add.().().(2).().().(3) # => 5

関数絡みのメソッド

  • 関数合成(Ruby 2.6)

    • Math.:asin << Math.:sin

    • Math.:acos >> Math.:cos

double = ->(x) { x * 2 }
add10 = ->(x) { x + 10 }

# 2倍してから10を足す
double_then_add_10 = double >> add10
#  (1 * 2) + 10 = 12
double_then_add_10.(1)

関数絡みのメソッド

  • 関数合成(Ruby 2.6)

    • Math.:asin << Math.:sin

    • Math.:acos >> Math.:cos

double = ->(x) { x * 2 }
add10 = ->(x) { x + 10 }

# 10を足してから2倍する
double_after_add_10 = double << add10
#  (1 + 10) * 2 = 22
double_after_add_10.(1)

本日のお題

  • 関数型プログラミングとは

  • Rubyの「関数」っぽいものの話

  • Rubyで「関数型プログラミング」っぽく書くとどうなるか

  • まとめ

ニュートン法

関数の出力を0にするような入力(の近似)を求めるアルゴリズム
適当な初期値から初めて、接線とx軸の交点を次の近似値にする

x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}

微分

2点近似で求める

f'(x_0) \simeq \frac{f(x_0 + h) - f(x_0 - h)}{2h}

x0

x0-h

x0+h

f(x0-h)

f(x0+h)

微分

f'(x_0) \simeq \frac{f(x_0 + h) - f(x_0 - h)}{2h}
h = 1e-5
d = ->(f, x) {
  (f.(x + h) - f.(x - h)) / (2 * h)
}.curry

微分

f = ->(x) { x * x - 2 * x + 1 }

# d は関数を受け取って、1次微分を返す関数
df = d.(f)

# f' = 2x - 2
p df.(0) # => -1.9999999999964488
p df.(1) # => 0.0

ニュートン法

x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}
# 次の近似値を求める
newton_step = ->(f, x0) {
  x0 - f.(x0) / d.(f).(0)
}.curry

# fix(f, x)は、収束するまでfの適用を繰り返す
nearly = ->(x, y) { (x - y).abs < 1e-6 }
fix = ->(f, x) {
  nearly.(x, f.(x)) ? x : fix.(f, f.(x))
}.curry

ニュートン法

# ニュートン法は、「次の近似値を求める」操作を
# 収束するまで繰り返す
newton = ->(f) { fix.(newton_step.(f)) }

# 例: 次の関数が0になる点を求める
f = ->(x) { x*x - 2*x + 1 }

# f(1) = 0
p newton.(f).(0) # => 0.9985865949537768

油断するとcall stack死

# 収束のしきい値を 1e-6 から 1e-10 にしてみる
nearly = ->(x, y) { (x - y).abs < 1e-10 }
fix = ->(f, x) {
  nearly.(x, f.(x)) ? x : fix.(f, f.(x))
}.curry
newton = ->(f) { fix.(newton_step.(f)) }
f = ->(x) { x*x - 2*x + 1 }

# => stack level too deep (SystemStackError)
p newton.(f).(0) 

数学以外の例

Webリクエスト

パラメタのHashからモデルを作って検証

問題なければ保存する

Model = Struct.new(
  :name, keyword_init: true
)

validate = ->(model) {
  raise if model.name == ''
}
save = ->(model) { p model }

Webリクエスト

とりあえず素直に作ってみる

request = ->(params) do
  model = Model.new(params)
  validate.(model)
  save.(model)
rescue
  p :ng
end

# => #<struct Model name="test">
request.(name: 'test') 
# => :ng
request.(name: '') 

Webリクエスト

関数合成にしてみる

validate = ->(model) {
  raise if model.name == ''; model
}

request = ->(params) do
  (Model.:new >> validate >> save).(params)
rescue
  p :ng
end

# => #<struct Model name="test">
request.(name: 'test') 
request.(name: '') # => :ng

Webリクエスト

検証の成功/失敗を戻り値で表してみる

Succ = Struct.new(:model)
Fail = Struct.new(:message)

validate = ->(model) { 
  model.name == '' 
    ? Fail.new(:not_valid)
    : Succ.new(model)
}

Webリクエスト

saveにはSuccかFailが渡ってくるので、
パターンマッチ(Ruby 2.7)で値を取り出す

save = ->(maybe_model) {
  case maybe_model
  in Succ(model); p model
  in Fail(message); p message                 
  end
}

Webリクエスト

requestが関数合成だけでできる! すごい!

request = Model.:new >> validate >> save

request.(name: 'test')
# => <struct Model name="test">
request.(name: '') # => :not_valid

Webリクエスト

ValidateとSaveをcallで呼べるオブジェクトにする

class Validate
  def self.call(model)
    model.name == '' ? Fail.new(:not_valid) : Succ.new(model)
  end
end

class Save
  def self.call(maybe_model)
    case maybe_model
      in Succ(model); p model
      in Fail(message); p message                 
    end
  end
end

request = Model.:new >> Validate >> Save

本日のお題

  • 関数型プログラミングとは

  • Rubyの「関数」っぽいものの話

  • Rubyで「関数型プログラミング」っぽく書くとどうなるか

  • まとめ

わかったこと

  • Rubyで無理に関数型プログラミングは
    しなくてよいのでは???

    • ​素直に関数型プログラミング言語を使おう

  • ​流儀を知ってると応用できることがある

    • ​mapとかreduceとかは関数を渡すのに近い

    • 「操作の合成」の概念があると簡潔になる

わかったこと2

  • Lambdaは広域で束縛しにくい

    • ​定数に束縛はできるが、見た目が微妙

    • defに入れると毎回生成される

  • ​callを持ったオブジェクトを作って>>で
    合成するのは面白そう

  • パターンマッチ便利

  • 関数合成やcurryを繰り返すと型が行方不明

関数型プログラミングするなら
型付けてぇ