Haskellでは、他の多くの関数型言語と同様に、関数foldl
は、たとえばfoldl (-) 0 [1,2,3,4] = -10
のように定義されています。
foldl (-) 0 [1, 2,3,4]
は定義上((((0 - 1) - 2) - 3) - 4)
であるため、これは問題ありません。
しかし、Racketでは、(foldl - 0 '(1 2 3 4))
は2です。これは、Racketが「インテリジェントに」次のように計算するためです。(4 - (3 - (2 - (1 - 0))))
、これは実際には2です。
もちろん、次のように補助関数フリップを定義すると、次のようになります。
(define (flip bin-fn)
(lambda (x y)
(bin-fn y x)))
そうすれば、RacketでHaskellと同じ動作を実現できます。(foldl - 0 '(1 2 3 4))
の代わりに、(foldl (flip -) 0 '(1 2 3 4))
と書くことができます。
問題は、ラケットのfoldl
が他の言語とは異なる、このような奇妙な(非標準で直感的でない)方法で定義されているのはなぜですか?
Haskellの定義は均一ではありません。ラケットでは、両方のフォールドへの関数の入力順序が同じであるため、foldl
をfoldr
に置き換えるだけで、同じ結果を得ることができます。 Haskellバージョンでこれを行うと、(通常は)異なる結果が得られます。これは、2つの異なるタイプで確認できます。
(実際、適切な比較を行うには、両方の型変数が整数であるこれらのおもちゃの数値の例を避ける必要があると思います。)
これには、セマンティックの違いに応じてfoldl
またはfoldr
のいずれかを選択することをお勧めするNice副産物があります。私の推測では、ハスケルの命令では、操作に応じて選択する可能性が高いと思います。これの良い例があります。各数値を減算するためにfoldl
を使用しました。これは非常に「明白な」選択であるため、foldl
が通常であるという事実を見逃しがちです。怠惰な言語では悪い選択です。
もう1つの違いは、Haskellバージョンは通常の方法でRacketバージョンよりも制限されていることです。Racketは任意の数のリストを受け入れることができるのに対し、それは正確に1つの入力リストで動作します。これにより、入力関数の引数の順序を統一することがより重要になります)。
最後に、折り畳みは新しいトリックとはほど遠いため、Racketが「他の多くの関数型言語」から分岐したと考えるのは誤りです。RacketのルーツはHaskell(またはこれらの他の言語)よりはるかに古いためです。したがって、質問は逆に進む可能性があります:Haskellのfoldl
が奇妙な方法で定義されているのはなぜですか?(いいえ、(-)
は良い言い訳ではありません。)
これは何度も何度も人々を悩ませているように思われるので、私は少し足を運びました。これは決して決定的なものではなく、私の中古の推測です。あなたがもっと知っているか、もっとよく知っているなら、これを自由に編集して、関係者に電子メールを送って尋ねてください。具体的には、これらの決定がなされた日付がわからないので、以下のリストは大まかな順序です。
最初にLISPがあり、いかなる種類の「折り畳み」についても言及されていませんでした。代わりに、LISPにはreduce
があります。これは、特にそのタイプを考慮すると、非常に不均一です。たとえば、:from-end
は、左スキャンか右スキャンかを決定するキーワード引数であり、異なるアキュムレータ関数を使用します。つまり、アキュムレータタイプはそのキーワードに依存します。 。これは他のハックに追加されます。通常、最初の値はリストから取得されます(:initial-value
を指定しない限り)。最後に、:initial-value
を指定せず、リストが空の場合、実際にはゼロ引数に関数を適用して結果を取得します。
これはすべて、reduce
が通常、その名前が示すように使用されることを意味します。つまり、値のリストを1つの値に縮小します。この場合、2つのタイプは通常同じです。ここでの結論は、フォールディングと同様の目的を果たしているということですが、フォールディングで得られる一般的なリスト反復構造ほど有用ではありません。これは、reduce
とそれ以降のフォールド操作の間に強い関係がないことを意味していると思います。
LISPに続き、適切に折りたたまれている最初の関連言語はMLです。以下のnewacctの回答に記載されているように、そこで行われた選択は、 ユニフォームタイプバージョン (つまり、Racketが使用するもの)を使用することでした。
次のリファレンスはBird&WadlerのItFP(1988)で、これは 異なるタイプ (Haskellのように)を使用します。ただし、それらは 付録に注意 ミランダは(Racketと同じタイプ)です。
後でミランダ 引数の順序を切り替えました (つまり、ラケットの順序からハスケルの順序に移動しました)。具体的には、そのテキストは次のように述べています。
警告-このfoldlの定義は、古いバージョンのMirandaの定義とは異なります。ここにあるのはBirdand Wadler(1988)のものと同じです。古い定義では、「op」の2つの引数が逆になっています。
Haskellは、さまざまなタイプを含め、ミランダから多くのものを取りました。 (もちろん、日付はわかりませんので、ミランダの変更はHaskellによるものかもしれません。)いずれにせよ、この時点でコンセンサスがなかったことは明らかであり、したがって上記の逆の質問が当てはまります。
OCamlはHaskellの方向性を採用し、 異なるタイプ を使用します。
「プログラムの設計方法」(別名HtDP)はほぼ同じ時期に書かれたものだと思いますが、彼らは 同じタイプ を選びました。ただし、動機や説明はありません。実際、その演習の後は、単に 組み込み関数の1つ と呼ばれます。
ラケットの foldoperations の実装は、もちろん、ここで言及されている「組み込み」でした。
それから SRFI-1 が来ました、そして選択は同じタイプのバージョン(ラケットとして)を使うことでした。この決定 質問でした ジョンデビッドストーンによる、SRFIのコメントを指摘します
注:MIT SchemeとHaskellは、
reduce
関数とfold
関数のFの引数の順序を反転します。
後でオーリン これに対処 :彼が言ったのは:
良い点ですが、2つの機能の一貫性が必要です。最初の状態値:srfi-1、最後のSML状態値:Haskell
特に彼のstate-valueの使用に注意してください。これは、一貫した型が演算子の順序よりもおそらく重要なポイントであるという見方を示唆しています。
「他の言語とは違う」
反例として、Standard ML(MLは非常に古くて影響力のある関数型言語です)のfoldl
も次のように機能します: http://www.standardml.org/Basis/list。 html#SIG:LIST.foldl:VAL
ラケットのfoldl
とfoldr
(および SRFI-1のfold
とfold-right
)その特性を持っている
(foldr cons null lst) = lst
(foldl cons null lst) = (reverse lst)
そのため、引数の順序が選ばれたと思います。
ラケットから ドキュメント 、foldl
の説明:
(foldl proc init lst ...+) → any/c
あなたの質問の2つの興味深い点が言及されています:
入力リストは左から右にトラバースされます
そして
foldlは一定の空間でlstを処理します
簡単にするために、その実装がどのように見えるかを1つのリストで推測します。
(define (my-foldl proc init lst)
(define (iter lst acc)
(if (null? lst)
acc
(iter (cdr lst) (proc (car lst) acc))))
(iter lst init))
ご覧のとおり、左から右への走査と一定のスペースの要件は満たされていますが(iter
の末尾再帰に注意してください)、orderproc
の引数の説明で指定されたことはありません。したがって、上記のコードを呼び出した結果は次のようになります。
(my-foldl - 0 '(1 2 3 4))
> 2
proc
の引数の順序を次のように指定した場合:
(proc acc (car lst))
その場合、結果は次のようになります。
(my-foldl - 0 '(1 2 3 4))
> -10
私のポイントは、foldl
のドキュメントは、proc
の引数の評価順序について何の仮定もしておらず、定数スペースが使用されていることと、リストは左から右に評価されます。
補足として、次のように書くだけで、式に必要な評価順序を取得できます。
(- 0 1 2 3 4)
> -10