私は疑問に思う:
1) 以下の機能はまったく同じです。
inc = (+1)
double = (*2)
func1 = double . inc
func2 x = double $ inc x
func3 x = double (inc x)
func4 = \x -> double (inc x)
func5
2)コンパイルできないのはなぜですか?
func5 = double $ inc -- doesn't work
これらの機能はまったく同じですか?
実は違う!いくつかの非常に微妙な違いがあります。まず第一に、恐ろしい単型性の制限について読んでください。要するに、クラス多相関数は、それらが「明らかに」関数であるかどうかにかかわらず、デフォルトで異なる型が与えられます。inc
あなたのコードでは、とdouble
は「明らかに」関数ではなく、単相型が与えられているため、この違いは現れません。しかし、わずかな変更を加えると、次のようになります。
inc, double :: Num a => a -> a
inc = (+1)
double = (*2)
func1 = double . inc
func2 x = double $ inc x
func3 x = double (inc x)
func4 = \x -> double (inc x)
次に、ghci では、「明らかに」関数ではないfunc1
andに単相型が与えられていることがわかります。func4
*Main> :t func1
func1 :: Integer -> Integer
*Main> :t func4
func4 :: Integer -> Integer
一方func2
、 とfunc3
には多相型が与えられます。
*Main> :t func2
func2 :: Num a => a -> a
*Main> :t func3
func3 :: Num a => a -> a
2 番目のわずかな違いは、これらの実装の評価動作が (非常にわずかに) 異なる可能性があることです。(.)
と($)
は関数であるため、実行する前に and を呼び出すとfunc1
少しfunc2
評価が必要になる場合があります。たとえば、おそらく最初の呼び出しは次のfunc1 3
ようになります。
func1 3
= {- definition of func1 -}
(double . inc) 3
= {- definition of (.) -}
(\f g x -> f (g x)) double inc 3
= {- beta reduction -}
(\g x -> double (g x)) inc 3
= {- beta reduction -}
(\x -> double (inc x)) 3
一方、eg の最初の呼び出しは、func4 3
はるかに簡単な方法でこの点に到達します。
func3 3
= {- definition of func3 -}
(\x -> double (inc x)) 3
ただし、これについてはあまり心配する必要はありません。最適化がオンになっているGHCでは、両方の呼び出しが飽和し、(.)
インライン($)
化され、この可能な違いが排除されると思います。そうでない場合でも、実際には非常に小さなコストになります。これは、定義ごとに 1 回だけ発生する可能性が高いためです (呼び出しごとに 1 回ではありません)。
なぜ
func5
コンパイルされないのですか?
コンパイルしたくないからです!想像してみてください。を評価する方法を見てみましょうfunc5 3
。「行き詰まる」ことがわかります。
func5 3
= {- definition of func5 -}
(double $ inc) 3
= {- definition of ($) -}
(\f x -> f x) double inc 3
= {- beta reduction -}
(\x -> double x) inc 3
= {- beta reduction -}
double inc 3
= {- definition of double -}
(\x -> x*2) inc 3
= {- beta reduction -}
(inc * 2) 3
= {- definition of inc -}
((\x -> x+1) * 2) 3
今、関数を 2 倍しようとしています。現時点では、関数の乗算がどうあるべきか (または、この場合は "2" がどうあるべきか) については述べていません。それは良いことではありません!私たちは、そのような複雑な用語に「行き詰まり」たくはありません。実際の数値や関数などの単純な用語だけに行き詰まりたいのです。
double
掛け算できるものだけを操作する方法を知っており、掛け算できるものではないことを最初に観察することで、この混乱全体を防ぐことがinc
できたはずです。型システムはそのようなことを行います。型システムはそのような観察を行い、何か奇抜なことが起こることが明らかな場合はコンパイルを拒否します。
最初の 4 つの関数は同じです。
に申し込もうとしていdouble
ますinc
。inc
乗算できないため、これは機能しません。
double $ inc
-- is the same as
double inc
型仕様を追加すると、次のように表示されます。
inc :: Integer -> Integer
double :: Integer -> Integer
double
を取りInteger
ますが、渡そうとしていますInteger -> Integer
。
Haskell ではトップレベル関数の型を明示的に述べるのが良い習慣であることに注意してください。これらは関数とプログラムについて多くのことを伝えることが多いからです。