Ruby の関数プログラミングでオイラー積を計算してみた
僕はほとんど数学の知識がないのですが, 比較的簡単に計算できるオイラー積を実際の数字を当てはめて Ruby の関数プログラミングで計算してみました.
あくまで Ruby の関数プログラミングで計算してみるとどういう風になるのだろうかという好奇心から, 得意でもない数学の分野に恐れ多くも足を踏み入れてみました.
結果的にはもちろん普通に計算してもいいのですが, Ruby の関数プログラミングで計算すると, こんな風にも計算できるんだと新たな発見をすることができました.
ただ可読性という観点においては, 初見の人は何をやっているかさっぱりだと思いますw
なので Ruby の関数プログラミングを使うとこんな風にも計算できるよ, 面白いよということを共有させていただきたく今回はこのような内容になりました.
Ruby を使われていて, 関数プログラミングって一体どんなもんだろうという興味のある方はぜひみていただければ幸いです.
オイラー積とか聞いたことないし, 私数学全然わからないのという方でも, あくまで今回の焦点は関数プログラミングなので, オイラー積の証明とかそういう数学の要素はできる限り排除しましたので, そういう方でも大丈夫だと思います.
何より僕自身が数学全然できないので, そんな証明の説明とか無理です.
あくまで Ruby の関数プログラミングの有用性を共有したいなと思ったので, その有用性をお伝えする上で, 便宜的にオイラー積の公式を使うとよりわかりやすくお伝えすることができると思ったので, そのオイラー積を採用したというまでのことです.
あと Ruby って関数プログラミングもできるんだよと教えてくれたきっかけは The Ruby Programming Language という本からです.
その本は Ruby の作者まつもとゆきひろさんと David Flanagan さんによるものです.
実際後で紹介させてもらうメソッド apply
, reduce
, compose
, apply_head
, apply_tail
はほとんどその本からの抜粋になります.
なのでより Ruby の関数プログラミングを知りたいという方はそちらの本を参照していただければと思います.
オイラー積を普通に計算してみる
まず Ruby の関数プログラミングを使って計算する前に, 普通に計算するとどういう風になるのかというところから始めたいと思います.
オイラー積の公式は次のようになり:
複素数 s
は 1
より大きい必要があるので 2
とし, n
は 1
, 2
, 3
という自然数列で, p
は 2
, 3
, 5
という素数になります.
あ, いきなり数学用語使ってしまいましたが, 僕自身あまりわかっていないので心配しないでください.
複素数は自然数の 1
, 2
, 3
とかもありえるっていうことと, 素数は 1
と自身の数のみでしか割り切れないということくらいしか知りませので.
左辺は無限和で右辺は無限積ということですが, 流石に無限に計算することは不可能ですのでそれぞれ左辺の n
は 1
から 1000
までの回数足していって, 右辺の p
は最初の素数 2
から始まって 1000
番目の素数までの積としたいと思います.
そのぐらい繰り返し計算したら, 左辺と右辺の値はほぼ同じになるだろうということで, 本当に公式通り同じ値になるのかという検証でもあります.
(僕はもう同じになるって知ってるんですけどね…)
ただ皆さんはまだ計算結果を見られていないので, 本当に同じになるのかなとほんの少しでもいいですので好奇心を持っていただけると幸いです.
でそうような条件で Ruby で普通に計算しようとすると次のようになります:
require 'prime'
n = 1_000
s = 2
numbers = 1..n
primes = Prime.first n
l = numbers.map { |x| x**s }.map { |x| 1.0 / x }.reduce { |x, y| x + y }
r = primes.map { |x| x**-s }.map { |x| 1 - x }.map { |x| 1.0 / x }.reduce { |x, y| x * y }
puts <<EOF
Calculatting Euler Product #{n} times with `s' is #{s}
The value on the left side is #{l}.
The value on the right side is #{r}.
Each of the values rounded to 2 decimal places is #{l.round 2} and #{l.round 2} respectively.
Thus the two values are almsot same.
EOF
計算結果:
Calculatting Euler Product 1000 times with `s' is 2
The value on the left side is 1.6439345666815615.
The value on the right side is 1.644913174706335.
Each of the values rounded to 2 decimal places is 1.64 and 1.64 respectively.
Thus the two values are almsot same.
結果は左辺の計算結果 l
と右辺の計算結果 r
をそれぞれ小数点第二位で四捨五入すると 1.64
という同じ値になりましたので, 公式のとおり同じになるんだなと検証することができました.
もちろん n
の値をより大きくしてもらえれば, より l
と r
の値が同じ値に近づきます. また複素数 s
の値も 1
より大きければ 2
以外にしたとしても l
と r
の計算結果は 2
の時の同じように大体同じになりますので, よろしければ色々な値を入れて試されて見ていただければと思います.
と Ruby でこの手の計算しようとすると, 普通はこのように map
や reduce
メソッドを使ってやる感じになると思います.
このように計算してもいいというか普通はこのように計算すると思いますが, 今回の本題である関数プログラミングを使って同じ計算をしようとするとどうなるのかを紹介させていただきたいと思います.
オイラー積を関数プログラミングで計算してみる
今回の本題に入らせていただくことができます.
そもそも関数プログラミングとはその名の通り関数メインで計算するプログラミングスタイルです.
Ruby は OOP (Object Oriented Programming) ですけれども, 関数メインでプログラミングしようと思えば, Lisp や Haskell のようにはいかないそうですが, できてしまうのです.
なので, 前節で計算したオイラー積を今度は関数プログラミングで計算するために, まず次のようなメソッドを定義したいと思います:
module Functional
def apply(enum)
enum.map &self
end
alias | apply
def reduce(enum)
enum.inject &self
end
alias <= reduce
def compose(f)
if self.respond_to?(:arity) && self.arity == 1
lambda { |*args| self[ f[ *args ] ] }
else
lambda { |*args| self[ *f[ *args ] ] }
end
end
alias * compose
def apply_head(*first)
lambda { |*rest| self[ *first + rest ] }
end
alias >> apply_head
def apply_tail(*last)
lambda { |*rest| self[ *rest + last ] }
end
alias << apply_tail
end
class Proc; include Functional; end
いきなりよくわからないコードで面食らってしまわれた方すみません.
実際このコードは, 最初の方でも軽く書きましたが, The Ruby Programming Language の 6. Methods, Procs, Lambdas, and Closures の Functional Programming という章からほとんど抜粋しており, ほんの少しだけ変更しております.
一つ一つ説明していきますので, 最終的には理解していただけると思いますので安心していただければと思います.
このコードが, 今回の Ruby の関数プログラミングの主な内容となりますので, このコードさえ理解していただければ今回の内容を理解していただけるということになります.
実際そんなに難しくありませんので.
まず Functional
というモジュールを定義しているわけですが, 別にこの名前はなんでもいいのです. ポイントは Functional
を Proc
クラスに include
で取り組んでいるということです.
そのことによって Functional
モジュール内に定義されたメソッドが, Proc クラスのインスタンスメソッドとして使用することができるようになります.
Proc クラスのインスタンスオブジェクトは proc
や lambda
で作ることができますが, つまりはその proc
や lambda
で作ったオブジェクトのインスタンスメソッドとして apply
, reduce
, compose
といったものを使うことができるようになります.
つまりは今回の関数プログラミングって Proc オブジェクトメインで計算していくということになります.
Proc オブジェクトを活用するために apply
, reduce
, compose
といったメソッドを定義しているのです.
5 つのメソッドの説明
それぞれ 5 つのメソッドを詳しく説明させていただきたいと思います.
apply
apply
はそれぞれの要素になんらかの変更を加えるメソッドです.
次の例で apply
の使い方を紹介したいと思います:
upcase = lambda { |x| x.upcase }
arr = ['a', 'b', 'c']
upcase.apply arr # => ["A", "B", "C"]
どうしてこのようなことができるのかというのは, まず upcase
が lambda
で作られた Proc オブジェクトであり, Proc オブジェクトなので先ほどの include Functional
によって読み込まれた Functional
モジュール内のメソッドを呼び出すことができます .
なので upcase.apply
と apply
を呼び出せているわけです.
upcase
はブロック引数 x
に与えられたオブジェクトから upcase
メソッドを呼び出してその与えられたオブジェクトを大文字にしたコピーを返します.
upcase
単体で使うと次のようなことができます:
upcase.call 'hello world' # => "HELLO WORLD"
そして apply
メソッドは何をするのかというと, 定義を見ると次のようになっていますが:
def apply(enum)
enum.map &self
end
まず引数として enum
を受け取ります. 先ほどのコードの場合 arr
という変数が渡されていますが, その arr
が enum
となります. で enum.map
となっているので, その arr
という Array を書き換えるコピーを返すわけですが, どんな風に書き換えるかというのは &self
次第というわけです.
&self
の self
というのは apply
メソッドが呼び出されているオブジェクト自体を指しているので, 先ほどのコードの場合は upcase
という Proc オブジェクトになります. そして &
はなんなのかということですが, これは Proc オブジェクトをブロックとして渡すための演算子になります. 内部的には to_proc
メソッドが呼び出されています.
つまりはその upcase
という Proc オブジェクトがブロックとして enum.map
に渡されているのです.
なので次のように考えてもらえるとわかりやすいと思います:
arr.map { |x| x.upcase }
実際には upcase
は lambda
なので proc
ではないので, 全く上記のコードと同じというわけではないのですが, 大体同じと考えられていいと思います.
なので upcase.apply arr
の返り値が ["A", "B", "C"]
となったわけです.
ちなみに alias | apply
と定義しているので apply
のエイリアスとして |
を使うことができるので, 先ほどの例は次のように書くこともできます:
upcase | arr # => ["A", "B", "C"]
こんな感じで Lambda を使って Ruby で関数プログラミングをすることができます.
もちろん Proc やメソッドでも同じことはできますが, 今回は Lambda で紹介させていただきたいと思います.
reduce
reduce
は複数の要素を一つにまとめるためのメソッドです.
reduce
も apply
と同じように使うことができます:
concat = lambda { |x, y| x + y }
arr = ['a', 'b', 'c']
concat.reduce arr # => "abc"
reduce
は次のように定義されていますので:
def reduce(enum)
enum.inject &self
end
そのコードは次のようなものと考えられていいと思います:
arr.inject { |x, y| x + y }
なので x
に最初の arr
の要素 'a'
が渡されて, y
に 'b'
と 'c'
がそれぞれ渡されて x
に付け加えられていったというわけです.
また reduce
も <=
というエイリアスが定義されているので, そのコードは次のように書くこともできます:
concat <= arr # => "abc"
compose
compose
は比較的シンプルな 2 つの Lambda を組み合わせて, より複雑な Lambda を生み出すことができます.
関数プログラミングでは, そのような操作を Function Composition と言います. .
同じように具体例から紹介させてもらうと, compose
を使うと次のようなことができます:
add_b = lambda { |x| x + 'b' }
add_c = lambda { |x| x + 'c' }
add_bc = add_c.compose add_b
add_bc.call 'a' # => "abc"
add_b
と add_c
というそれぞれ 'b'
と 'c'
を追加するシンプルな Lambda を compose
によって組み合わせ, add_bc
というその 2 つの Lambda の機能を合わせ持った新たな Lambda を作っています.
なので add_bc.call 'a'
で呼び出した時に, 'b'
と 'c'
が 'a'
に順々に結合されて "abc"
が返されました.
compose
のエイリアスとして *
が定義されているので, 次のようにも書けますね:
add_bc = add_c * add_b
add_bc.call 'a' # => "abc"
どうしてこのように 2 つの Lambda を組み合わせることができるのかというのは, まず compose
の定義をみていただきたいと思います:
def compose(f)
if self.respond_to?(:arity) && self.arity == 1
lambda { |*args| self[ f[ *args ] ] }
else
lambda { |*args| self[ *f[ *args ] ] }
end
end
少し見た目がややこしそうですが, やってることはシンプルです.
まず compose
は Function (関数) の f
を引数として取ります. 先ほどの場合は Lambda の add_b
になりますね.
if self.respond_to?(:arity) && self.arity == 1
というのは, compose
は add_c
で呼び出されるわけですので, self
は add_c
になります.
そして respond_to?(:arity)
で arity
というメソッドを呼び出すことができるかという判定をしています. arity
というのは渡す引数の数を返すメソッドで Proc オブジェクトである Lambda は当然 arity
というメソッドを持っています.
そして self.arity
で 1
の場合と, それ以外の場合とで条件分岐させています.
つまりはこの場合, add_c.arity
が 1
の場合とそうでない場合ということになり, add_c
は 1 つの引数を受け取るため add_c.arity
は 1
となり, 次の行が返されます:
lambda { |*args| self[ f[ *args ] ] }
lambda
とあるように, 新たな Lambda を返しているのですが, どんなにたくさんの引数を渡されてもいいようにパラメータを *args
としています. そしてその任意の数の引数をそのまま f
の引数として展開して f
を呼び出すようにし, その f
の返り値, つまりは add_b
の返り値を self
つまりは add_c
の第一引数として渡すようにしています.
f
の返り値を self
の第一引数として渡すようにしているというところがポイントで , どうして self
が第一引数しか受け取らないということがわかるのかというのは, if self.respond_to?(:arity) && self.arity == 1
の行で self
は 1 つの引数しか受け取らないとあらかじめ確認しているからです.
もし self
が 1
以外の場合, 複数の引数を受け取るか, もしくは一つも引数を受け取らないということになるので, 次の行を返しています:
lambda { |*args| self[ *f[ *args ] ] }
f
ではなく *f
というのがポイントで f
の返り値が Array の場合はそれぞれの要素を展開して self
のそれぞれの引数として渡せるようにしています.
どうしてそのように展開するのかというのは, self
が 1
以外の数の引数を受け取るということを確認済みだからです.
このように self
が受け取る引数の数に応じて, f
の返り値を展開するかしないか柔軟に決めています.
また self[ *f[ *args ] ]
とあるように, 一番最初に呼び出されるのは f
でその次に self
となるので, 先ほどの例の場合は, まず 'a'
という引数を add_b
に渡し , add_b
を呼び出してからその返り値 "ab"
を add_c
に渡し最終的な返り値が "abc"
となります.
なので compose
で複数の Lambda を組み合わせると, 右から左にかけてそれぞれの Lambda が呼び出されていくことになります.
apply_head
apply_head
はある Lambda の最初の方の引数をあらかじめ渡しておいてその Lambda の最初の方の引数を固定した新しい Lambda を返します.
関数プログラミングでは Partial Application と言われます.
汎用性のある関数をベースに, より特定の機能に特化させた関数を作るという場合に使われます.
apply_head
を使う例になります:
concat = lambda { |x, y| x + y }
add_hello = concat.apply_head 'Hello '
add_hello.call 'World' # => "Hello World"
このように concat
は 2 つの引数を受け取る Lambda ですが concat.apply_head 'Hello '
によって concat
の x
のパラメータにあらかじめ Hello
という値を渡しておいた add_hello
という Lambda を作っています.
なので add_hello.call 'World'
とした時に concat
の y
のパラメータに World
が渡されて, あらかじめ渡されていた x
の 'Hello '
と結合し "Hello World"
が返されました.
apply_head
の定義をみていただくと:
def apply_head(*first)
lambda { |*rest| self[ *first + rest ] }
end
任意の数の引数 *first
を受け取り, その first
と新しく返す Lambda が受け取る引数 *rest
の rest
を +
で連結させて self
の引数として渡す Lambda を返します.
first
と rest
は共に Array で +
で結合され一つの Array なので, それぞれの要素を展開するために self
に渡す際は *
を頭につけています.
こうすることによって concat
の x
というブロックパラメータにあらかじめ 'Hello'
が渡された add_hello
という Lambda を作ることができます.
このように Partial Application を使うときの元になる Lambda は実質的に受け取る引数の数が 2 つ以上である必要がありますね.
1 つの引数しか受け取らない Lambda に Partial Application しても, ただの Lambda 呼び出しと大差がありません.
apply_tail
apply_tail
も apply_head
と同じように Partial Application なのですが, apply_head
が最初の方の引数を埋めるというのに対して apply_tail
は最後の方の引数を埋めます:
concat = lambda { |x, y| x + y }
add_world = concat.apply_tail ' World'
add_world.call 'Hello' # => "Hello World"
なので concat.apply_tail ' World'
で concat
の y
パラメータをあらかじめ埋めているということになります.
そういうわけで 'Hello'
の後に ' World'
が結合されました.
apply_tail
の定義をみていただくと:
def apply_tail(*last)
lambda { |*rest| self[ *rest + last ] }
end
受け取る引数 *last
の last
が rest
の後に結合されて self
に渡されるようになってします.
apply_head
の場合と逆ですね.
このように +
で結合する時の順序を入れ替えることによって, 最初の方の引数を埋めるか, 最後の方の引数を埋めるか決めているわけです.
オイラー積をそれら 5 つのメソッドを使って計算する
apply
, reduce
, compose
, apply_head
, apply_tail
とそれぞれのインスタンスメソッドを説明させていただきましたが, 実際にそれらのメソッドを使って, いわゆる関数プログラミングでオイラー積を計算してみたいと思います:
require 'prime'
n = 1_000
s = 2
numbers = 1..n
primes = Prime.first n
sum = lambda { |x, y| x + y }
difference = lambda { |x, y| x - y }
product = lambda { |x, y| x * y }
quotient = lambda { |x, y| x / y }
power = lambda { |x, y| x**y }
e = power << s
negative_e = power << -s
one_minus = difference >> 1
one_nth = quotient >> 1.0
l = sum <= one_nth * e | numbers
r = product <= one_nth * one_minus * negative_e | primes
puts <<EOF
Calculatting Euler Product #{n} times with `s' is #{s}
The value on the left side is #{l}.
The value on the right side is #{r}.
Each of the values rounded to 2 decimal places is #{l.round 2} and #{l.round 2} respectively.
Thus the two values are almsot same.
EOF
計算結果:
Calculatting Euler Product 1000 times with `s' is 2
The value on the left side is 1.6439345666815615.
The value on the right side is 1.644913174706335.
Each of the values rounded to 2 decimal places is 1.64 and 1.64 respectively.
Thus the two values are almsot same.
いかがでしょうか.
いかがでしょうかと言われても, はぁという感じになってしまうかもしれないですが, 結構個人的には Ruby の関数プログラミングやばいって感じで軽い感動すらしています.
n
, s
, numbers
, primes
まではオイラー積を普通に計算してみた時のコードと同じですが, その下からが Lambda を使った関数プログラミングの実践になります.
まず sum
, difference
, product
, quotient
, power
と 2 つの引数 x
, y
の和, 差, 積, 商, 冪乗を求める Lambda をそれぞれ定義しています.
そして e
という power
の第二引数に s
あらかじめ渡しておいた Lambda を power
の Partial Application として <<
を使って定義しています.
negative_e
も e
と同じように power
の Partial Applying として <<
で power
の第二引数に -s
をあらかじめ渡しておいた Lambda を定義しました.
one_minus
と one_nth
も Partial Application ですが <<
ではなく >>
を使用して, difference
と quotient
それぞれの第一引数に 1
と 1.0
をあらかじめ渡したおいた Lambda を定義しています.
そうやって元の Lambda 5 つに加えて, 元の Lambda を基に新しく 4 つ Lambda を作り, 計 9 つの Lambda を Functional
モジュールで定義したメソッドと組み合わせて l
と r
を計算しています.
圧巻なのは l
と r
の計算部分です:
l = sum <= one_nth * e | numbers
r = product <= one_nth * one_minus * negative_e | primes
Lambda の演算子 |
, <=
, *
, >>
, <<
を定義したり, 9 つの Lambda を定義してあげると, それらを組み合わせることによってこんなにも簡潔に書くことができてしまうのです.
可読性という観点からは少し疑問が残るかもしれないですが, ある意味洗練されているのではないでしょうか.
普通に書いた場合と比べてみると, その簡潔さ, 洗練さが際立ちます:
# Using standard method
l = numbers.map { |x| x**s }.map { |x| 1.0 / x }.reduce { |x, y| x + y }
r = primes.map { |x| x**-s }.map { |x| 1 - x }.map { |x| 1.0 / x }.reduce { |x, y| x * y }
# Using functional programming
l = sum <= one_nth * e | numbers
r = product <= one_nth * one_minus * negative_e | primes
このように一つ一つはシンプルな Lambda でも色々組み合わせることによって複雑な計算もやってのけてしまうというのが, おそらく関数プログラミングの肝なのでしょうね.
演算子の Precedence と Associativity
ただその洗練極まりないコードですが, どうしてそのようにちゃんと計算できるのかというのは実は Ruby の演算子の Precedence や Associativity というものが絡んでいます.
先ほどの l
と r
のコードは, 計算する際に演算子として |
, <=
, *
を使用しているわけなのですが, それぞれの演算子には優先順位 (Precedence) というものがあり , さらに右か左のどちらから解釈するか (Associativity) というものも決まっています:
Operator | Associativity |
---|---|
* | Left |
| | Left |
<= | Left |
Precedence は高い順になっていますので, この 3 つの中で一番優先順位が高い演算子は *
になります.
Associativity は 3 つとも全て Left なので left-associative となり演算子の左の式から評価されて, 右の式が評価されるという順序になります.
そういうわけで l
と r
それぞれ, どのような順序で式が評価されるのかということを表すために丸括弧を使用すると次のように書き表すことができます:
l = sum <= ((one_nth * e) | numbers)
r = product <= (((one_nth * one_minus) * negative_e) | primes)
このように暗黙的に解釈されるので, ちゃんと計算できるわけなのです.
Ruby の算術演算子で **
が一番最初に計算されて, その次に *
, /
, %
が計算されて, 最後に +
, -
が計算されるということと同じ要領ですね.
まとめ
Ruby で関数プログラミングをやってみたいという興味のある方は, ぜひ今回の内容を参考にしていただけたらと思います.
もっと Ruby の関数プログラミングについて知りたいという方は, 本記事が参考にさせていただいた The Ruby Programming Language を参照していただければと思います.
あと注意点としましては, その The Ruby Programming Language によりますと, Ruby で関数プログラミングをする際に |
, <=
, *
, >>
, <<
などの演算子を再定義してしまうのは, 他の人たちにとって読みづらく, 保守しにくいコードになってしまう可能性が高いということで, 他の人たちと一緒に書くコードでは使用しないほうがいいのでしょう.
それにしても Ruby ってやろうと思えば演算子すらも自分の手で再定義できてしまうという柔軟さがすごいです.
Ruby の関数プログラミングの洗練さと Ruby の深遠さを垣間見た内容でした.
関連記事
Ruby の正規表現を備忘録としてまとめてみた2018.08.30
Ruby のブロック, Proc, Lambda の違い2018.06.21
Ruby のグローバル変数をそれぞれ調べてみた2018.06.12
Ruby の StandardError でどういう間違いをすると, どの例外が発生するのかのメモ2018.07.13
Ruby のパーセントリテラルの書き方2018.06.25