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


33

関数型プログラミングではクロージャは不純であると考えられますか?

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

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

純粋な関数の基準の1つは、「関数は常に同じ引数値を指定すると同じ結果値を評価する」ということです。

仮に

f: x -> x + y

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

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

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

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


6
Haskellでは常にクロージャーを使用していますが、Haskellはそれと同じくらい純粋です。
トーマスエディング14

5
純粋な関数型言語でyは、変更できないため、の出力f(3)は常に同じになります。
リリーチョン14

4
yfへの入力として明示的にマークされていない場合でも、の定義の一部ですf- (明示的に依存するために関数f_yを表すことができます)のf観点から定義されているため、変更は異なる関数を提供します。特定のために定義された特定の関数は非常に純粋です。(たとえば、2つの関数とは異なる関数であり、同じ文字を使用してそれらを示しているにもかかわらず、両方とも純粋です。)yyyf_yyf: x -> x + 3f: x -> x + 5
ShreevatsaR 14

回答:


26

純度は2つのことで測定できます。

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

1の答えがyesで2の答えがnoの場合、関数は純粋です。クロージャーは、closed-over変数を変更した場合にのみ関数を不純にします。


最初の項目決定論ではありませんか?または、それも純度の一部ですか?私はプログラミングの文脈における「純度」の概念にあまり詳しくありません。

4
@JimmyHoffa:必ずしもではありません。ハードウェアタイマーの出力を関数に配置できますが、関数の外部は変更されません。
ロバートハーヴェイ14

1
@RobertHarveyこれは、関数への入力をどのように定義するかに関するすべてですか?ウィキペディアからの私の引用は、関数の引数に焦点を当てていますが、入力として閉じた変数をさらに考慮します。
user2179977

8
@ user2179977:可変でない限り、関数への追加入力としてクローズドオーバー変数を考慮すべきではありません。むしろ、クロージャー自体を関数であり、の異なる値で閉じるときは異なる関数であると考える必要がありyます。したがって、たとえば、それ自体が関数であるgような関数を定義g(y)しますx -> x + y。次に、g整数の関数が返す関数であることが、g(3)整数戻り整数の関数であり、g(2)ある異なる戻り整数こと整数の機能。3つの機能はすべて純粋です。
スティーブジェソップ14

1
@ダークホッグ:はい。私の更新を参照してください。
ロバートハーヴェイ14

10

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

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

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

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

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

それを変更しましょう:

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

ここではクロージャーを使用しています(xを閉じています)が、それはただと同じですfoo- bar異なる引数での異なる呼び出しは異なる値を作成しますy(覚えている-関数は値です)すべて不変なので純度はそのままです。

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

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

baz本当に違いはありませんbar-両方で、y引数plusを返すという名前の関数値を作成しますx。実際のところ、Lambda Calculusでは、クロージャーを使用して複数の引数を持つ関数を作成しますが、それでも不純ではありません。


9

他の人は回答で一般的な質問をうまくカバーしているので、編集であなたが合図する混乱をクリアすることだけを見ていきます。

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

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

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

let make y =
    fun x -> x + y

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

なお、ほとんどの時間数段落をバックアップ。

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

2と3については、これらの言語は純度に関する保証を提供しないことに注意してください。そこにある不純性は、クロージャーの特性ではなく、言語そのものの特性です。クロージャは、それ自体で画像をあまり変更しません。


1
Haskellで変更可能な値を完全に閉じることができますが、そのようなことにはIOモナドで注釈が付けられます。
ダニエル・グラッツァー14

1
@jozefgいいえ、不変のIO A値を閉じますが、クロージャーのタイプはIO (B -> C)そうです。純度は維持されています
カレス

5

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

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

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

編集:あなた自身の編集/例に関して...

仮に

f:x-> x + y

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

ここでは、Dependsは間違った単語の選択です。あなたがやったのとまったく同じウィキペディアの記事を引用する:

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

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

y不変(通常は関数型言語の場合)であると仮定すると、条件1が満たされます:のすべての値についてx、の値はf(x)変化しません。これはy、定数と変わらず、x + 3純粋であるという事実から明らかです。また、突然変異やI / Oが進行していないことも明らかです。


3

非常に迅速:置換は「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、新しい名前で環境を拡張するときです。この関数は、解釈されたAbsトラクション項(ラムダ項)の「内部」でのみ呼び出されます。環境は、Var用語を評価するたびに「ルックアップ」されるため、を含むトラクションによってキャプチャされたで参照されているものにVar解決されます。NameEnvAbsVar

繰り返しになりますが、単純な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

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

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

「変数」の概念

置換と抽象化が単純なLCの意味で何を意味するかを見ると、それらは明らかに単純であることがわかります。変数は、文字通り、即時の環境検索にすぎません。ラムダ抽象化は、文字通り、内部式を評価するための拡張環境を作成することに他なりません。このモデルには、mk_counter/で見た種類の振る舞いの余地がありません。これは、incr許容されるバリエーションがないためです。

多くの人にとって、これは「変数」が意味するもの、つまり変動の中心です。ただし、セマンティクスは、LCで使用される変数の種類とJavascriptで使用される「変数」の種類を区別することを好みます。そうするために、彼らは後者を「可変セル」または「スロット」と呼ぶ傾向があります。

この命名法は、数学における「変数」の長い歴史的な用法に従っており、「未知」のようなものを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つの概念が混同されて混乱を招くことがよくあります。


1

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

代わりに常に引数として値を渡すことができますが、通常はかなりの困難なしに渡すことはできません。例(coffeescript):

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

あなたの提案により、あなたはただ返すことができます:

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.