7

Standard ML、F#、OCaml、Haskellなどの静的に型付けされた関数型プログラミング言語では、関数は通常、パラメーターを互いに分離し、関数名から空白で区切って記述されます。

let add a b =
    a + b

ここでの型は" int -> (int -> int)"です。つまり、intを受け取り、その順番がintを取り、最後にintを返す関数を返す関数です。これによりカリー化が可能になります。

タプルを引数として取る同様の関数を定義することもできます。

let add(a, b) =
    a + b

この場合、タイプは「(int * int) -> int」になります。

言語設計の観点から、型代数でこれら2つの型パターンを単純に識別できない理由はありますか?つまり、「(a * b)->c」が「a->(b-> c)」になり、両方のバリアントを同じように簡単にカレーできるようになります。

私が言及した4つの言語のような言語が設計されたとき、この質問が浮かび上がったに違いないと思います。では、これら4つの言語すべてがこれら2つのタイプのパターンを「統合」しないことを選択した理由を示す理由や調査を知っている人はいますか?

4

5 に答える 5

7

今日のコンセンサスは、カリー化 (フォーム) によって複数の引数a -> b -> cを処理し、(リストなどで) タプル型の値が本当に必要な場合に備えてタプルを予約することだと思います。このコンセンサスは、(純粋に慣例として) 複数の引数を取る関数にタプルを使用する標準 ML 以降、静的に型付けされたすべての関数型言語で尊重されています。

これはなぜですか?標準 ML はこれらの言語の中で最も古い言語であり、人々が最初に ML コンパイラを作成したとき、カリー化された引数を効率的に処理する方法は知られていませんでした。(問題の根底にあるのは、カリー化された関数まだ見たことのない他のコードによって部分的に適用される可能性があるという事実であり、その可能性を念頭に置いてコンパイルする必要があります。) 標準 ML が設計されて以来、コンパイラはサイモン・マーロウとサイモン・ペイトン・ジョーンズによる優れた論文で最新技術について読むことができます。

それらすべての中で最も古い関数型言語である LISP は、カリー化や部分適用に非常に不向きな具体的な構文を持っています。うーん。

于 2009-01-02T02:57:59.157 に答える
4

カレーとカレーのない関数型を混同しない理由の少なくとも1つは、タプルが戻り値として使用される場合(これらの型付き言語で複数の値を返すための便利な方法)、非常に混乱することです。統合型システムでは、どのようにして機能をうまく構成できるのでしょうか。a -> (a * a)また、に変換されますかa -> a -> a?もしそうなら、(a * a) -> aそしてa -> (a * a)同じタイプですか?そうでない場合、どのように作曲a -> (a * a)(a * a) -> aますか?

あなたは作曲の問題に対して興味深い解決策を提案します。ただし、部分適用との相性が悪いため、満足のいくものではありません。これは、カレー機能の重要な便利さです。Haskellの問題を説明しようと思います:

apply f a b = f a b
vecSum (a1,a2) (b1,b2) = (a1+b1,a2+b2)

さて、おそらくあなたのソリューションは理解できるでしょうmap (vecSum (1,1)) [(0,1)]が、同等のものはmap (apply vecSum (1,1)) [(0,1)]どうですか?複雑になります!完全に解凍するということは、(1,1)がapplyのa&b引数で解凍されることを意味しますか?それは意図ではありません...そしていずれにせよ、推論は複雑になります。

つまり、(1)古いシステムで有効なコードのセマンティクスを維持し、(2)統合されたシステムに合理的な直感とアルゴリズムを提供しながら、カレーとアンカレーの関数を統合することは非常に難しいと思います。しかし、それは興味深い問題です。

于 2009-01-02T01:04:16.720 に答える
2

タプルを別のタイプとして持つと便利であり、異なるタイプを別々に保つのが良いからかもしれません。

少なくともHaskellでは、あるフォームから別のフォームに移動するのは非常に簡単です。

Prelude> let add1 a b = a+b
Prelude> let add2 (a,b) = a+b
Prelude> :t (uncurry add1)
(uncurry add1) :: (Num a) => (a, a) -> a
Prelude> :t (curry add2)
(curry add2) :: (Num a) => a -> a -> a

uncurry add1と同じでadd2あり、curry add2と同じadd1です。他の言語でも同様のことが可能だと思います。タプルで定義されたすべての関数を自動的にカリー化すると、どのような大きなメリットがありますか?(それがあなたが求めているように見えるので。)

于 2009-01-02T00:54:33.260 に答える
2

タプルはこれらの言語には存在せず、単に関数パラメーターとして使用されます。これらは、構造化されたデータを表す非常に便利な方法です。たとえば、2D ポイント ( int * int)、リスト要素 ( 'a * 'a list)、またはツリー ノード ( 'a * 'a tree * 'a tree) です。また、構造はタプルの構文糖衣にすぎないことも忘れないでください。すなわち、

type info = 
  { name : string;
    age : int;
    address : string; }

(string * int * string)タプルを扱う便利な方法です。

プログラミング言語でタプルが必要な根本的な理由はありません(ブーリアンや整数と同じように、ラムダ計算でもタプルを構築できます — 実際、カリー化を使用します)。しかし、タプルは便利で便利です。

*例えば、

tuple a b = λf.f a b
fst x     = x (λa.λb.a)
snd x     = x (λa.λb.b)
curry f   = λa.λb.f (λg.g a b)
uncurry f = λx.x f
于 2009-01-02T01:23:44.620 に答える
2

namin の良い答えの下にあるコメントを拡張します。

したがって、タイプを持つ関数を想定します'a -> ('a * 'a)

let gimme_tuple(a : int) =
    (a*2, a*3)

次に、タイプを持つ関数を想定します('a * 'a) -> 'b

let add(a : int, b) =
    a + b

次に、構成(私が提案する合成を想定)は、私が見る限り問題を引き起こさないでしょう:

let foo = add(gimme_tuple(5))
// foo gets value 5*2 + 5*3 = 25

しかし、最後のコード スニペットに取って代わる多態的な関数を考えることができaddます。たとえば、2 つのタプルを取り、2 つの要素のリストを作成する小さな関数です。

let gimme_list(a, b) =
    [a, b]

これはタイプ('a * 'a) -> ('a list). 構成は今問題になるでしょう。検討:

let bar = gimme_list(gimme_tuple(5))

bar値を持つか、タイプの関数になり、最終的にタプルをヘッドとするリストを返すでしょう[10, 15] : int listか? これが機能するためには、naminの回答へのコメントで、型システムに、実際の仮パラメーターへのバインディングが「可能な限り完全」であるという追加のルールが必要である、つまり、システムが非部分バインディングを試行する必要があると仮定しました最初に、完全なバインディングが達成できない場合にのみ、部分的なバインディングを試みます。この例では、完全なバインディングが可能であるため、値を取得することを意味します。bar(int * int) -> ((int * int) list)(10, 15)[10, 15]

そのような「可能な限り最大限」という概念は、本質的に無意味なのでしょうか? わかりませんが、すぐには理由がわかりません。

もちろん、1 つの問題は、最後のコード スニペットの 2 番目の解釈が必要な場合、それを取得するために余分なフープ (通常は無名関数) をジャンプする必要があることです。

于 2009-01-02T02:05:21.557 に答える