Edited at
HaskellDay 2

Rubyistに贈るHaskell入門

More than 3 years have passed since last update.

Haskell分からない人が贈るHaskell入門

高階関数におけるRubyのブロックとHaskellの関数の違いに関してかなり雑でしたので、

"Rubyistに贈るHaskell入門"がちょっと微妙だったので補足記事その1

こちらの補足記事を併せて読むことをオススメします。


Rubyと一緒なら怖くない!

Haskellは関数型言語です。

「関数型」、この単語を聞いた時点で回れ右してオブジェクトの世界に向かって全力疾走したくなりますが、Rubyは関数型言語にも影響を受けている言語です。

ならばRubyで関数型の概念を理解することができるかもしれない!


関数型の特徴

関数型にはいくつかの特徴がありますが、今回は以下の特徴をRubyによって説明します。


  • 高階関数

  • カリー化

  • 関数の部分適用

  • 関数合成

これらの雰囲気をRubyでつかみ、Haskellへの第一歩としましょう。


高階関数

おめでとうございます。Rubyistの皆さんは既に高階関数の半分を理解していると言えるかもしれません。

高階関数とは「関数を引数にしたり、関数を返り値にしたりすること」です。前者はRubyのブロックに近いものです。

ブロックを使うメソッドであるmapを見てみましょう。


map.rb

[1, 2, 3].map do |i|

i * 2
end
#=> [2 ,4, 6]

そしてこちらがHaskellでの同等の処理です。


map.hs

map (\i -> i * 2) [1, 2, 3] -- [2, 4, 6]

-- Haskellではハイフン2つでコメントになります

Rubyの「Array メソッド ブロック」の順番に対してHaskellは「関数 ブロックっぽいもの リスト」の順番で変な感じですが、それぞれのパーツはとても似ています。

Rubyのdo |引数| 処理 endはそのままHaskellでは\引数 -> 処理という形になっていますね。(\i -> i * 2)のようなものはラムダ式と呼ばれていて、iを引数にしてi * 2を返す関数です。(Windows環境ではバックスラッシュが¥マークになります))

話は逸れますが、Haskellは引数を省略できることが多々あります。

Rubyでは一部のブロック内の処理が&を使って短く書けることがある感じと似ているかもしれません。

先ほどのmapはそれぞれ

Rubyでは


map.rb

[1, 2, 3].map(&2.method(:*)) #=> [2, 4, 6]


Haskellでは


map'.hs

map (* 2) [1, 2, 3]

-- [2, 4, 6]

とも書くことができます。

さて、高階関数の半分を知ったところで高階関数のもう半分、「関数を返り値にする」を考えてみます。

RubyにはProcという手続きオブジェクトがあります。


proc.rb

x = proc do |i|

i * 2
end

x.call(2) #=> 4


ブロック(do |引数| 処理 end)はそれ単体を変数に入れることはできませんが、Procオブジェクトとして作ることで変数に入れることができるようになります。

従って「関数を返り値とする関数」はRubyでは「Procを返すメソッド」と言うことができるでしょう。


return_proc.rb

def multi(x)

proc do |y|
x * y
end
end

multi(3).call(2) #=> 6


multiというメソッドは「引数xを受け取り「引数yを受け取ってx * yを返すProc」を返すメソッド」です。

言葉にするとややこしいですね・・・。

このmultiをHaskellで書いてみます。


multi.hs

multi x = (\y -> x * y)

(multi 3) 2 -- 6


Haskellの関数定義はずいぶんとサッパリしています。必要最小限のことしか書いていない感じがします。

関数名 引数 = 処理という形式です。

Haskellでは半角スペースで区切って並べることで関数に引数を渡すことができます。

ちょっとイジワルして (multi 3) 2 の括弧を外してみましょう。


spite.hs

multi x = (\y -> x * y)

multi 3 2 -- 6


なんてこったい!素知らぬ顔で正しい結果を返してきました!

multimulti 3の時点で、「yを受け取って3 * yを返す関数」になっているので問題なく動きます。Haskellの方が一枚上手だったようです。


カリー化

今度こそHaskellに困った顔をさせるために引数を一度に二つ取るmulti2という関数を作って、引数を1つつづ渡してみましょう。


multi2.hs

multi2 x y = x * y

(multi2 3) 2 -- 6


またまたHaskellは正しい値を返しました!2つの引数を取る関数なのに1つづつ引数を渡しても良いのでしょうか?

実はHaskellの関数は標準でカリー化されています。

カリー化とは「複数の引数を取る関数」を「引数を1つずつ受け取って、その度に1つ少ない引数を受け取る関数」にすることです。

Rubyにもカリー化のためのメソッド、Proc#curryがあります。

一度に引数を受け取る形のmulti2をRubyで書いてカリー化してみます。


curry.rb

multi2 =  proc |x, y|

x * y
end

multi2.call(2, 3) #=> 6

curry = multi2.curry

curry.call(3).call(2) #=> 6


一度に2つの引数を受け取るProcをカリー化することで1つずつ引数を受け取るProcになっていますね。

Haskellでは標準でカリー化が行われているので、引数を1つづつ受け取るように書いても、一度に複数の引数を受け取るように書いても同じです。

multi x = (\y -> x * y)

--同じ!

multi x y = x * y


部分適用

ここまで理解できた皆さんならば、部分適用を理解するのは簡単です。

だって部分適用というのはカリー化した関数に1つづつ引数を渡すだけなのですから。


multi2 x y = x * y

multiFive = multi2 5 -- multi2に5を部分適用

multiFive 10 -- 50
multiFive 20 -- 100

multiFivemulti2に5を部分適用したものです。

Haskellでは自動的にカリー化してくれるので、複数の引数を受け取るように見える関数には好きな時に好きな数、引数を渡すことができます。

部分適用をすることで、複数の引数を取る関数のうち、いくつかの関数を固定したまま色々なところで使うことができるようになったり、後述の関数合成ができるようになります。

めでたしめでたし。


関数合成

さて、ついに最後の関数合成です。

関数合成とは名前の通り「関数を合成すること」です。


$(g \circ f)(x) = g(f(x))$


学校の数学でこんな式を習った人がいるかもしれません。

f と g を合体させて、f で処理した後に g で処理する新しい関数にします。

プログラミングにおける関数合成はこれと全く同じです!

ただし、Haskellでは型システムがあるので、 a -> bb -> c のように片方の返り値ともう片方の引数の型が同じである必要があります。

まだピンときません。Rubyで関数合成をしてみましょう。

Rubyの標準ライブラリには関数合成のためのメソッドはないので自分で作ります。


compose.rb

class Proc

def compose(g)
proc {|x| g.call(self.call(x)) }
end
end

procの中で$g(f(x))$と同じことをするようにしています。

ちょっと使ってみましょう。


compose.rb

class Proc

def compose(g)
proc {|x| self.call(g.call(x)) }
end
end

multi_five = proc {|b| b * 5 }

plus_five = proc {|a| a + 5 }

composed = multi_five.compose(plus_five)

composed.call(10) #=> 75


(+ 5)をするProcと(* 5)するProcを合成して1つの新しいProcにしています。

Haskellは標準で関数合成のための関数が用意してあります。 . という関数です。


compose.hs

plusFive a = a + 5

multiFive b = b * 5

composed = multiFive . plusFive

composed 10 -- 75


良い感じです。


最後に

Rubyでブロックを当たり前のように使うのと同じように、Haskellでは高階関数、部分適用、関数合成を当たり前のように使います。

これらは処理を抽象化させるための強力な道具です。これらの概念を知っているとRubyでも恩恵を受けることができるかもしれません。(例えばlambda_driverとかね)

Haskellでは型クラス、モナド、遅延評価など、面白い特徴がいっぱいありますが、今回はここまでとさせて頂きます。

終わり!