Haskellにおける手動型推論

関数を考えます

f g h x y = g (g x) (h y)

その種類は何ですか?明らかに:t f を使って調べることができますが、これを手動で推測する必要がある場合、これを実行するための最良の方法は何ですか?

The method I have been shown is to assign types to parameters and deduce from there - e.g. x :: a, y :: b gives us that g :: a -> c and h :: b -> d for some c,d (from g x, h y) and then we keep on making deductions from there (c = a from g (g x) (h y) etc.).

しかし、これは時々巨大な混乱に変わるだけであり、私はそれ以上の控除をする方法や私が終わったときにうまくいく方法がよくわからないことがよくあります。他の問題が時々起こります - 例えば、この場合 x は関数であることがわかりますが、それは不正をして型を調べる前には私には明白ではありませんでした。

常に機能する(そして人間が素早く実行するのに合理的な)特別なアルゴリズムはありますか?それ以外の場合、私が見逃しているヒューリスティックやヒントはありますか?

1
Haskellが使うアルゴリズムは、 Algorithm W です。 en.wikipedia.org/wiki/…
追加された 著者 Willem Van Onsem,
Haskellが使うアルゴリズムは、 Algorithm W です。 en.wikipedia.org/wiki/…
追加された 著者 Willem Van Onsem,
しかし、それがなぜ混乱を引き起こすのか、ちょっと変だと思います。通常、パラメータに型を割り当て、後でより具体的にする、新しい型変数を導入する、または与えられた2つを削除することは基本的に同じであるなどの考え方が一般的です。
追加された 著者 Willem Van Onsem,
しかし、それがなぜ混乱を引き起こすのか、ちょっと変だと思います。通常、パラメータに型を割り当て、後でより具体的にする、新しい型変数を導入する、または与えられた2つを削除することは基本的に同じであるなどの考え方が一般的です。
追加された 著者 Willem Van Onsem,
@ A.モリス:あなたが答えとして何を期待しているのかよくわかりません。引用されたアルゴリズムはそれがあるべきほど複雑にされていません。かなり短い実装があります( github.com/wh5a /ステップバイステップ/ blob/master /… )。はい、それはいくらかの仕事を必要とします、典型的にはアルゴリズムが何人かの人間のためのものが「常識」をコード化しなければならないので、機械は常識を持ちません。
追加された 著者 Willem Van Onsem,
@ A.モリス:あなたが答えとして何を期待しているのかよくわかりません。引用されたアルゴリズムはそれがあるべきほど複雑にされていません。かなり短い実装があります( github.com/wh5a /ステップバイステップ/ blob/master /… )。はい、それはいくらかの仕事を必要とします、典型的にはアルゴリズムが何人かの人間のためのものが「常識」をコード化しなければならないので、機械は常識を持ちません。
追加された 著者 Willem Van Onsem,
この問題では、g(gx) - これは関数であり(後に(hy)をパラメータとしてとるため)、再帰的に呼ばれる性質上gが型を持つことを示唆しているためです。 (a - > a)そこから残りはおそらくもっと簡単に流れますか?
追加された 著者 zehnpaard,
この問題では、g(gx) - これは関数であり(後に(hy)をパラメータとしてとるため)、再帰的に呼ばれる性質上gが型を持つことを示唆しているためです。 (a - > a)そこから残りはおそらくもっと簡単に流れますか?
追加された 著者 zehnpaard,
@WillemVanOnsem論理や哲学を(まだ)勉強したことがないというのは怖いです。
追加された 著者 A. Morris,
@WillemVanOnsem論理や哲学を(まだ)勉強したことがないというのは怖いです。
追加された 著者 A. Morris,
@WillemVanOnsem少なくとも、与えられた関数の型を推定する方法の説明
追加された 著者 A. Morris,
@WillemVanOnsem少なくとも、与えられた関数の型を推定する方法の説明
追加された 著者 A. Morris,

8 答え

最上位レベルで関数を調べましょう。

f g h x y = g (g x) (h y)

まず型に名前を割り当て、それから機能についてさらに学びながらそれに沿ってそれらを特殊化します。

まず、最上位の式に型を割り当てましょう。それを a と呼びましょう。

g (g x) (h y) :: a

最初の引数を取り出して、それぞれ型を代入しましょう。

-- 'expanding'  (g (g x)) (h y) :: a
h y :: b
g (g x) :: b -> a

そしてまた

-- 'expanding' g (g x) :: b -> a
g x :: c
g :: c -> b -> a

そしてまた

-- 'expanding' g x :: c
x :: d
g :: d -> c

But hold on: we now have that g :: c -> b -> a and that g :: d -> c. So by inspection, we know that c and d are equivalent (written c ~ d) and also that c ~ b -> a.

これは、推測した g の2つのタイプを単純に比較することで推測できます。型変数はそれらの同等物に合うほど一般的なものであるため、これは型の矛盾ではないことに注意してください。たとえば、 Int〜Bool がどこかにあると推測した場合、この は矛盾になります。

それで、私たちは今、全部で以下の情報を手に入れました:

y :: e
h :: e -> b
x :: b -> a             -- Originally d, applied d ~ b -> a.
g :: (b -> a) -> b -> a -- Originally c -> b -> a, applied c ~ b -> a

This was done by substituting the most specific form of each type variable, that is substituting c and d for the more specific b -> a.

ですから、どの引数がどこにあるのかを調べるだけで、それがわかります。

f :: ((b -> a) -> b -> a) -> (e -> b) -> (b -> a) -> e -> a

これはGHCによって確認されています。

9
追加された

最上位レベルで関数を調べましょう。

f g h x y = g (g x) (h y)

まず型に名前を割り当て、それから機能についてさらに学びながらそれに沿ってそれらを特殊化することから始めます。

まず、最上位の式に型を割り当てましょう。それを a と呼びましょう。

g (g x) (h y) :: a

最初の引数を取り出して、それぞれ型を代入しましょう。

-- 'expanding'  (g (g x)) (h y) :: a
h y :: b
g (g x) :: b -> a

そしてまた

-- 'expanding' g (g x) :: b -> a
g x :: c
g :: c -> b -> a

そしてまた

-- 'expanding' g x :: c
x :: d
g :: d -> c

But hold on: we now have that g :: c -> b -> a and that g :: d -> c. So by inspection, we know that c and d are equivalent (written c ~ d) and also that c ~ b -> a.

これは、推測した g の2つのタイプを単純に比較することで推測できます。型変数はそれらの同等物に合うほど一般的なものであるため、これは型の矛盾ではないことに注意してください。たとえば、 Int〜Bool がどこかにあると推測した場合、この は矛盾になります。

それで、私たちは今、全部で以下の情報を手に入れました:

y :: e
h :: e -> b
x :: b -> a             -- Originally d, applied d ~ b -> a.
g :: (b -> a) -> b -> a -- Originally c -> b -> a, applied c ~ b -> a

This was done by substituting the most specific form of each type variable, that is substituting c and d for the more specific b -> a.

ですから、どの引数がどこにあるのかを調べるだけで、それがわかります。

f :: ((b -> a) -> b -> a) -> (e -> b) -> (b -> a) -> e -> a

これはGHCによって確認されています。

9
追加された

まあ関数は:

f g h x y = g (g x) (h y)

もっと冗長:

f g h x y = (g (g x)) (h y)

最初は、4つすべてのパラメータ( ghx 、および y )にはそれぞれ異なる種類があるとします。私たちの関数の出力タイプも紹介します(ここでは t )。

g :: a
h :: b
x :: c
y :: d
f g h x y :: t

しかし今、私たちはいくらかの推論を行うつもりです。たとえば g x があるので、これは g 関数と x パラメータを持つ関数アプリケーションがあることを意味します。つまり、 g は入力タイプが c の関数であるため、 g のタイプを次のように再定義します。

g :: a ~ (c -> e)
h :: b
x :: c
y :: d
f g h x y :: t

(here the tilde ~ means that two types are the same, so a is the same as c -> e)).

Since g has type g :: c -> e, and x has type c, this thus means that the result of the function application g x has type g x :: e.

関数として g 、引数として g x という別の関数アプリケーションがあります。つまり、 g の入力タイプ( c )は gx のタイプ()と同じである必要がありますe )、したがって c〜e であることがわかっているので、型は次のようになります。

     c ~ e
g :: a ~ (c -> c)
h :: b
x :: c
y :: d
f g h x y :: t

Now we see a function application with h the function, and y the argument. So that means that h is a function, and the input type is the same as the type of y :: d, so h has type d -> f, so that means:

     c ~ e
g :: a ~ (c -> c)
h :: b ~ (d -> f)
x :: c
y :: d
f g h x y :: t

finally we see a function application with g (g x) the function, and h y the argument, so that means that the ouput type of g (g x) :: c should be a function, with f as input type, and t as output type, so that means that c ~ (f -> t), and therefore:

     c ~ e
     c ~ (f -> t)
g :: a ~ (c -> c) ~ ((f -> t) -> (f -> t))
h :: b ~ (d -> f)
x :: (f -> t)
y :: d
f g h x y :: t

つまり、 f には ghxyf の型は次のとおりです。

f :: ((f -> t) -> (f -> t)) -> (d -> f) -> (f -> t) -> d -> t
--   \_________ __________/    \__ ___/    \__ ___/    |
--             v                  v           v        |
--             g                  h           x        y
5
追加された

まあ関数は:

f g h x y = g (g x) (h y)

もっと冗長:

f g h x y = (g (g x)) (h y)

最初は、4つすべてのパラメータ( ghx 、および y )にはそれぞれ異なる種類があるとします。私たちの関数の出力タイプも紹介します(ここでは t )。

g :: a
h :: b
x :: c
y :: d
f g h x y :: t

しかし今、私たちはいくらかの推論を行うつもりです。たとえば g x があるので、これは g 関数と x パラメータを持つ関数アプリケーションがあることを意味します。つまり、 g は入力タイプが c の関数であるため、 g のタイプを次のように再定義します。

g :: a ~ (c -> e)
h :: b
x :: c
y :: d
f g h x y :: t

(here the tilde ~ means that two types are the same, so a is the same as c -> e)).

Since g has type g :: c -> e, and x has type c, this thus means that the result of the function application g x has type g x :: e.

関数として g 、引数として g x という別の関数アプリケーションがあります。つまり、 g の入力タイプ( c )は gx のタイプ()と同じである必要がありますe )、したがって c〜e であることがわかっているので、型は次のようになります。

     c ~ e
g :: a ~ (c -> c)
h :: b
x :: c
y :: d
f g h x y :: t

Now we see a function application with h the function, and y the argument. So that means that h is a function, and the input type is the same as the type of y :: d, so h has type d -> f, so that means:

     c ~ e
g :: a ~ (c -> c)
h :: b ~ (d -> f)
x :: c
y :: d
f g h x y :: t

finally we see a function application with g (g x) the function, and h y the argument, so that means that the ouput type of g (g x) :: c should be a function, with f as input type, and t as output type, so that means that c ~ (f -> t), and therefore:

     c ~ e
     c ~ (f -> t)
g :: a ~ (c -> c) ~ ((f -> t) -> (f -> t))
h :: b ~ (d -> f)
x :: (f -> t)
y :: d
f g h x y :: t

つまり、 f には ghxyf の型は次のとおりです。

f :: ((f -> t) -> (f -> t)) -> (d -> f) -> (f -> t) -> d -> t
--   \_________ __________/    \__ ___/    \__ ___/    |
--             v                  v           v        |
--             g                  h           x        y
5
追加された

あなたはすでにそれをする方法を説明しました、しかしたぶんあなたは統一ステップを逃しました。つまり、2つの変数が同じであることがわかっていることがあります。

x :: a
y :: b
g :: a -> b    -- from g x
h :: c -> d    -- from h y
a ~ b          -- from g (g x)

ab が同じであることはわかっています。これは、 gx 、a bに渡したためです。 ga が必要です。そのため、今度はすべての ba に置き換え、すべての部分式について検討するまで続けます。

あなたの「巨大な混乱」のコメントに関して、私が言うべきことがいくつかあります。

  1. これがその方法です。それが難しすぎるなら、あなたはただ練習する必要があります、そしてそれはより簡単になるでしょう。あなたは直感を開発し始め、そしてそれはもっと簡単になるでしょう。
  2. この特定の機能は簡単な機能ではありません。私は12年間Haskellをプログラムしてきました、そして私はまだこれのために紙の上で統一アルゴリズムを通過する必要があります。それがとても抽象的であるという事実は助けにはなりません - この関数の目的がそれが非常に容易になるものであることを私が知っていたら
2
追加された

あなたはすでにそれをする方法を説明しました、しかしたぶんあなたは統一ステップを逃しました。つまり、2つの変数が同じであることがわかっていることがあります。

x :: a
y :: b
g :: a -> b    -- from g x
h :: c -> d    -- from h y
a ~ b          -- from g (g x)

ab が同じであることはわかっています。これは、 gx 、a bに渡したためです。 ga が必要です。そのため、今度はすべての ba に置き換え、すべての部分式について検討するまで続けます。

あなたの「巨大な混乱」のコメントに関して、私が言うべきことがいくつかあります。

  1. これがその方法です。それが難しすぎるなら、あなたはただ練習する必要があります、そしてそれはより簡単になるでしょう。あなたは直感を開発し始め、そしてそれはもっと簡単になるでしょう。
  2. この特定の機能は簡単な機能ではありません。私は12年間Haskellをプログラムしてきました、そして私はまだこれのために紙の上で統一アルゴリズムを通過する必要があります。それがとても抽象的であるという事実は助けにはなりません - この関数の目的がそれが非常に容易になるものであることを私が知っていたら
2
追加された

その下にあるすべてのエンティティのタイプを書き留めるだけです。

f g h x y = g (g x)   (h y) 
                 x :: x  y :: y
                       h :: y -> a            , h y :: a
               g :: x -> b                    , g x :: b
            g    :: b -> (a -> t)             , x ~ b , b ~ (a -> t)
f :: (x -> b) -> (y -> a) -> x -> y -> t      , x ~ b , b ~ (a -> t)
f :: (b -> b) -> (y -> a) -> b -> y -> t      , b ~ (a -> t)
--       g           h       x    y

Thus f :: ((a -> t) -> (a -> t)) -> (y -> a) -> (a -> t) -> y -> t. That's all.

確かに、

~> :t let f g h x y = g (g x) (h y) in f
    :: ((t1 -> t) -> t1 -> t) -> (t2 -> t1) -> (t1 -> t) -> t2 -> t

これはこのようになります:

  1. x must have some type, let's call it x: x :: x.
  2. y must have some type, let's call it y: y :: y.
  3. h y must have some type, let's call it a: h y :: a. hence h :: y -> a.
  4. g x must have some type, let's call it b: g x :: b. hence g :: x -> b.
  5. g _ _ must have some type, let's call it t. hence g :: b -> a -> t.
    which is the same as g :: b -> (a -> t).
  6. the two type signatures for g must unify, i.e. be the same under some substitution of type variables involved, since the two signatures describe the same entity, g.
    thus we must have x ~ b, b ~ (a -> t). This is the substitution.
  7. Having all the types of the arguments to f, we know it produces what g produces, i.e. t. So we can write down its type, (x -> b) -> (y -> a) -> x -> y -> t.
  8. Lastly, we substitute the types according to the substitution, to reduce the number of type variables involved. Thus we substitute b for x first, and then a -> t for b, each time removing the eliminated type variable from the substitution.
  9. When the substitution is empty, we are DONE.

Of course we could have chosen to replace b with x at first, ending up with the substitution x ~ (a -> t), and then we'd end up with the same type in the end, if we always replace the simpler types with the more complex ones (like, replacing b with (a -> t), and not vice versa).

簡単なステップ、保証された結果

1
追加された

その下にあるすべてのエンティティのタイプを書き留めるだけです。

f g h x y = g (g x)   (h y) 
                 x :: x  y :: y
                       h :: y -> a            , h y :: a
               g :: x -> b                    , g x :: b
            g    :: b -> (a -> t)             , x ~ b , b ~ (a -> t)
f :: (x -> b) -> (y -> a) -> x -> y -> t      , x ~ b , b ~ (a -> t)
f :: (b -> b) -> (y -> a) -> b -> y -> t      , b ~ (a -> t)
--       g           h       x    y

Thus f :: ((a -> t) -> (a -> t)) -> (y -> a) -> (a -> t) -> y -> t. That's all.

確かに、

~> :t let f g h x y = g (g x) (h y) in f
    :: ((t1 -> t) -> t1 -> t) -> (t2 -> t1) -> (t1 -> t) -> t2 -> t

これはこのようになります:

  1. x must have some type, let's call it x: x :: x.
  2. y must have some type, let's call it y: y :: y.
  3. h y must have some type, let's call it a: h y :: a. hence h :: y -> a.
  4. g x must have some type, let's call it b: g x :: b. hence g :: x -> b.
  5. g _ _ must have some type, let's call it t. hence g :: b -> a -> t.
    which is the same as g :: b -> (a -> t).
  6. the two type signatures for g must unify, i.e. be the same under some substitution of type variables involved, since the two signatures describe the same entity, g.
    thus we must have x ~ b, b ~ (a -> t). This is the substitution.
  7. Having all the types of the arguments to f, we know it produces what g produces, i.e. t. So we can write down its type, (x -> b) -> (y -> a) -> x -> y -> t.
  8. Lastly, we substitute the types according to the substitution, to reduce the number of type variables involved. Thus we substitute b for x first, and then a -> t for b, each time removing the eliminated type variable from the substitution.
  9. When the substitution is empty, we are DONE.

Of course we could have chosen to replace b with x at first, ending up with the substitution x ~ (a -> t), and then we'd end up with the same type in the end, if we always replace the simpler types with the more complex ones (like, replacing b with (a -> t), and not vice versa).

簡単なステップ、保証された結果

1
追加された