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 の関数プログラミングを使って計算する前に, 普通に計算するとどういう風になるのかというところから始めたいと思います.

オイラー積の公式は次のようになり:

The euler product formula

複素数 s1 より大きい必要があるので 2 とし, n1, 2, 3 という自然数列で, p2, 3, 5 という素数になります.

あ, いきなり数学用語使ってしまいましたが, 僕自身あまりわかっていないので心配しないでください.

複素数は自然数の 1, 2, 3 とかもありえるっていうことと, 素数は 1 と自身の数のみでしか割り切れないということくらいしか知りませので.

左辺は無限和で右辺は無限積ということですが, 流石に無限に計算することは不可能ですのでそれぞれ左辺の n1 から 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 の値をより大きくしてもらえれば, より lr の値が同じ値に近づきます. また複素数 s の値も 1 より大きければ 2 以外にしたとしても lr の計算結果は 2 の時の同じように大体同じになりますので, よろしければ色々な値を入れて試されて見ていただければと思います.

と Ruby でこの手の計算しようとすると, 普通はこのように mapreduce メソッドを使ってやる感じになると思います.

このように計算してもいいというか普通はこのように計算すると思いますが, 今回の本題である関数プログラミングを使って同じ計算をしようとするとどうなるのかを紹介させていただきたいと思います.

オイラー積を関数プログラミングで計算してみる

今回の本題に入らせていただくことができます.

そもそも関数プログラミングとはその名の通り関数メインで計算するプログラミングスタイルです.

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 というモジュールを定義しているわけですが, 別にこの名前はなんでもいいのです. ポイントは FunctionalProc クラスに include で取り組んでいるということです.

そのことによって Functional モジュール内に定義されたメソッドが, Proc クラスのインスタンスメソッドとして使用することができるようになります.

Proc クラスのインスタンスオブジェクトは proclambda で作ることができますが, つまりはその proclambda で作ったオブジェクトのインスタンスメソッドとして 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"]

どうしてこのようなことができるのかというのは, まず upcaselambda で作られた Proc オブジェクトであり, Proc オブジェクトなので先ほどの include Functional によって読み込まれた Functional モジュール内のメソッドを呼び出すことができます .

なので upcase.applyapply を呼び出せているわけです.

upcase はブロック引数 x に与えられたオブジェクトから upcase メソッドを呼び出してその与えられたオブジェクトを大文字にしたコピーを返します.

upcase 単体で使うと次のようなことができます:

upcase.call 'hello world'  # => "HELLO WORLD"

そして apply メソッドは何をするのかというと, 定義を見ると次のようになっていますが:

  def apply(enum)
    enum.map &self
  end

まず引数として enum を受け取ります. 先ほどのコードの場合 arr という変数が渡されていますが, その arrenum となります. で enum.map となっているので, その arr という Array を書き換えるコピーを返すわけですが, どんな風に書き換えるかというのは &self 次第というわけです.

&selfself というのは apply メソッドが呼び出されているオブジェクト自体を指しているので, 先ほどのコードの場合は upcase という Proc オブジェクトになります. そして & はなんなのかということですが, これは Proc オブジェクトをブロックとして渡すための演算子になります. 内部的には to_proc メソッドが呼び出されています.

つまりはその upcase という Proc オブジェクトがブロックとして enum.map に渡されているのです.

なので次のように考えてもらえるとわかりやすいと思います:

arr.map { |x| x.upcase }

実際には upcaselambda なので proc ではないので, 全く上記のコードと同じというわけではないのですが, 大体同じと考えられていいと思います.

なので upcase.apply arr の返り値が ["A", "B", "C"] となったわけです.

ちなみに alias | apply と定義しているので apply のエイリアスとして | を使うことができるので, 先ほどの例は次のように書くこともできます:

upcase | arr  # => ["A", "B", "C"]

こんな感じで Lambda を使って Ruby で関数プログラミングをすることができます.

もちろん Proc やメソッドでも同じことはできますが, 今回は Lambda で紹介させていただきたいと思います.

reduce

reduce は複数の要素を一つにまとめるためのメソッドです.

reduceapply と同じように使うことができます:

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_badd_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 というのは, composeadd_c で呼び出されるわけですので, selfadd_c になります.

そして respond_to?(:arity)arity というメソッドを呼び出すことができるかという判定をしています. arity というのは渡す引数の数を返すメソッドで Proc オブジェクトである Lambda は当然 arity というメソッドを持っています.

そして self.arity1 の場合と, それ以外の場合とで条件分岐させています.

つまりはこの場合, add_c.arity1 の場合とそうでない場合ということになり, add_c は 1 つの引数を受け取るため add_c.arity1 となり, 次の行が返されます:

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 つの引数しか受け取らないとあらかじめ確認しているからです.

もし self1 以外の場合, 複数の引数を受け取るか, もしくは一つも引数を受け取らないということになるので, 次の行を返しています:

lambda { |*args| self[ *f[ *args ] ] }

f ではなく *f というのがポイントで f の返り値が Array の場合はそれぞれの要素を展開して self のそれぞれの引数として渡せるようにしています.

どうしてそのように展開するのかというのは, self1 以外の数の引数を受け取るということを確認済みだからです.

このように 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 ' によって concatx のパラメータにあらかじめ Hello という値を渡しておいた add_hello という Lambda を作っています.

なので add_hello.call 'World' とした時に concaty のパラメータに World が渡されて, あらかじめ渡されていた x'Hello ' と結合し "Hello World" が返されました.

apply_head の定義をみていただくと:

def apply_head(*first)
  lambda { |*rest| self[ *first + rest ] }
end

任意の数の引数 *first を受け取り, その first と新しく返す Lambda が受け取る引数 *restrest+ で連結させて self の引数として渡す Lambda を返します.

firstrest は共に Array で + で結合され一つの Array なので, それぞれの要素を展開するために self に渡す際は * を頭につけています.

こうすることによって concatx というブロックパラメータにあらかじめ 'Hello' が渡された add_hello という Lambda を作ることができます.

このように Partial Application を使うときの元になる Lambda は実質的に受け取る引数の数が 2 つ以上である必要がありますね.

1 つの引数しか受け取らない Lambda に Partial Application しても, ただの Lambda 呼び出しと大差がありません.

apply_tail

apply_tailapply_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'concaty パラメータをあらかじめ埋めているということになります.

そういうわけで 'Hello' の後に ' World' が結合されました.

apply_tail の定義をみていただくと:

def apply_tail(*last)
  lambda { |*rest| self[ *rest + last ] }
end

受け取る引数 *lastlastrest の後に結合されて 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_ee と同じように power の Partial Applying として <<power の第二引数に -s をあらかじめ渡しておいた Lambda を定義しました.

one_minusone_nth も Partial Application ですが << ではなく >> を使用して, differencequotient それぞれの第一引数に 11.0 をあらかじめ渡したおいた Lambda を定義しています.

そうやって元の Lambda 5 つに加えて, 元の Lambda を基に新しく 4 つ Lambda を作り, 計 9 つの Lambda を Functional モジュールで定義したメソッドと組み合わせて lr を計算しています.

圧巻なのは lr の計算部分です:

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 というものが絡んでいます.

先ほどの lr のコードは, 計算する際に演算子として |, <=, * を使用しているわけなのですが, それぞれの演算子には優先順位 (Precedence) というものがあり , さらに右か左のどちらから解釈するか (Associativity) というものも決まっています:

Operator Associativity
* Left
| Left
<= Left

Precedence は高い順になっていますので, この 3 つの中で一番優先順位が高い演算子は * になります.

Associativity は 3 つとも全て Left なので left-associative となり演算子の左の式から評価されて, 右の式が評価されるという順序になります.

そういうわけで lr それぞれ, どのような順序で式が評価されるのかということを表すために丸括弧を使用すると次のように書き表すことができます:

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 の深遠さを垣間見た内容でした.