Help us understand the problem. What is going on with this article?

Haskell - $の仕組みを覗いてみよう

More than 1 year has passed since last update.

この記事は、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行目からの部分に定義があります。

Base.hs
($)                     :: (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の文法に従って定義された演算子だったのです。
その気になれば自前の$を作ることも可能です。(!)

仕組みとしてとても美しいと思いませんか?

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away