この記事は、Haskell (その3) Advent Calendar 2017の8日目の記事です。
Haskellのコードでよく登場する$がどのような原理で働くのかを解説します。
カッコの略記で大活躍の$
Haskellでプログラムを組んでいると、カッコをネストする場面によく出くわします。
例えば、1〜10のうち、偶数のもののみの2乗の和(つまり、$2^2+4^2+6^2+8^2+10^2$ )を求めるプログラムを考えてみます。
main = print ( sum ( map (^2) ( filter even [1..10] ) ) )
おびただしい数のカッコですね…!ちょっとごちゃごちゃして読みにくい感じです。
ここで、我らが$が登場します。
Haskell超入門では、「$から行末までを括弧で囲むのと同じ効果があります」とありますので、その通りに書き換えてみます。
main = print $ sum $ map (^2) $ filter even [1..10]
カッコが減って見やすくなりましたね。
このように、読みやすいコードを書くには欠かせない$です。
ところで、一体どのような仕組みで$がカッコを省略するように働くのか、疑問に思ったことはありませんか?
ここでは、その仕組みを解き明かしてみましょう。
$の定義を見てみよう
実は、他の多くの演算子と同じく$も関数です。従って、どこかに($)の定義があるはずです。
まずは、($)が定義されているモジュールを調べるために、GHCiを使ってみましょう。
$ ghci
Configuring GHCi with the following packages:
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
Prelude> :i ($)
($) ::
forall (r :: GHC.Types.RuntimeRep) a (b :: TYPE r).
(a -> b) -> a -> b
-- Defined in ‘GHC.Base’
infixr 0 $
Prelude>
GHCiによれば、Defined in ‘GHC.Base’だそうですから、GHC.Baseモジュールを覗いてみます。
ghc/Base.hs at master - ghc/ghc
執筆時点では、1282行目からの部分に定義があります。
($) :: (a -> b) -> a -> b
f $ x = f x
まず、型を見ると、「(a -> b)型の関数」と「a型の値」を受取って、「b型の値」を返す関数、となっています。
さらに、定義を見ると、f $ x = f xとなっています。
つまり、関数とその引数を受け取っていますが、引数をそのまま受け取った関数に垂れ流しているように見えます。
こんなに単純でいいのでしょうか?具体例でちょっと考えてみましょう。
main = print $ sum [1..10]
先ほどの定義に従えば、このコードの$演算子を適用すると
main = print sum [1..10]
となる、ということになります。やはり、おかしい感じです。
これでは$があっても無くても何も変わらないような気がしてしまいます。
$の挙動を知るには、定義を見るだけでは不十分のようです。
決め手は優先順位
先ほどのコードの解釈に足りなかった概念、それはズバリ「優先順位」です。
Haskellの演算子には適用の優先順位が存在します。
以下がHaskellの演算子の優先順位を示す表です。
※演算子 - ウォークスルーHaskellの表をお借りしました。
| 優先順位 | 演算子 |
|---|---|
| (10) | その他のすべての関数適用 |
| 9 |
!! .
|
| 8 |
^ ^^ **
|
| 7 |
* / `div` `mod` `rem` `quot` |
| 6 |
+ -
|
| 5 |
: ++
|
| 4 |
== /= >= `elem` `notElem` |
| 3 | && |
| 2 | ‖ |
| 1 |
>> >>=
|
| 0 |
$ $! `seq` |
ご覧の通り、$演算子は優先順位が最も低く設定されています。
これが、$の挙動を知る鍵です。
関数適用を手作業で追ってみよう
では、先ほどのコードをもう一度見てみましょう。
main = print $ sum [1..10]
まずは、優先順位に従ってカッコをつけます。
「$の適用」の優先順位はあらゆるものより低いので、
main = (print) $ (sum [1..10])
となります。優先順位の働きによって、暗黙のうちにカッコがつけられていたのですね!
あとは、(print)関数に(sum [1..10])が渡されて、計算結果が出力される、という感じです。
複数の$がある時の挙動
$が複数あるときも同じようにして挙動を把握できるでしょうか。
最初の例を持ってきてみます。
main = print $ sum $ map (^2) $ filter even [1..10]
$の優先順位が一番低いことを利用して、カッコをつけてみます。
main (print) $ (sum) $ (map (^2)) $ (filter even [1..10] )
少し違和感がありますね。$を使わないコードでは
main = print ( sum ( map (^2) ( filter even [1..10] ) ) )
となっていたはずです。カッコが入れ子になってくれていないようです。
一体どういうことでしょうか。
$の最後の性質:結合性
実は、ここにはもう一つ欠けている概念があります。
それは、演算子の「結合性」です。
これは、複数の演算子が連なったときの、演算子の適用順を決める概念です。
簡単な例で考えてみましょう。
a = 1 + 2 + 3 + 4
このような式があったとき、どういう順番で計算していくでしょうか?
Haskellでは、このような計算の順番を決定していく方法が2つあります。
一つは「左結合」と呼ばれる方法で、左側の演算子から適用していきます。つまり、
a = 1 + 2 + 3 + 4
a = 3 + 3 + 4
a = 6 + 4
a = 10
--つまり、
a = ((1 + 2) + 3) + 4
とするやり方です。
もう一つは「右結合」で、さっきとは逆に右側から計算していきます。
a = 1 + 2 + 3 + 4
a = 1 + 2 + 7
a = 1 + 9
a = 10
--つまり、
a = 1 + (2 + (3 + 4))
ちなみに、$演算子は右結合となっています。
結合性を使ってもう一度
さて、結合性を使ってもう一度先ほどの式を解釈してみましょう。
main = print $ sum $ map (^2) $ filter even [1..10]
$の優先順位が一番低いことを利用して、カッコをつけてみます。
main (print) $ (sum) $ (map (^2)) $ (filter even [1..10] )
さらに、$は右結合でしたから、右側の$から適用されるようにカッコを付けてみます。
main (print) $ ( (sum) $ ( (map (^2)) $ (filter even [1..10] ) ) )
ちゃんとカッコがネストされましたね!
あとは$を適用すれば、
main (print) ( (sum) ( (map (^2)) (filter even [1..10] ) ) )
となって、ちゃんと$を使わない式と同じになっていることが分かります。
これで、$の仕組みを完全に解き明かすことができました。
※実は、優先順位と結合性も、先ほどのBase.hsで宣言されています。
執筆時点では145行目にあります。
infixr 0 $, $!
infixrは右結合を意味します。(infixlなら左結合です。)
その横の数字は優先度を表しています。
つまり、$はコンパイラが特別に用意してくれた記号というわけではなく、
Haskellの文法に従って定義された演算子だったのです。
その気になれば自前の$を作ることも可能です。(!)
仕組みとしてとても美しいと思いませんか?