Haskellの$
と.
。
f $ g x = f (g x)
(f . g) x = f (g x)
どちらも同じに見えるんですけど。
Haskellを学びはじめてから、結構長い間これが分からなかったのでまとめ。
結論
func x = f (g (h x))
は$
,.
を使うとOKのようには書けるがNGのようには書けない。
$ | . | ||
---|---|---|---|
func x = f $ g $ h $ x |
OK | func x = f . g . h $ x |
OK |
func x = f $ g $ h x |
OK | func x = f . g . h x |
NG |
func = f $ g $ h |
NG | func = f . g . h |
OK |
Haskllerは綺麗なモノが大好きなので誰に教わることもなく、慣れるに従って、括弧よりも$
、$
よりも.
を使うようになる気がする。
具体例
たとえば、
- 1から10までの数字のうち
- 奇数のものに
- すべてに2を足したうえで
- 合計して
- 印刷する
main = do
let list = [1..10]
let odds = filter odd list
let plus = map (+2) odds
let result = sum plus
print result
括弧
括弧で書くと
print (sum (map (+2) (filter odd [1..10])))
($)
これを$
で書き換える。
左のカッコを$
で置き換え対応する右カッコを削る、またはひとつひとつの処理を$
というパイプでつなぐイメージ。
print $ sum $ map (+2) $ filter odd [1..10]
-- または
print $ sum $ map (+2) $ filter odd $ [1..10]
あえて二つ目を示した理由は次でわかる。
(.)
今度は関数合成.
を使う。$
版から書き換えるなら最後の$
以外は.
に置き換えることができる。
print . sum . map (+2) . filter odd $ [1..10]
また、最後の引数を落とすと、この式は型(Integral a, Show a) => [a] -> IO ()
の関数となるので
> f = print . sum . map (+2) . filter odd
> f [1..10]
35
もOK。
今回のケースではいわゆるポイントフリースタイルがもっともシンプルなので具体例の回答としてはこれがベストと思う。
($)のNGケース
ひとつ前の例で.
を$
に単純に置き換えると、
> f = print $ sum $ map (+2) $ filter odd
<interactive>:6:30: error:
? Couldn't match expected type ‘[Integer]’
with actual type ‘[Integer] -> [Integer]’
? Probable cause: ‘filter’ is applied to too few arguments
...............................
これはNG。
map (+2)
はInteger
のリストを期待しているのにfilter odd
は部分適用された関数[Integer]->[Integer]
なので型が合わない。
(.)のNGケース
逆に$
でOKのケースを.
に単純に置き換えた次のケースは
> print . sum . map (+2) . filter odd [1..10]
<interactive>:7:26: error:
? Couldn't match expected type ‘a -> [Integer]’
with actual type ‘[Integer]’
? Possible cause: ‘filter’ is applied to too many arguments
........................
やはりNG。これは
> print . sum . map (+2) . (filter odd [1..10])
こう書いたのと同じことになる。.
は関数を合成しようとして引数には前後とも関数を期待しているのに(filter
は自身の引数と先に結合してしまうため)第二引数がリストになってしまう
期待されるのは括弧で書くと
> (print . sum . map (+2) . filter odd) [1..10]
35
なので、$
を使って括弧を除くと
print . sum . map (+2) . filter odd $ [1..10]
こうなる。上で最後の$
以外はとしたのは、このような事情から。
**合成関数に直接値を喰わせるときは.
でも空白区切りでもなく$
.**これは大事。
補足
$
と.
の型と実装を比較。
($) :: (a -> b) -> a -> b
f $ x = f x
(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)
$
は一つだけでは見かけ上、何もしない。
しかし、f x
と書いた場合f
とx
は左結合で、どんな中置演算子より強く結びつくが、$
を使うことで右結合かつ優先順位最低(ゼロ)となることで、複数指定した場合に括弧の入れ子を単なる連結に変えてみせることができる。
一方、.
は関数を二つとって関数を返す。
まさに関数を合成しているわけだが、実装は第二引数の関数へ値を適用した結果を第一引数の関数に食わせるもの。
(関数合成というと何やら黒魔術めいたもので複数の関数の機能を一つに錬成するようなイメージを持っていました(笑)。そのため難しいものだと思って初めは敬遠していたのですが、実際は何のことは無いパイプラインです。小さく作ってじゃんじゃん繋ぎましょう)