課題16の回答を書き換えて、できるだけ簡潔に書いてみましょう。
今回は純粋にリファクタリングをしてください。特に振る舞いを変える必要はありません。
Haskellには、コードを極めて簡潔に書くテクニックが、たくさんあります。この課題では、そうしたテクニックのうち、特によく使われるものを紹介します。みなさんがHaskellを書くときに必ずしもここで学習したことを使う必要はないですが、既存のHaskell製コードの中でも高確率で使われているので、是非習得してください。
まずは、関数の部分適用という手法を知ることで、適度に関数の定義を書かずに済ますテクニックのほか、Haskellにおける関数->
の秘密も知りましょう。
まずは一例として、map
関数の型定義を思い出してください:
ghci> :t map
map :: (a -> b) -> [a] -> [b]
関数(a -> b)
とリスト[a]
という、二つの引数を受け取るようになっていますね。
ghci> map (\x -> x * 2) [3, 8, 9]
[6,16,18]
ではこれに一つ目の引数である、関数(a -> b)
のみを適用するとどうなるでしょうか:
ghci> map (\x -> x * 2)
<interactive>:5:1: error:
? No instance for (Show ([Integer] -> [Integer]))
arising from a use of ‘print’
(maybe you haven't applied a function to enough arguments?)
? In a stmt of an interactive GHCi command: print it
案の定エラーになりました。No instance for (Show ([Integer] -> [Integer]))
という行で始まるこのエラーは、「整数のリストを受け取って整数のリストを返す関数[Integer] -> [Integer]
は、Show
型クラスのインスタンスではない」という意味の型エラーです。GHCiは入力したHaskellの式を評価した後、結果をprint
関数で表示するので、評価した結果の値はShow
型クラスのインスタンスでなければならないのでした。
ここからが本題です。No instance for (Show ([Integer] -> [Integer]))
における、「整数のリストを受け取って整数のリストを返す関数[Integer] -> [Integer]
」という型の式とは、一体何のことでしょう?そう、入力したmap (\x -> x * 2)
のことです。map (\x -> x * 2)
は、[Integer] -> [Integer]
という型の値なのです!
毎度おなじみ:t
コマンドでもチェックしてみましょう:
ghci> :t map (\x -> x * 2)
map (\x -> x * 2) :: Num b => [b] -> [b]
map (\x -> x * 2)
の型は、Num b => [b] -> [b]
、すなわちNum
型クラスのインスタンスである型b
(数値型)の値のリストを受け取って、同じくb
のリストを返す関数だそうです。先ほど型エラーが発生した際は[Integer] -> [Integer]
と言っていたのに、食い違ってますね。これは課題hoge(7?)で解説した、Num
型クラスの「デフォルトの型」が採用された結果です。Num
型クラスにはデフォルトの型としてInteger
型(整数型)が設定されているので、GHCiが式を評価しようとしたけどNum
型クラスの型が決まらなかった、という場合はInteger
が使用されるのです。
いずれにしても、Integer
型クラスはNum
型クラスのインスタンスなので、map (\x -> x * 2)
に[Integer]
型、すなわち整数のリストを渡せば、[Integer]
、整数のリストが返ってくるはずです。試しに渡してみましょう!
ghci> doubles = map (\x -> x * 2)
ghci> doubles [3, 8, 9]
[6,16,18]
map (\x -> x * 2) [3, 8, 9]
と同じ結果が返りました!
なぜこのような振る舞いになるのでしょうか?その秘密は、Haskellにおける関数型の、ある特徴にあります。次のような、「a
という型の値とb
という型の値を受け取ってc
という型の値を返す関数を例にしましょう:
a -> b -> c
このように2つ(以上)の引数を受け取る関数型に、カッコを補うと、実はHaskellは次のように解釈していることがわかります:
a -> (b -> c)
上記のように、a -> (b -> c)
は、a
という型の値を一つ受け取って、(b -> c)
という型の関数を返す関数です。a
型の値を1つ受け取ることによって、
(b -> c)
という、引数を(残り)一つ受け取る関数になるのです。
Haskellには、厳密な意味では「一つの引数を受け取って一つの値を返す関数」しか存在しません。a -> b -> c
のように、2つ以上の引数を受け取るよう振る舞う関数は、「関数を返す関数」を組み合わせることによって表現されます。
注意a -> b -> c
のような関数について話す時は「この関数は二つの引数を受け取る」といった表現を普通に使用します。本入門のこれ以降でも用いる予定ですので「Haskellには一つの引数を受け取る関数しかないのだから~」などとはいちいち考えなくて結構です。
この特徴を利用すると、余計なラムダ抽象を書かずに済ませたり、引数の名前を考えずに済ませたりすることができます。
例えば「文字列のリストにおける、すべての文字を大文字にする」という処理を次↓のように書いていたとします:
ghci> import Data.Char
ghci> map (\s -> map toUpper s) ["hello", "world!"]
["HELLO","WORLD!"]
こちらの(\s -> map toUpper s)
は、「map
関数にtoUpper
関数を渡して作った関数」に引数s
を渡しているだけの関数なので、次↓のように書き換えることができます:
ghci> map (map toUpper) ["hello", "world!"]
["HELLO","WORLD!"]
このように、a -> b -> c
といった型の複数の引数を受け取る関数から、最初のいくつかの引数だけを適用した新しい関数を作ることを「部分適用」と言います。Haskellでは部分適用を利用することで、一つの関数から簡単に新しい関数を作ることができます。
ところで、ここで一つ思い出していただきたいことがあります。例えばmap
関数のように、これまで紹介した「関数を受け取る関数」はこんな型でした:
ghci> :t map
map :: (a -> b) -> [a] -> [b]
もしこの(a -> b) -> [a] -> [b]
の(a -> b)
からカッコを取り除いて、
map :: a -> b -> [a] -> [b]
という型にしたら、どのような意味になるのでしょうか?
実はa -> b -> [a] -> [b]
という型の関数にしたら「a
型の値、b
型の値、[a]
型の値という三つの引数を受け取り、[b]
型の値を返す関数」という意味になってしまいます。Haskellには厳密な意味では「一つの引数を受け取る関数」しかないのでそれに則って言い換えると「a
型の値を受け取って『b
型の値を受け取って《[a]
型の値を受け取って[b]
型の値を返す関数》』を返す関数」を返す関数」です。つまりカッコを補って書き換えると、こんな関数になっているものと解釈されます:
map :: a -> (b -> ([a] -> [b]))
関数を表す->
という型は、右結合(同じ優先順位の演算子を複数並べたとき、右辺に書かれた式が優先して結合される)なので、カッコを書くことで初めて「関数を受け取る関数」として宣言できるようになっています。カッコを書き忘れると上記のように思いの外引数の多い関数になってしまいますので、ご注意ください。
次のテクニックを学ぶ前に、これまで触れてこなかった、演算子の型定義をチェックする方法について紹介させてください。Haskellでは、map
やputStrLn
といった普通の関数に加えて、+
や&&
などの「演算子」も普通の関数と全く同じように扱えるようになっています。普通の関数がmap f [1, 2, 3]
といった具合に**引数を関数の後に書く(前置記法で記述する)**のに対して、*
や&&
は1 + 4
、True && False
といったように、**引数を関数の左右に書く(中置記法で記述する)**関数だと捉えてください。
普通の関数は前置記法で書き、演算子は中値記法で書く、という違いから、Haskellの構文における取り扱い、すなわちGHCがHaskellのソースコードを読んで解釈する時の扱いも異なります。そのため、例えば次のように:t
コマンドを直接演算子に使用しても、エラーになってしまいます:
ghci> :t &&
<interactive>:1:1: error: parse error on input ‘&&’
これを直すには&&
をカッコ()
で囲います:
ghci> :t (&&)
(&&) :: Bool -> Bool -> Bool
&&
演算子は左辺と右辺にBool
型の値をとってBool
型の値を返すので、Bool -> Bool -> Bool
、すなわちBool
型の値を二つ受け取ってBool
型の値を返す関数なのです。
これは、課題hoge(16?)で!
という演算子をimport
する際、
import Data.Map.Strict ((!))
と、カッコで囲っていたのと同じことです。!
という名前の演算子を普通の関数と同じようにimport
するには、カッコ()
で囲う必要があるのです。
以上から察せられるでしょうか?実はHaskellにおける演算子は、カッコ()
で囲うことによって、普通の関数 --- 厳密には関数を始めとする変数の名前である「識別子」 --- と同じように扱われるようになります。
なので、ここまで紹介した:t
やimport
に限らず、普通の識別子が書けるほとんどあらゆる場面において、カッコで囲った演算子を使用できます:
-- 例えば、普通の関数と同様前置関数(の識別子)として扱ったり、
ghci> (+) 7 4
11
-- そのまま変数の識別子として、値を代入したりできます!
ghci> (+) = "+ 演算子と同じ見た目だけど中にあるのは文字列"
-- もちろん、変数なので他の関数に渡すことも!
ghci> putStrLn (+)
+ 演算子と同じ見た目だけど中にあるのは文字列
更に、前節で紹介した「Haskellにおける関数はカリー化されている」という特徴を思い出してください。「2つ以上の引数を受け取るよう振る舞う関数は、「関数を返す関数」を組み合わせることによって表現」されるという事実は、演算子にも適用されます:
-- (+) という関数に 4 を渡すと、
ghci> plus4 = (+) 4
-- 4 を足す関数が返ってくる。
ghci> plus4 3
7
-- 4 + 3 と同じ
-- (-) という関数に 10 を渡すと、
ghci> subtractFrom10 = (-) 10
-- 10 から何かを引く関数が返ってくる。
ghci> subtractFrom10 4
6
-- 10 - 4 と同じ
この特徴を利用すると、次の(\x -> 100 * x)
ように、演算子を一度使うためだけにラムダ抽象を書く必要がなくなります:
-- ↓のように書いていたのを...
ghci> map (\x -> 100 * x) [1, 2, 3]
[100,200,300]
-- ↓のように書き換えることができる!
ghci> map ((*) 100) [1, 2, 3]
[100,200,300]
続いて紹介するのは、$
という演算子です。$
を使うことで、関数呼び出しの際使用するカッコを減らすことができます。
$
は演算子なので、先程紹介した通り:t
コマンドで型定義を見るにはカッコで囲って($)
と書いてください:
ghci> :t ($)
($) :: (a -> b) -> a -> b
第1引数(演算子なのでまたの名を右辺)には関数(a -> b)
を、第2引数にあたる左辺にはa
型の値を渡します。名前が同じ型引数は必ず同じ型を表す、というルールがあるので、右辺の関数(a -> b)
における引数a
と、左辺の引数a
は同じ型となります。そして(a -> b)
型の値とa
型の値を受け取ると、$
は結果としてb
型の値を返します。このb
型は右辺の関数(a -> b)
におけるb
、つまり戻り値の型と同じです。まとめると、$
は関数を一つと、その関数の引数を受け取り、その関数が返す結果の型と同じ型の値を返します。
文章での説明だと分かりづらいので、実際に使ってみましょう。左辺に関数と、右辺にその関数の引数を渡せば使えるんでしたね。であれば例えば次のように使えるはずです:
ghci> words $ "abc def"
["abc","def"]
ghci> putStrLn $ "Hello, world!"
Hello, world!
ghci> show $ [1, 2, 3]
"[1,2,3]"
三つ例を挙げてみました。特にエラーにならず、ちゃんと使えているようですね。では実行した結果はいかがでしょうか...?
実は上記の場合、$
と書いても書かなくても何も変わりません!
ghci> words "abc def"
["abc","def"]
ghci> putStrLn "Hello, world!"
Hello, world!
ghci> show [1, 2, 3]
"[1,2,3]"
$
という演算子は、左辺に渡した関数に、右辺に渡した引数を渡して実行し、その結果を返す、ただそれだけの演算子なのです。一体それが何の役に立つのでしょうか?それを知るためには、GHCiの:i
というコマンドを使うのが分かりやすいです。
※おことわり: 「結合の優先順位」については後ほど説明します。
GHCiの:i
コマンド(:info
の略)は、指定した識別子(変数などの名前)の型だけでなく、識別子が表すものの種類に応じて、便利な情報を色々教えてくれます。例えば件の$
といった演算子の場合、演算子の結合の優先順位まで教えてくれます:
ghci> :i ($)
($) :: (a -> b) -> a -> b -- Defined in ‘GHC.Base’
infixr 0 $
最後の行にあるinfixr 0 $
という箇所が、演算子の優先順位を表しています。infixr
というキーワードが、$
演算子が右結合の演算子であることを示します(左結合の場合はinfixl
と書かれます)。infixr
に続いて書かれている0
という数字が、結合の優先順位を示しています。0
から9
までの数字を書けるようになっていまして、数字が大きければ大きいほど、結合の優先順位が高くなります。つまり、infixr 0
である$
演算子は「右結合の演算子で、最も結合の優先順位が低い」と定義されています。
他の演算子とも比べてみましょう。
ghci> :i (+)
-- ... 省略 ...
infixl 6 +
ghci> :i (-)
-- ... 省略 ...
infixl 6 -
ghci> :i (*)
-- ... 省略 ...
infixl 7 *
ghci> :i (/)
-- ... 省略 ...
infixl 7 /
足し算(+)
引き算(-)
よりかけ算(*)
割り算(/)
の方が優先順位が高く、左結合になっていることがわかります。いずれにしてもinfixr 0 $
よりは結合の優先順位が高いようですね。
さて、ここまで「演算子の結合の優先順位」という言葉を何の説明もなく使用しました。他の多くのプログラミング言語にもある概念ではありますが、普段あまり意識しないかと思いますので、ここで説明しておきましょう。
「演算子の結合の優先順位」は、例えば次のような、演算子を複数使った式をイメージすると説明しやすいです:
1 + 2 * 3
よく知られている数学の慣習上、かけ算は足し算より先に計算するので、2 * 3
の結果6
を1
に足して7
と計算するのが一般的でしょう。Haskellにも同様の解釈をしてもらうためには、+
演算子よりも*
演算子の結合の優先順位を高くして、1 + 2
よりも先に2 * 3
を処理してもらわなければなりません。このように、演算子とその引数を複数並べたとき、どのような順番で処理するかを定めたルールが「演算子の結合の優先順位」です。
先ほど:i
で調べたとおり、Haskellでは足し算の演算子+
や引き算の演算子-
よりも、かけ算の演算子*
や割り算の演算子/
の方が優先順位が高いことになっています。ちゃんと数学の慣習に従っていますね。
それから、「右結合」と「左結合」という用語にも触れておきましょう。これまで調べた四則演算の演算子はいずれも「左結合」でした。というわけで恐らくより身近であろう「左結合」から説明します。
「左結合」の演算子は、同じ演算子(より正確には、同じ優先順位の演算子)を複数並べたとき、左側の演算子を先に計算するよう定められています。典型的には、引き算を複数使った次の式を思い浮かべてみると分かりやすいでしょう:
10 - 5 - 4
みなさんはこのような式を計算するときは、必ず次のように左側の10 - 5
から順に計算するでしょう:
10 - 5 - 4
↓
5 - 4
↓
1
これを仮に右側の5 - 4
から計算した場合、結果が意図しないものになってしまいます:
10 - 5 - 4
↓
5 - 4
↓
10 - 1
↓
9
これでは困るので、四則演算の演算子(+
・-
・*
・/
)は、同じ(優先順位の)ものを複数並べた場合、左側のものから先に計算するよう定められています。これが「左結合」です。
Haskellにはこれまで挙げた四則演算の演算子(+
・-
・*
・/
)以外にもたくさんの演算子があり、そのいずれも結合の優先順位や、右結合か左結合かが定められています。$
もその一つで、最も結合の優先順位が低い、右結合の演算子として設定されていることは、先ほど説明しました。一体どうしてこのように設定されているのでしょうか?
それは、関数呼び出しの結合を他の演算子より弱めることで、他の演算子と関数を併せて使った際に、他の演算子を優先して結合させることで、カッコで囲う必要を減らせることです。
例えば、次のように+
演算子で計算した結果をprint
関数に渡すとします:
ghci> print (1 + 2 + 3)
6
こちらのコードでは、1 + 2 + 3
を必ずカッコで囲わなければなりません。次のようにカッコを省略すると、型エラーになってしまいます:
ghci> :{
ghci| print 1 + 2 + 3
ghci| :}
<interactive>:2:13: error:
? No instance for (Num (IO ())) arising from a use of ‘+’
? In the expression: print 1 + 2 + 3
In an equation for ‘it’: it = print 1 + 2 + 3
このエラーは、IO ()
という型の値は(Num
型クラスのインスタンスではないので)+
演算子が利用できない、という意味です。IO ()
という型はまさしくprint 1
の結果の型です。すなわちprint 1 + 2 + 3
という式は、1 + 2 + 3
よりも前にprint 1
を計算する式だと解釈されたのです。
このように解釈されるのは、Haskellでは関数呼び出しが最も強く結合するように定められているからです。print 1
と書いたら、隣に+ 3
があろうと* 4
があろうとその他どんな中値記法の演算子があろうと、関数呼び出しであるprint 1
が最優先されます1。
一方$
演算子は、普通の関数呼び出しは愚か、その他のほとんどの演算子より弱く結合するよう設定されているので、利用すれば次のようにカッコを省略して書くことができます:
ghci> print $ 1 + 2 + 3
6
その他にも、次👇のように++
演算子で文字列を結合した結果をputStrLn
関数に渡す場合:
ghci> putStrLn ("aaa" ++ show 111)
aaa111
これも$
演算子を使ってカッコを省略できます:
ghci> putStrLn $ "aaa" ++ show 111
aaa111
課題4で書いていた次の式👇も、
ghci> unlines (reverse (lines "aaa\nbbb\nccc"))
"ccc\nbbb\naaa\n"
このように$
を使ってカッコをゼロにできます:
ghci> unlines $ reverse $ lines "aaa\nbbb\nccc"
"ccc\nbbb\naaa\n"
要するに「$
よりも後ろはカッコが自動で補われる」と解釈すると良いでしょう。
ただし、👇のように$
よりも結合の優先順位が高い演算子が左側にある場合はうまく行きません:
ghci> "aaa" ++ show $ 1 + 110
<interactive>:9:1: error:
? Couldn't match expected type ‘Integer -> t’
with actual type ‘[Char]’
? The first argument of ($) takes one argument,
but its type ‘[Char]’ has none
In the expression: "aaa" ++ show $ 1 + 110
In an equation for ‘it’: it = "aaa" ++ show $ 1 + 110
? Relevant bindings include it :: t (bound at <interactive>:9:1)
<interactive>:9:10: error:
? Couldn't match expected type ‘[Char]’
with actual type ‘() -> String’
? Probable cause: ‘show’ is applied to too few arguments
In the second argument of ‘(++)’, namely ‘show’
In the expression: "aaa" ++ show
In the expression: "aaa" ++ show $ 1 + 110
これは、$
の左辺に"aaa" ++ show
を、右辺に1 * 110
を渡した、と解釈されたことによる型エラーです。$
よりも++
の方が結合の優先順位が高いので、"aaa" ++ show
を先に計算する、と解釈されたのです。
分かりやすくするためにカッコを補うと、次のとおりです:
("aaa" ++ show) $ (1 + 110)
このように$
よりも結合の優先順位が高い演算子が左側にある場合は、$
を使ってカッコを省略することはできません。このような場合は、$
を使わずにカッコを書く方がお勧めです:
ghci> "aaa" ++ show (1 + 110)
"aaa111"
しかし、「$
より左側に優先順位の高い他の演算子がある場合は$
が上手く機能しない」ということを理解していても、前述のようなミスをしてしまうことはよくあります。そんなときは、私がこれまでの経験で得た、次のようなコツを思い出してみるといいかもしれません:
- 演算子の結合の優先順位を間違えたケースでは、先ほどのように型エラーが波及するので、複数の型エラーが同じ箇所で発生することが多い
- よくわからない型エラーが同じ箇所で複数でてきた場合は、演算子の優先順位を間違えたことを疑うといいかもしれない
expected type
かactual type
のどちらか一方が関数になっているけど、もう片方が関数じゃない時(あるいは、引数の数がそれぞれで異なる)場合も疑うといいかも知れない- これ以外にも同じような型エラーになることはあるが、いずれにしても、単純な間違いを一つ犯しているだけである場合が多いので、型エラーがたくさん出ても慌てないことが大事
今度は、前節で紹介した関数適用演算子$
と同じくらいよく使用される、関数合成演算子.
を紹介します。何はともあれ:t
コマンドで型定義を見てみましょう:
ghci> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
.
は(b -> c)
・(a -> b)
という型の、二つの関数を両辺に受け取ります。ところがそれだけでなく、右辺の関数の引数と同じ型a
の値まで受け取って初めて、左辺の関数の結果と同じc
型の値を返すようです。...おかしいですね、演算子は左辺と右辺にしか値を受け取らないので引数は二つまでのはずでは...?
この謎の正体は、.
を「関数を返す関数」として解釈することで明らかになります。すなわち、.
は(b -> c)
・(a -> b)
という型の二つの関数を受け取った結果、a -> c
という型の関数を返す演算子と捉えてみてください。a -> c
を.
が返す一つの関数としてカッコを補うと、.
の型は次のように表されます:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
これで、.
が「関数を返す関数」であることが分かりました。では、どのような関数を返すのでしょうか?実際に使って確かめてみましょう。簡単な例として、show
関数とlength
関数を.
に渡してみます:
ghci> :t length . words
length . words :: String -> Int
String -> Int
、すなわち文字列を受け取って整数を返す関数ができました。試しに使ってみましょう:
ghci> length . words "aaa bbb ccc"
<interactive>:7:10: error:
? Couldn't match expected type: a -> t0 a0
with actual type: [String]
? Possible cause: ‘words’ is applied to too many arguments
In the second argument of ‘(.)’, namely ‘words "aaa bbb ccc"’
In the expression: length . words "aaa bbb ccc"
In an equation for ‘it’: it = length . words "aaa bbb ccc"
? Relevant bindings include
it :: a -> Int (bound at <interactive>:7:1)
... おっと、型エラーになってしまいました。$
演算子の節で触れた通り、Haskellでは関数がどんな演算子よりも優先して結合されるので、length . words "aaa bbb ccc"
という式はwords "aaa bbb ccc"
を最優先して計算してしまいます。words "aaa bbb ccc"
は当然関数を返さないので、.
で合成することができません!
従って、length . words
をカッコで囲ってあげましょう:
ghci> (length . words) "aaa bbb ccc"
3
できました。3
という結果ですね。これは、"aaa bbb ccc"
という文字列をwords
関数で単語のリストに分割し、その結果をlength
関数に渡してリストの長さを数えた結果です。すなわち、.
によってできた(length . words)
という関数は、引数をwords
関数に渡してその結果をlength
関数に渡す、words
関数とlength
関数を繋いだ関数なのです。
一般化すると、.
は両辺に関数を受け取り、新しい関数を作ります。この新しい関数は、右辺に渡した関数の引数を右辺の関数に渡し、右辺の関数の結果を左辺の関数に渡します。そして左辺の関数の結果を返す --- という流れで振る舞います。.
は、左辺の関数と右辺の関数を繋ぐ演算子なのです。関数を「合成する」演算子だとも言われます。
.
演算子を利用すると、二つの関数を使ったf (g x)
のような形の式を、f . g
という形に書き換えることができます。(g x)
にあったカッコやx
という変数がなくなって、スッキリしますね。
これを応用すれば、例えば次のようにmap
関数に渡す関数を作る際、ラムダ抽象やその引数s
を書かずに済みます:
map (\s -> show (length s)) ["aaa", "bb", "c"]
-- ^^^^^^^^^^^^^^^^^^^^^^^
-- この部分のラムダ抽象を...
map (show . length) ["aaa", "bb", "c"]
-- ^^^^^^^^^^^^^^^
-- `.`を使って短くできました!
あるいは、関数定義を書く際にも利用できます。次のケースは、前節で紹介した$
演算子を使って定義した関数です:
reverseLines input = unlines $ reverse $ lines input
既にカッコを省略できていて簡潔ですが、実は.
を使えばもっと簡潔に書くことができます:
reverseLines = unlines . reverse . lines
引数input
まで消えてより短くなりました!このように.
は、一番右の変数を削除するのによく用いられます。
先ほどの「:t
で演算子の型定義も調べる・演算子を普通の関数のように前置記法で記述する」の節では、演算子の記号をカッコで囲うことで、普通の前置関数と同じように使えることを紹介しました。
ghci> -- 3 * 4 と同じ
ghci> (*) 3 4
12
ghci> -- 10 - 2 と同じ
ghci> (-) 10 2
8
これと、「Haskellにおける関数はカリー化されている」という事実を組み合わせることができるのも、説明したとおりです:
-- 2倍する関数を作る
ghci> double = (*) 2
ghci> double 10
20
足し算+
やかけ算*
の演算子ではこれで十分なのですが、割り算/
などのように左辺と右辺を逆にすると計算結果が変わる演算子では不都合な場合があります:
-- 「1000で割る関数」を作るつもりが...
ghci> dividesBy = (/) 1000
-- 出来上がったのは、「1000 *を*引数に渡した値で割る関数」
ghci> dividesBy 100
10.0
もちろん、次のように引数の順番を入れ替えた関数を作れば、期待した関数が出来上がります:
ghci> dividesBy l = l / 1000
ghci> dividesBy 100
0.1
実はこれをもっと簡単に作れるようにするのが、ここで紹介する「演算子のセクション」という機能です。例えば、問題の「1000 で割る関数」は「演算子のセクション」を使えば次のように書くことができます:
ghci> dividesBy = (/ 1000)
ghci> dividesBy 100
0.1
先ほどのdividesBy
と同じように振る舞ってますね!
「演算子のセクション」は、次のように演算子とそのどちらかの引数をまとめてカッコで囲うことで作ることができます:
(/ 1000)
これは、割り算の演算子/
の右辺に1000
を渡して作った関数です。左辺の値は引数として渡されるので、先ほど示したとおり「1000
で割る関数」が出来上がります。
もちろん、左辺に引数を渡して右辺に演算子を渡すこともできます:
(1000 /)
これは逆に渡した数で1000
を割る関数ですね。
「演算子のセクション」を活用すれば「演算子を前置関数に変換する」で紹介したこちら↓の例も...
ghci> map ((+) 4) [1, 2, 3]
[5,6,7]
次のように、より少ないカッコで書き換えることができます:
ghci> map (+ 4) [1, 2, 3]
[5,6,7]
足し算は右辺と左辺を入れ替えても結果が変わらないので、次のように書いてもOKです:
ghci> map (4 +) [1, 2, 3]
[5,6,7]
ただし、演算子のセクションは、引き算の演算子-
に対して利用する場合、意図通りに動かない場合があります。例えば(- 4)
と入力して、(- 4)
という、-
をカッコで囲った「-
演算子のセクション」を作ってみます:
ghci> (- 4)
-4
GHCiは-4
と出力しました。どうやら(- 4)
は、演算子のセクションでできるであろう「4引く関数」ではなく、単なる数値の-4
として解釈されてしまうようです。:t
コマンドで型を調べてみると、そのことがもっとはっきり分かります:
ghci> :t (- 4)
(- 4) :: Num a => a
Num a => a
という型は、「Num
型クラスのインスタンスである型のうちのいずれか」です。つまり(- 4)
は、整数など何らかの数値を表す型の値なんですね。そう、やはり(- 4)
は単なる-4
という数値型の値であり、演算子のセクションによって作ることができる「左辺の値を受け取って4引く関数」ではないのです。
これは、我々が-
を利用する際の習慣に従うために定められた、-
に対する特別扱いです。ちなみに-
と4
の間にスペースを入れようと入れまいと、結果は変わりません:
ghci> (-4)
-4
ghci> :t (-4)
(-4) :: Num a => a
ghci> (- 4)
-4
ghci> :t (- 4)
(- 4) :: Num a => a
一方、-
の左辺に4
を書いた場合、問題なく「-
演算子のセクション」が作れます。「4引く関数」ではなく「4から受け取った値を引く関数」が出来上がります:
ghci> :t (4 -)
(4 -) :: Num a => a -> a
:t
コマンドの結果として出てきたNum a => a -> a
という型は、「Num
型クラスのインスタンスである型、a
型の値を引数に取り、引数と同じa
型の値を返す関数」です。先ほどのNum a => a
が単なるa
型の値だったのから、a -> a
という関数に変わりました。
もちろん、実際に関数としても使うこともできます:
ghci> (4 -) 4
0
復習を兼ねて、ここまでで紹介した種々のテクニックを活用してみましょう。文字列を処理する関数をいくつか書いて、それを短くしてみます。
まずは、map
関数を使い、文字列のリストの各要素に「挨拶する」、すなわち"Hello, "
という文字列を先頭に付け加える関数を書いてみましょう:
greetEach xs = map (\x -> ("Hello, " ++ x)) xs
第一に、「Haskellにおける関数はカリー化されている」、つまり二つ以上の引数を受け取る関数に一つ引数を渡すと、残りの引数を受け取る関数が返ってくる、という性質を用いれば、greetEach
の引数xs
を省略できます:
greetEach = map (\x -> ("Hello, " ++ x))
一般に、関数の最後の引数が、関数の本体である関数呼び出しの最後の引数として一度だけ使用されている場合、その引数は省略することができます。
より単純な例に置き換えると、
f x y z = g x y z
上記のf
関数の引数、z
はg
の最後の引数として一度だけ使われているので、
f x y = g x y
と書き換えることができます。
そしてこの規則を繰り返し適用すると、y
、x
の順で省略できるようになると分かるでしょう。つまり最終的には次のように変換できます:
f = g
シンプルになりました!
ちなみに、このように引数を省略する変換を、「eta reduction」と言います。
もっと短くしてみましょう。今度は演算子のセクションを使って、map
関数に渡す関数の式(\x -> ("Hello, " ++ x))
を簡潔にします。演算子のセクションを使って、次のように「先頭に"Hello, "
という文字列を付け加える関数」を作ることで、引数x
を省略することができます:
-- これを...
(\x -> ("Hello, " ++ x))
-- こう書き換える
("Hello, " ++)
新しく短くできた関数をmap
関数に渡してみましょう:
greetEach = map ("Hello, " ++)
下記の最初のバージョンよりかなり短くできました!
greetEach xs = map (\x -> ("Hello, " ++ x)) xs
-- v.s.
greetEach = map ("Hello, " ++)
以上のような、greetEach
関数から引数xs
を取り除いたり、map
関数に渡す関数(\x -> ("Hello, " ++ x))
から引数x
を取り除いたりするように、引数に名前を付けずに関数を組み立てる手法を、「ポイントフリースタイル」と言います。
もう少し複雑な例を見てみましょう。先ほどのgreetEach
関数、リストに入った各文字列に挨拶をするのはいいのですが、ちょっと元気が足りませんね。挨拶だけでは寂しいので、末尾に「!
」を付けてみましょう。ポイントフリースタイルを使わずに書いた場合、次のとおりです:
greetEach xs = map (\x -> ("Hello, " ++ x ++ "!")) xs
試しに実行してみると、確かに元気に挨拶していますね!
ghci> greetEach ["Tom", "Bob", "Alice"]
["Hello, Tom!","Hello, Bob!","Hello, Alice!"]
この関数をポイントフリースタイルで書いてみましょう(どうしてこうなるかは、自力で考えてみてください!)。
greetEach = map (("Hello, " ++) . (++ "!"))
引数xs
もx
もなくなり、短くなりました!...が、++) . (++
の辺り、ちょっと暗号っぽくもなってきてますね。この時点で読みづらい、という方もいらっしゃるかも知れません。
ポイントフリースタイルは、やればやるほど短く簡潔に書ける書き方画ですが、その分読みづらくなることもあります。更に極端な例として「ポイントフリースタイルへの道 〜最大公約数編〜 - Qiita」という記事があります。これまでの課題で紹介していない機能も利用していますが、参考までにどうぞ。 大事なことは、短く書ける人を上級者と呼ぶなということです。
これまで、パターンマッチを行う際はcase
式を使うよう説明してきました:
-- 課題11の`doubles`関数、数値のリストを受け取って
-- すべての要素を2倍にする関数の再掲
doubles xs =
case xs of
[] -> []
x : xsLeft -> x * 2 : doubles xsLeft
doubles
関数では、引数であるxs
に対し、直接case
式でパターンマッチしています。このようなパターンはしばしばあるので、Haskellでは次のように、関数の宣言時に直接パターンマッチする方法も提供してくれています。
その機能を使うと例えばdoubles
関数は、引数のxs
という名前を用意する間もなく定義できます:
doubles [] = []
doubles (x : xsLeft) = x * 2 : doubles xsLeft
随分形が変わってしまいましたが、先ほどのdoubles
関数とこちらのdoubles
関数は、全く同じように振る舞います。どこがどのように変わったのか、一つずつ見てみましょう。
->
の代わりに=
でパターンとその定義を区切っている点と、(x : xsLeft)
のように、パターン全体をカッコで囲う必要がある点にご注意ください。
カッコで囲う必要があるのは、case
式と異なり関数定義では複数の引数を扱うことがあるためです。例えばdoubles
関数に引数を増やして、2以外の、引数で指定した任意の数を掛けられる関数、times
関数を定義してみたとします。関数定義で引数を直接パターンマッチしたい場合、次のようにx : xsLeft
をカッコで囲わなければなりません:
times n [] = []
times n (x : xsLeft) = x * n : times n xsLeft
times n (x : xsLeft) = ...
の(x : xsLeft)
からカッコを外してしまうと、次のようにParse error
になってしまいます:
ghci> :{
ghci| times n [] = []
ghci| times n x : xsLeft = x * n : times n xsLeft
ghci| :}
<interactive>:47:1: error: Parse error in pattern: times
times n x : xsLeft = ...
と書いた場合、あたかも(n x) : xsLeft
という一塊でパターンマッチングしているかのように見えるなど(実際にはそのようなパターンマッチングはできませんが)、うまく解釈できない構文になってしまうからです。書き換えの際はご注意ください。
それからおまけに、関数の引数でのパターンマッチを利用した、もっとかっこいい例を紹介しましょう。Haskellについて紹介する際によく用いられる例なので、よそでも見たことがあるかも知れません。引数として整数を受け取って、その整数番目のフィボナッチ数を計算する関数です。まずは普通のcase
式を利用した書き方から:
fib n =
case n of
0 -> 0
1 -> 1
_ -> fib (n - 2) + fib (n - 1)
引数n
に対してcase
式でパターンマッチングすることで、n
が0
や1
であればそのまま0
や1
を返し、それ以外の数であれば(フィボナッチ数の定義通り)二つ前のフィボナッチ数と一つ前のフィボナッチ数を足して返します。
これを、case
式の代わりに関数の引数におけるパターンマッチで書き換えると...
fib 0 = 0
fib 1 = 1
fib n = fib (n - 2) + fib (n - 1)
このように、フィボナッチ数の漸化式を用いた定義とそっくりな見た目になります。かっこいい!
ただ、case
式を使ったバージョンであれ引数でのパターンマッチングを使ったバージョンであれ、いずれにしても効率はすごく悪いのでくれぐれも実際に使わないようご注意ください。Haskellでフィボナッチ数を計算するより効率の良い方法は検索すればたくさんヒットするでしょう。
今度は、レコード型の値をパターンマッチする際に使える構文を紹介します。
例として用いるレコード型は、お馴染みEntry
型です:
data Entry =
Entry { category :: String, price :: Integer }
deriving Show
レコード型の値に対するパターンマッチは、二つ方法があります。まずはレコード型のラベルを宣言した順に従って、各フィールドの値を変数に代入する方法です。Entry
型に対してこの方法を用いた場合、次のような構文で引数を宣言します:
f (Entry cat pri) = {- ...ここで category が入った cat を、と price が入った pri を使う式 ... -}
上記の例は、値コンストラクターEntry
の第一引数にあたるcategory
をcat
という変数に、第二引数にあたるprice
をpri
という変数に、それぞれパターンマッチで代入する例です。このように書くことで関数の本体、{- ...ここで category が入った cat を、と price が入った pri を使う式 ... -}
の箇所でcat
とpri
を使えるようになります。
実際にEntry
型の値を受け取って、そのcategory
とprice
にパターンマッチする関数を書いてみましょう:
ghci> :{
ghci| formatEntry :: Entry -> String
ghci| formatEntry (Entry cat pri) = cat ++ ": " ++ show pri
ghci| :}
ghci> entry = Entry "Magazine" 120
ghci> formatEntry entry
"Magazine: 120"
Entry
型の値を受け取って、そのcategory
とprice
を使って文字列を作るformatEntry
関数を定義しました。formatEntry
関数の引数を宣言する部分で(Entry cat pri)
と書いてEntry
型の値にパターンマッチしています。こうするとcategory
がcat
に、price
がpri
という変数に割り当てられるので、formatEntry
の本体ではcat ++ ": " ++ show pri
という式で利用しています。
パターンマッチはレコード型の中の各フィールドに対してもできるので、例えば、category
が空文字列のものを特別扱いしたくなったら、次のように値コンストラクターEntry
の第一引数の箇所で、空文字列""
にマッチさせます:
ghci> :{
ghci| formatEntry :: Entry -> String
ghci| formatEntry (Entry "" pri) = "<Unknown category>" ++ ": " ++ show pri
ghci| formatEntry (Entry cat pri) = cat ++ ": " ++ show pri
ghci| :}
ghci> entry = Entry "" 999999999999999
ghci> formatEntry entry
"<Unknown category>: 999999999999999"
Entry
とその引数の箇所をカッコで囲うのを忘れないでください!
ghci> :{
ghci| formatEntry :: Entry -> String
ghci| formatEntry Entry cat pri = cat ++ ": " ++ show pri
ghci| :}
<interactive>:89:13: error:
? The constructor ‘Entry’ should have 2 arguments, but has been given none
? In the pattern: Entry
In an equation for ‘formatEntry’:
formatEntry Entry cat pri = cat ++ ": " ++ show pri
The equation(s) for ‘formatEntry’ have three arguments,
but its type ‘Entry -> String’ has only one
レコード型の値に対してパターンマッチをするもう一つの方法は、レコードラベルを使った方法です。「どのレコードラベルの値をどの変数に割り当てるか」を明示するので、フィールドの順番に寄らずにマッチできます。
例として、先程のformatEntry
関数をレコードラベルを使ったパターンマッチに書き換えてみましょう:
ghci> :{
ghci| formatEntry :: Entry -> String
ghci| formatEntry (Entry { category = "", price = pri }) = "<Unknown category>" ++ ": " ++ show pri
ghci| formatEntry (Entry { category = cat, price = pri }) = cat ++ ": " ++ show pri
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- この部分がレコードラベルを使ったパターンマッチ
ghci| :}
パターンマッチしている部分のみ切り出してみます:
Entry { category = "", price = pri }
Entry { category = cat, price = pri }
最初に紹介したパターンマッチ((Entry "" pri)
や(Entry cat pri)
)と異なり、
- まず値コンストラクター
Entry
の後ろに、ブレースで囲ってパターンマッチするレコードラベルや変数を列挙しています - そして、レコードラベルと変数(あるいはマッチさせたい値)の間に
=
を書いて結びつけます
このように書くことで、先程と同様にcategory
とprice
の値をそれぞれcat
とpri
に割り当てたりするformatEntry
を定義できます。
レコードラベルを使ったパターンマッチの大きなアドバンテージは、フィールドの順番に依存しないことです。従って、formatEntry
関数は次のようにcategory
とprice
をマッチさせる順番を入れ替えても同じように振る舞います:
ghci> :{
ghci| formatEntry :: Entry -> String
ghci| formatEntry (Entry { price = pri, category = "" }) = "<Unknown category>" ++ ": " ++ show pri
ghci| formatEntry (Entry { price = pri, category = cat }) = cat ++ ": " ++ show pri
ghci| :}
hoge
便利なサンプルがPrelude
モジュールにいくつかある。
以下はソースコードからの抜粋:
fst :: (a, b) -> a
fst (x, _) = x
snd :: (a, b) -> b
snd (_, y) = y
課題8で紹介したとおり、これらはPrelude
モジュールに入っているので、何もimport
しなくても使える
おまけ: curry
とuncurry
curry
は「(サイズ2の)タプルを受け取る関数」を(Haskellの世界で普通の、カリー化された)2引数関数に変換する。
curry :: ((a, b) -> c) -> a -> b -> c
curry f x y = f (x, y)
uncurry
はその逆。ここでタプルに対するパターンマッチが出てくる。
uncurry :: (a -> b -> c) -> ((a, b) -> c)
uncurry f (x, y) = f x y
おそらくuncurry
の方が実践ではよく使う
典型的な使用例: Map
方の値をtoList
関数で変換した結果を、そのまま2引数の関数に渡す
(+)
をuncurry
関数でタプルを受け取る関数に変換することで、toList
結果の値をそのまま使える。
ghci> import qualified Data.Map.Strict as M
ghci> numberAndChars = M.fromList [(1, 3), (2, 4)]
ghci> map (uncurry (+)) $ M.toList numberAndChars
[4,6]
ラムダ抽象で書き換えると↓と同じ
ghci> map (\pair -> fst pair + snd pair) $ M.toList numberAndChars
[4,6]
引数でのパターンマッチは、ラムダ抽象でも使える。
つまり、
tupleToEntry :: (String, Integer) -> Entry
tupleToEntry (cat, pri) = Entry cat pri
↑と↓は同じ!
tupleToEntry :: (String, Integer) -> Entry
tupleToEntry = \(cat, pri) -> Entry cat pri
課題8で↓のように書いていたのと実質同じようなもの。
-- ...
let (divResult, modResult) = divMod numerator denominator
-- ...
パターンマッチに関する話をまとめると
- 原則1: 値コンストラクターで値を組み立てるのと逆のことをするのがパターンマッチ。
- リストには
[x, y]
という構文の、特別な値コンストラクターがある。本来はx : y : []
。
- リストには
- 原則2: 関数の引数を含めた、変数への代入を行うあらゆる場面でパターンマッチは使える。
でも、これは危ない!
ghci> dangerous = \(Just x) -> x
ラムダ抽象は
dangerous (Just x) = x
dangerous Nothing = "error!"
みたいに、関数定義の構文のように引数でパターンマッチしても場合分けができないので、
dangerous = \(Just x) -> x
みたいに書きたくなったら、↓のようなcase
に変換するしかない(LambdaCase
についてはいつか脚注に)。
dangerous = \mx ->
case mx of
Just x -> ...
Nothing -> ...
結果、うっかりNothing
が渡されてエラーになってしまうことも
ghci> dangerous Nothing
*** Exception: <interactive>:8:2-15: Non-exhaustive patterns in lambda
警告を有効にしても教えてくれない😱
ghci> :set -Wall
ghci> dangerous = (\(Just x) -> x)
-Wincomplete-uni-patterns
を有効にしないといけない!
ghci> :set -Wincomplete-uni-patterns
ghci> dangerous = \(Just x) -> x
<interactive>:10:2: warning: [-Wincomplete-uni-patterns]
Pattern match(es) are non-exhaustive
In a lambda abstraction: Patterns not matched: Nothing
GHCの将来のバージョンで、-Wincomplete-uni-patterns
が-Wall
に含まれるかも。
-Wall
を有効にしても有効にならない警告の一覧はGHC Users Guideを参照。
https://functor.tokyo/blog/2017-07-28-ghc-warnings-you-should-enable
shell> cabal install hlint
# あるいは...
shell> stack install hlint
ここで紹介していないTupleSections
というGHCの拡張を使ったものも(GHCの拡張の話を後回しにしたいので敢えて紹介していません... あしからず)。
shell> hlint assets/16.hs
assets/16.hs:18:14: Warning: Avoid lambda
Found:
\ x y -> x + y
Perhaps:
(+)
assets/16.hs:19:19: Suggestion: Use tuple-section
Found:
\ w -> (w, 1)
Perhaps:
(, 1)
Note: may require `{-# LANGUAGE TupleSections #-}` adding to the top of the file
2 hints
HLintのさらに便利な使い方は素晴らしき HLint を使いこなすを。
「この関数はこのモジュール以外では使わないでください」などというプロジェクト固有のルールを設定することもできます。
Footnotes
-
ユーザーが定義できる演算子ではない、Haskellの構文として定義されている演算子については例外があります。 ↩