web-dev-qa-db-ja.com

クロージャーは不純な機能的なスタイルと見なされますか?

関数型プログラミングでは、クロージャーは不純と見なされますか?

値を関数に直接渡すことで、一般的にクロージャを回避できるようです。したがって、可能な限り閉鎖を回避する必要がありますか?

それらが不純であり、回避できると私が正しく述べているのに、なぜ多くの関数型プログラミング言語がクロージャをサポートするのですか?

純粋な関数 の基準の1つは、「関数は常に同じ引数値が与えられた場合、同じ結果値を評価することです。 -)。」

と思います

_f: x -> x + y
_

f(3)は常に同じ結果をもたらすとは限りません。 f(3)は、yの引数ではないfの値に依存します。したがって、fは純粋な関数ではありません。

すべてのクロージャは引数ではない値に依存しているので、どのクロージャが純粋であることはどのように可能ですか?はい、理論的には、閉じた値は一定である可能性がありますが、関数自体のソースコードを見るだけではそれを知る方法はありません。

これが私を導くところは、同じ機能が1つの状況では純粋であるが別の状況では不純かもしれないということです。 常に関数がそのソースコードを調べることによって純粋であるかどうかを判断することはできません。むしろ、そのような区別ができる前に、それが呼び出されている時点で、その環境の状況でそれを考慮する必要があるかもしれません。

私はこれについて正しく考えていますか?

35
user2179977

純度は次の2つの方法で測定できます。

  1. 同じ入力を指定すると、関数は常に同じ出力を返しますか。つまり、それは参照的に透明ですか?
  2. 関数はそれ自体の外で何かを変更しますか、つまり副作用がありますか?

1の答えがイエスで2の答えがノーの場合、関数は純粋です。クロージャは、クローズドオーバー変数を変更した場合にのみ、関数を不純にします。

26
Robert Harvey

クロージャーは、可能な関数型プログラミングの最も純粋な形式であるラムダ計算で表示されるため、「不純」とは呼びません...

関数型言語の関数はファーストクラスの市民であるため、クロージャは「不純」ではありません。つまり、値として扱うことができます。

これを想像してください(疑似コード):

foo(x) {
    let y = x + 1
    ...
}

yは値です。その値はxに依存しますが、xは不変であるため、yの値も不変です。さまざまなfoosを生成するさまざまな引数を使用してyを何度も呼び出すことができますが、これらのysはすべて異なるスコープに存在し、さまざまなxsに依存しているため、純粋ですそのまま残ります。

それを変更しましょう:

bar(x) {
    let y(z) = x + z
    ....
}

ここではクロージャーを使用していますが(xでクローズしています)、fooと同じです。異なる引数でbarを呼び出すと、異なる値のyが作成されます。 (覚えておいてください-関数は値です)これらはすべて不変なので、純粋さが損なわれません。

また、クロージャーはカレーと非常によく似た効果があることに注意してください。

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

bazbarと実際に違いはありません-両方で、引数にyを加えたxという名前の関数値を作成します。実際問題として、ラムダ計算では、クロージャーを使用して複数の引数を持つ関数を作成しますが、それでも不純ではありません。

10
Idan Arye

他の人は一般的な質問を答えでうまくカバーしているので、編集で合図する混乱を取り除くことだけを見ていきます。

クロージャは関数の入力にはならず、関数本体に「入り」ます。より具体的には、関数はその本体の外部スコープの値を参照します。

あなたはそれが機能を不純にする印象を受けています。一般的にそうではありません。関数型プログラミングでは、値は不変ほとんどの場合です。それはクローズドオーバー値にも適用されます。

次のようなコードがあるとします。

let make y =
    fun x -> x + y

make 3make 4を呼び出すと、makey引数をクロージャする2つの関数が得られます。そのうちの1つはx + 3を返し、もう1つはx + 4を返します。ただし、これらは2つの異なる機能であり、どちらも純粋です。それらは同じmake関数を使用して作成されましたが、それだけです。

ほとんどの場合数段落前に注意してください。

  1. 純粋なHaskellでは、不変の値のみを閉じることができます。クローズする変更可能な状態はありません。あなたはそのようにして純粋な関数を確実に得るでしょう。
  2. F#などの不純な関数型言語では、参照セルと参照型を閉じて、不純な関数を有効にすることができます。関数が純粋であるかどうかを知るには、関数が定義されているスコープを追跡する必要があるという点で正しいです。これらの言語で値が変更可能かどうかは簡単にわかるので、それほど問題にはなりません。
  3. OOP C#やJavaScriptなどのクロージャをサポートする言語では、状況は不純な関数型言語に似ていますが、変数はデフォルトで変更可能であるため、外側のスコープを追跡することはより難しくなります。

2と3の場合、これらの言語は純粋性に関する保証を提供しないことに注意してください。不純物は、閉鎖の特性ではなく、言語自体の特性です。クロージャーは、それ自体で状況を大きく変えることはありません。

9
scrwtp

通常、「不純」の定義を明確にするようにお願いしますが、この場合、それは問題ではありません。 純粋に機能的 という用語を対照しているとすると、本質的に破壊的なクロージャについては何もないので、答えは「いいえ」です。あなたの言語がクロージャーなしで純粋に機能的だったとしても、それはクロージャーがあれば純粋に機能的です。代わりに「機能しない」という意味の場合でも、答えは「いいえ」です。クロージャは関数の作成を容易にします。

データを関数に直接渡すことで、一般的にクロージャを回避できるようです。

はい。ただし、関数にはもう1つのパラメーターがあり、そのタイプが変更されます。クロージャを使用すると、変数に基づいて関数を作成できますなしパラメータを追加します。これは、たとえば、2つの引数をとる関数があり、1つの引数しかとらないバージョンを作成する場合に便利です。

EDIT:独自の編集/例に関して...

と思います

f:x-> x + y

f(3)は常に同じ結果を与えるとは限りません。 f(3)は、fの引数ではないyの値に依存します。したがって、fは純粋な関数ではありません。

Dependsは、ここではWordの間違った選択です。あなたがしたのとまったく同じウィキペディアの記事を引用してください:

コンピュータプログラミングでは、関数に関するこれらのステートメントが両方とも成立する場合、関数は純粋な関数として記述できます。

  1. 関数は常に、同じ引数値が指定された同じ結果値を評価します。関数の結果値は、プログラムの実行が進むにつれて、またはプログラムの異なる実行間で変化する可能性のある非表示の情報や状態に依存したり、I/Oデバイスからの外部入力に依存したりすることはできません。
  2. 結果の評価は、変更可能なオブジェクトの変異やI/Oデバイスへの出力など、意味的に観察可能な副作用や出力を引き起こしません。

yが不変(通常、関数型言語の場合)であると仮定すると、条件1が満たされます。xのすべての値について、f(x)の値は変化しません。これは、yが定数と同じであり、x + 3は純粋です。また、ミューテーションやI/Oが発生していないことも明らかです。

5
Doval

非常に迅速に:「likeを代入するとlikeにつながる」場合、置換は「参照透過的」であり、すべての効果が戻り値に含まれている場合、関数は「純粋」です。これらはどちらも正確にすることができますが、同一ではなく、一方が他方を示唆するものではないことに注意することが重要です。

それでは、クロージャーについて話しましょう。

退屈な(ほとんど純粋な)「閉鎖」

ラムダ項を評価するときに、(バインドされた)変数を環境ルックアップとして解釈するため、クロージャが発生します。したがって、評価の結果としてラムダ項を返すと、その内部の変数は、定義されたときに取得した値を「閉じた」ものになります。

単純なラムダ計算ではこれは簡単なことであり、概念全体が消えてしまいます。これを実証するために、比較的軽量なラムダ計算インタープリターを次に示します。

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

注目すべき重要な部分は、環境を新しい名前で拡張するときにaddEnvにあります。この関数は、解釈されたAbstraction項(ラムダ項)の「内部」でのみ呼び出されます。 Var項を評価するたびに環境が「検索」されるため、これらのVarsは、取得されたNameで参照されているEnvに解決されますAbsを含むVartractionによる。

さて、再び、単純なLC用語では、これは退屈です。つまり、バインドされた変数は、誰もが気にする限りは定数であるということです。それらは、環境で次のように示す値として直接かつ即座に評価されますその時点までの語彙スコープ。

これも(ほぼ)純粋です。ラムダ計算における用語の唯一の意味は、その戻り値によって決まります。唯一の例外は、オメガの用語によって具現化される非終了の副作用です。

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

興味深い(純粋でない)閉鎖

ここで、特定の背景では、上記のプレーンなLC=で説明されているクロージャは、クローズした変数と対話できるという概念がないため、退屈です。特に、「クロージャ」という言葉は、次のJavascriptのようなコードを呼び出します

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

これは、内部関数nincr変数を閉じており、incrの呼び出しがその変数と有意に相互作用していることを示しています。 mk_counterは純粋ですが、incrは明らかに不純です(参照としても透過的ではありません)。

これら2つのインスタンスの違いは何ですか?

「変数」の概念

単純なLCの意味での置換と抽象化の意味を見ると、それらは明らかに明白であることがわかります。変数は文字通りnothing即時の環境ルックアップよりも多いです。ラムダ抽象化は文字通りnothing内部式を評価する拡張環境を作成する以上のものです。このモデルには、mk_counter/incrで見たような動作の余地はありません。許容されるバリエーションはありません。

多くの人にとって、これは「変数」が意味するもの、つまり変化の中心です。ただし、意味論者はLCで使用される変数の種類とJavascriptで使用される「変数」の種類を区別することを好みます。そうするために、後者を「可変セル」または"スロット"。

この命名法は、数学における「変数」の長い歴史的な使用法に従い、「未知」のようなものを意味していました。(数学的)式「x + x」では、xが時間とともに変化することを許可していません。代わりに、 xが取る(単一の、定数)値に関係なく意味を持つことを意味します。

したがって、値をスロットに入れて取り出す能力を強調するために、「スロット」と言います。

さらに混乱を加えるために、JavaScriptでは、これらの「スロット」は変数と同じように見えます。

var x;

作成し、次に書くときに

x;

これは、そのスロットに現在格納されている値を調べることを示しています。これをより明確にするために、純粋な言語は、スロットを名前を(数学、ラムダ計算)の名前と見なす傾向があります。この場合、スロットから取得または挿入するときに明示的にラベルを付ける必要があります。そのような表記は次のようになります

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

この表記法の利点は、数学変数と可変スロットを明確に区別できるようになったことです。変数は値としてスロットを使用できますが、変数によって指定された特定のスロットは、そのスコープ全体で一定です。

この表記を使用して、mk_counterの例を書き直すことができます(今回はHaskellに似た構文ですが、明らかにHaskellに似ていないセマンティクスです)。

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

この場合、この可変スロットを操作するプロシージャを使用しています。これを実装するには、xのような名前の一定の環境だけでなく、必要なすべてのスロットを含む可変環境も閉じる必要があります。これは、人々が非常に愛する「閉鎖」という一般的な概念に近いものです。

繰り返しますが、mkCounterは非常に不純です。また、それは非常に参照不透明です。ただし、副作用は名前のキャプチャやクロージャでは発生せず、代わりに可変セルのキャプチャと、getput

最終的に、これがあなたの質問に対する最後の答えだと思います:純粋性は、(数学的)変数のキャプチャーではなく、キャプチャーされた変数によって名前が付けられた可変スロットで実行される副作用操作によって影響を受けます。

LCに近づこうとしない、または純粋性を維持しようとしない言語では、これら2つの概念が混同されて混乱につながることがよくあります。

3
J. Abrahamson

いいえ、関数型プログラミングの通常のケースである、閉じた値が定数である限り(クロージャーや他のコードによって変更されない限り)、クロージャーによって関数が不純になることはありません。

代わりに常に引数として値を渡すことができますが、通常は、かなりの困難なしに行うことができないことに注意してください。例(coffeescript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

あなたの提案によって、あなたは単に返すことができます:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

この関数は、現時点では呼び出されたではなく、ただ定義されたなので、closedValueに必要な値を渡す方法を見つける必要があります関数が実際に呼び出されるポイント。せいぜいこれは多くのカップリングを作成します。最悪の場合、呼び出しポイントでコードを制御しないため、事実上不可能です。

クロージャーをサポートしていない言語のイベントライブラリは、通常、任意のデータをコールバックに戻す他の方法を提供しますが、それは見栄えが悪く、ライブラリのメンテナーとライブラリユーザーの両方にとって非常に複雑になります。

1
Karl Bielefeldt