Yコンビネーターは、物事の「機能」面からのコンピューターサイエンスコンセプトです。ほとんどのプログラマーは、コンビネーターについて聞いたことさえあれば、コンビネーターについてまったく知りません。
- Yコンビネーターとは何ですか?
- コンビネーターはどのように機能しますか?
- 彼らは何のために良いのですか?
- それらは手続き型言語で役立ちますか?
Yコンビネーターは、物事の「機能」面からのコンピューターサイエンスコンセプトです。ほとんどのプログラマーは、コンビネーターについて聞いたことさえあれば、コンビネーターについてまったく知りません。
回答:
あなたが長い読書の準備ができているなら、マイクバニエは持っています素晴らしい説明をし。要するに、再帰を必ずしもネイティブでサポートしているわけではない言語で実装することができます。
Yコンビネータは、関数内から関数を参照できない場合に再帰を可能にする「関数」(他の関数で動作する関数)です。コンピュータサイエンス理論では、それは再帰を一般化し、その実装を抽象化し、それにより問題の関数の実際の作業からそれを分離します。再帰関数にコンパイル時の名前を必要としない利点は、一種のボーナスです。=)
これは、ラムダ関数をサポートする言語に適用されます。表現ラムダベースの性質は、通常、名前で自分自身を参照できないことを意味します。そして、変数を宣言して参照し、次にラムダを代入して自己参照ループを完了するという方法でこれに対処することは、脆弱です。ラムダ変数をコピーすると、元の変数が再割り当てされ、自己参照が壊れます。
Y型コンビネーターは、静的型付き言語(手続き型言語がそうであることが多い)での実装や使用が面倒です。通常、型の制限により、問題の関数の引数の数をコンパイル時に知る必要があるためです。これは、使用する必要のある引数カウントに対してy結合子を記述する必要があることを意味します。
以下は、C#でのYコンビネーターの使用方法と動作の例です。
Yコンビネーターの使用には、再帰関数を作成する「通常とは異なる」方法が含まれます。最初に、関数をそれ自体ではなく、既存の関数を呼び出すコードの一部として記述する必要があります。
// Factorial, if func does the same thing as this bit of code...
x == 0 ? 1: x * func(x - 1);
次に、それを関数を呼び出して呼び出す関数に変換し、その関数を返します。これは1つの関数を取り、それを使って別の関数を生成するため、関数型と呼ばれます。
// A function that creates a factorial, but only if you pass in
// a function that does what the inner function is doing.
Func<Func<Double, Double>, Func<Double, Double>> fact =
(recurs) =>
(x) =>
x == 0 ? 1 : x * recurs(x - 1);
これで、関数を取り、階乗のように見える別の関数を返す関数ができましたが、それ自体を呼び出すのではなく、外部関数に渡された引数を呼び出します。どのようにしてこれを階乗にしますか?内部関数をそれ自体に渡します。Y-Combinatorは、永続的な名前を持つ関数であることにより、再帰を導入することができます。
// One-argument Y-Combinator.
public static Func<T, TResult> Y<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> F)
{
return
t => // A function that...
F( // Calls the factorial creator, passing in...
Y(F) // The result of this same Y-combinator function call...
// (Here is where the recursion is introduced.)
)
(t); // And passes the argument into the work function.
}
階乗自体を呼び出すのではなく、階乗が階乗ジェネレータを呼び出します(Y-Combinatorへの再帰呼び出しによって返されます)。そして、tの現在の値に応じて、ジェネレーターから返される関数は、t-1でジェネレーターを再度呼び出すか、または単に1を返し、再帰を終了します。
それは複雑で不可解ですが、実行時にすべてが揺さぶられ、その動作の鍵は「据え置き実行」と、2つの関数にまたがる再帰の分割です。内部Fは引数として渡され、必要な場合にのみ、次の反復で呼び出されます。
fix :: (a -> a) -> a
。そして、a
canは必要なだけの引数の関数にすることができます。これは、静的型付けが実際にこれを面倒にすることはないことを意味します。
これは、数年前に書いた説明であるhttp://www.mail-archive.com/boston-pm@mail.pm.org/msg02716.htmlから削除しました。
この例ではJavaScriptを使用しますが、他の多くの言語でも機能します。
私たちの目標は、1変数の関数のみを使用し、割り当てを使用せずに、1変数の再帰関数を記述できるようにすることです。これは、名前で物事を定義するなどの理由です。与えられます。)不可能のようですよね?例として、階乗を実装しましょう。
さて、ステップ1は、少し浮気をすれば簡単にできるということです。2つの変数の関数と代入を使用すると、少なくとも代入を使用して再帰を設定する必要を回避できます。
// Here's the function that we want to recurse.
X = function (recurse, n) {
if (0 == n)
return 1;
else
return n * recurse(recurse, n - 1);
};
// This will get X to recurse.
Y = function (builder, n) {
return builder(builder, n);
};
// Here it is in action.
Y(
X,
5
);
さて、もっとごまかすことができるかどうか見てみましょう。まず、割り当てを使用していますが、使用する必要はありません。XとYをインラインで書くだけです。
// No assignment this time.
function (builder, n) {
return builder(builder, n);
}(
function (recurse, n) {
if (0 == n)
return 1;
else
return n * recurse(recurse, n - 1);
},
5
);
しかし、2つの変数の関数を使用して、1つの変数の関数を取得しています。修正できますか?Haskell Curryという名前の賢い人には、きちんとしたトリックがあります。もしあなたがより高い次数の関数を持っていれば、1変数の関数しか必要ありません。証明は、次のような純粋に機械的なテキスト変換を使用して、2(一般的な場合はそれ以上)の変数の関数から1つの変数に取得できることです。
// Original
F = function (i, j) {
...
};
F(i,j);
// Transformed
F = function (i) { return function (j) {
...
}};
F(i)(j);
ここで...はまったく同じままです。(このトリックは、発明者から「カリー化」と呼ばれています。Haskellという言語はHaskell Curryの名前でもあります。役に立たない雑学の下でファイルしてください。)次に、この変換をどこにでも適用して、最終バージョンを取得します。
// The dreaded Y-combinator in action!
function (builder) { return function (n) {
return builder(builder)(n);
}}(
function (recurse) { return function (n) {
if (0 == n)
return 1;
else
return n * recurse(recurse)(n - 1);
}})(
5
);
ぜひお試しください。戻って、それをボタンに結び付けるalert()。このコードは、2つの変数の代入、宣言、関数を使用せずに、階乗を再帰的に計算します。(しかし、それがどのように機能するかを追跡しようとすると、頭が回転する可能性があります。派生せずにそれを渡すと、わずかに再フォーマットするだけで、コードが邪魔になり混乱することになります。)
階乗を再帰的に定義する4行を、必要な他の再帰関数に置き換えることができます。
function (n) { return builder(builder)(n);}
代わりになぜ書いたのbuilder(builder)
ですか?
これを一から構築しようとする用途はあるのでしょうか。どれどれ。基本的な再帰的な階乗関数は次のとおりです。
function factorial(n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
fact
計算自体を実行する代わりに、無名の階乗計算関数を返すという名前の新しい関数をリファクタリングして作成してみましょう。
function fact() {
return function(n) {
return n == 0 ? 1 : n * fact()(n - 1);
};
}
var factorial = fact();
少し変ですが、何も問題はありません。各ステップで新しい階乗関数を生成しているだけです。
この段階での再帰はまだかなり明白です。fact
この関数は、自身の名前を知っている必要があります。再帰呼び出しをパラメーター化しましょう:
function fact(recurse) {
return function(n) {
return n == 0 ? 1 : n * recurse(n - 1);
};
}
function recurser(x) {
return fact(recurser)(x);
}
var factorial = fact(recurser);
それは素晴らしいことですが、recurser
それでも自分の名前を知る必要があります。それもパラメータ化しましょう:
function recurser(f) {
return fact(function(x) {
return f(f)(x);
});
}
var factorial = recurser(recurser);
ここで、recurser(recurser)
直接呼び出す代わりに、その結果を返すラッパー関数を作成しましょう。
function Y() {
return (function(f) {
return f(f);
})(recurser);
}
var factorial = Y();
これでrecurser
名前を完全に取り除くことができます。これは、Yの内部関数への単なる引数であり、関数自体で置き換えることができます。
function Y() {
return (function(f) {
return f(f);
})(function(f) {
return fact(function(x) {
return f(f)(x);
});
});
}
var factorial = Y();
まだ参照されている唯一の外部名はですがfact
、これも簡単にパラメーター化でき、完全で汎用的なソリューションを作成していることは明らかです。
function Y(le) {
return (function(f) {
return f(f);
})(function(f) {
return le(function(x) {
return f(f)(x);
});
});
}
var factorial = Y(function(recurse) {
return function(n) {
return n == 0 ? 1 : n * recurse(n - 1);
};
});
recurser
。それが何をしているのか、その理由は少しもわかりません。
recurser
関数は、fact
名前で自分自身を参照することのない再帰バージョンを提供するため、この目標への最初のステップです。
function Y(recurse) { return recurse(recurse); } let factorial = Y(creator => value => { return value == 0 ? 1 : value * creator(creator)(value - 1); });
。そして、これは私がそれを消化する方法です(それが正しいかどうかはわかりません):関数を明示的に参照しないことで(コンビネータとしては許可されません)、2つの部分的に適用/カリー化された関数(作成者関数と計算関数)を使用できます計算関数の名前を必要とせずに再帰を実現するラムダ/無名関数を作成できるものはどれですか。
上記の回答のほとんどは、Yコンビネータは何を説明され、それが何であるかではなく、ために。
固定小数点コンビネータは、ラムダ計算が完全に処理されていることを示すために使用されます。これは計算理論において非常に重要な結果であり、関数型プログラミングの理論的基盤を提供します。
固定小数点コンビネーターを研究することも、関数型プログラミングを本当に理解するのに役立ちました。私は実際のプログラミングでそれらの使用を見つけたことはありません。
JavaScriptの y 結合子:
var Y = function(f) {
return (function(g) {
return g(g);
})(function(h) {
return function() {
return f(h(h)).apply(null, arguments);
};
});
};
var factorial = Y(function(recurse) {
return function(x) {
return x == 0 ? 1 : x * recurse(x-1);
};
});
factorial(5) // -> 120
編集:私はコードを見ることから多くを学びますが、これは背景なしに飲み込むのが少し難しいです-それについては申し訳ありません。他の回答によって提示されたいくつかの一般的な知識を使用して、何が起こっているのかを特定することができます。
Y関数は「yコンビネーター」です。var factorial
Yが使用されている行を見てみましょう。recurse
内部の関数でも後で使用されるパラメーター(この例では)を持つ関数を渡したことに注意してください。パラメーター名は基本的に、内部関数の名前になり、再帰呼び出しを実行できるようになります(recurse()
その定義で使用されるため)。y-combinatorは、匿名の内部関数を、渡された関数のパラメーター名に関連付ける魔法を実行しますY.
arguments.callee
:strictモードでは許可されていませんdeveloper.mozilla.org/en/JavaScript/...
(function fact(n){ return n <= 1? 1 : n * fact(n-1); })(5)
関数型プログラミングに詳しくなく、今は始めたくないが、少し興味があるプログラマー向け:
Yコンビネータは、関数に名前を付けることはできないが、引数として渡したり、戻り値として使用したり、他の関数内で定義したりできる状況で、再帰を実装できる式です。
関数を引数として自分自身に渡すことで機能するため、自分自身を呼び出すことができます。
これはラムダ計算の一部です。ラムダ計算は、実際には数学ですが、事実上プログラミング言語であり、コンピューターサイエンス、特に関数型プログラミングの基本です。
プログラミング言語では関数に名前を付ける傾向があるため、Yコンビネーターの日常的な実用的な価値は限られています。
警察のラインナップで識別する必要がある場合は、次のようになります。
Y =λf。(λx.f(xx))(λx.f(xx))
繰り返されるため、通常はそれを見つけることができ(λx.f (x x))
ます。
λ
シンボルは、その名の結石ラムダを与えるギリシャ文字のラムダ、であり、多くのがあります(λx.t)
それが何のようなラムダ計算のルックスだからスタイルの用語が。
U x = x x
、Y = U . (. U)
(表記ハスケル状乱用)。IOW、適切なコンビネーター付きY = BU(CBU)
。このように、Yf = U (f . U) = (f . U) (f . U) = f (U (f . U)) = f ((f . U) (f . U))
。
固定小数点コンビネーターは、fix
定義により同値を満たす高次関数です
forall f. fix f = f (fix f)
fix f
x
固定小数点方程式の解を表します
x = f x
自然数の階乗は、
fact 0 = 1
fact n = n * fact (n - 1)
を使用してfix
、一般的な/μ再帰関数に対する任意の構成的証明を、不特定の自己参照なしに導出できます。
fact n = (fix fact') n
どこ
fact' rec n = if n == 0
then 1
else n * rec (n - 1)
そのような
fact 3
= (fix fact') 3
= fact' (fix fact') 3
= if 3 == 0 then 1 else 3 * (fix fact') (3 - 1)
= 3 * (fix fact') 2
= 3 * fact' (fix fact') 2
= 3 * if 2 == 0 then 1 else 2 * (fix fact') (2 - 1)
= 3 * 2 * (fix fact') 1
= 3 * 2 * fact' (fix fact') 1
= 3 * 2 * if 1 == 0 then 1 else 1 * (fix fact') (1 - 1)
= 3 * 2 * 1 * (fix fact') 0
= 3 * 2 * 1 * fact' (fix fact') 0
= 3 * 2 * 1 * if 0 == 0 then 1 else 0 * (fix fact') (0 - 1)
= 3 * 2 * 1 * 1
= 6
この正式な証明は
fact 3 = 6
書き直しに固定小数点コンビネーター同値を系統的に使用する
fix fact' -> fact' (fix fact')
型なしラムダ計算の定式化は、文脈自由文法で構成されてい
E ::= v Variable
| λ v. E Abstraction
| E E Application
where v
変数の範囲と、ベータおよびイータ削減ルール
(λ x. B) E -> B[x := E] Beta
λ x. E x -> E if x doesn’t occur free in E Eta
ベータリダクションx
は、抽象(「関数」)本体内の変数のすべての自由発生をB
式(「引数」)で置き換えますE
。Etaの削減により、冗長な抽象化が排除されます。形式から省略されることもあります。既約全く削減ルールが適用されない先の式は、である通常のまたは標準的な形式。
λ x y. E
の略記です
λ x. λ y. E
(抽象多分岐)、
E F G
の略記です
(E F) G
(アプリケーション左結合性)、
λ x. x
そして
λ y. y
あるアルファ同等。
抽象化とアプリケーションは、ラムダ計算の2つの唯一の「言語プリミティブ」ですが、それらは任意に複雑なデータと操作のエンコードを可能にします。
教会の数字は、Peano-axiomatic naturalsに似た自然数のエンコーディングです。
0 = λ f x. x No application
1 = λ f x. f x One application
2 = λ f x. f (f x) Twofold
3 = λ f x. f (f (f x)) Threefold
. . .
SUCC = λ n f x. f (n f x) Successor
ADD = λ n m f x. n f (m f x) Addition
MULT = λ n m f x. n (m f) x Multiplication
. . .
正式な証明
1 + 2 = 3
ベータ削減の書き換えルールを使用:
ADD 1 2
= (λ n m f x. n f (m f x)) (λ g y. g y) (λ h z. h (h z))
= (λ m f x. (λ g y. g y) f (m f x)) (λ h z. h (h z))
= (λ m f x. (λ y. f y) (m f x)) (λ h z. h (h z))
= (λ m f x. f (m f x)) (λ h z. h (h z))
= λ f x. f ((λ h z. h (h z)) f x)
= λ f x. f ((λ z. f (f z)) x)
= λ f x. f (f (f x)) Normal form
= 3
ラムダ計算では、結合子は自由変数を含まない抽象です。最も単純に:I
アイデンティティーコンビネーター
λ x. x
恒等関数に同型
id x = x
このようなコンビネーターは、SKIシステムのようなコンビネーター結石のプリミティブオペレーターです。
S = λ x y z. x z (y z)
K = λ x y. x
I = λ x. x
ベータ削減は強く正規化されていません。ベータ還元では、すべての還元可能な表現「redexes」が通常の形式に収束するわけではありません。簡単な例は、オメガω
コンビネーターの分岐アプリケーションです。
λ x. x x
それ自体に:
(λ x. x x) (λ y. y y)
= (λ y. y y) (λ y. y y)
. . .
= _|_ Bottom
左端の部分式(「ヘッド」)の削減が優先されます。適用順は、置換前に引数を正規化しますが、通常の順序はしません。2つの戦略は、Cなどの熱心な評価とHaskellなどの遅延評価に類似しています。
K (I a) (ω ω)
= (λ k l. k) ((λ i. i) a) ((λ x. x x) (λ y. y y))
熱心な適用順序ベータ削減の下で発散
= (λ k l. k) a ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ y. y y) (λ y. y y))
. . .
= _|_
で以来、厳格なセマンティクス
forall f. f _|_ = _|_
しかし、怠惰な通常次のベータ削減の下で収束します
= (λ l. ((λ i. i) a)) ((λ x. x x) (λ y. y y))
= (λ l. a) ((λ x. x x) (λ y. y y))
= a
式が正規形の場合、正規次数のベータ削減で見つけられます。
固定小数点コンビネーターの本質的な特性Y
λ f. (λ x. f (x x)) (λ x. f (x x))
によって与えられます
Y g
= (λ f. (λ x. f (x x)) (λ x. f (x x))) g
= (λ x. g (x x)) (λ x. g (x x)) = Y g
= g ((λ x. g (x x)) (λ x. g (x x))) = g (Y g)
= g (g ((λ x. g (x x)) (λ x. g (x x)))) = g (g (Y g))
. . . . . .
同等
Y g = g (Y g)
に同型である
fix f = f (fix f)
型なしラムダ計算は、一般/μ再帰関数を介した任意の構成的証明をエンコードできます。
FACT = λ n. Y FACT' n
FACT' = λ rec n. if n == 0 then 1 else n * rec (n - 1)
FACT 3
= (λ n. Y FACT' n) 3
= Y FACT' 3
= FACT' (Y FACT') 3
= if 3 == 0 then 1 else 3 * (Y FACT') (3 - 1)
= 3 * (Y FACT') (3 - 1)
= 3 * FACT' (Y FACT') 2
= 3 * if 2 == 0 then 1 else 2 * (Y FACT') (2 - 1)
= 3 * 2 * (Y FACT') 1
= 3 * 2 * FACT' (Y FACT') 1
= 3 * 2 * if 1 == 0 then 1 else 1 * (Y FACT') (1 - 1)
= 3 * 2 * 1 * (Y FACT') 0
= 3 * 2 * 1 * FACT' (Y FACT') 0
= 3 * 2 * 1 * if 0 == 0 then 1 else 0 * (Y FACT') (0 - 1)
= 3 * 2 * 1 * 1
= 6
(乗算遅延、合流)
チャーチの型付けされていないラムダ計算では、に加えて、再帰的に列挙可能な無限大の固定小数点結合子が存在することが示されていY
ます。
X = λ f. (λ x. x x) (λ x. f (x x))
Y' = (λ x y. x y x) (λ y x. y (x y x))
Z = λ f. (λ x. f (λ v. x x v)) (λ x. f (λ v. x x v))
Θ = (λ x y. y (x x y)) (λ x y. y (x x y))
. . .
通常次数のベータリダクションは、拡張されていない型指定のないラムダ計算をチューリング完全な書き換えシステムにします。
Haskellでは、固定小数点コンビネーターをエレガントに実装できます
fix :: forall t. (t -> t) -> t
fix f = f (fix f)
すべての部分式が評価される前に、Haskellの遅延は有限に正規化されます。
primes :: Integral t => [t]
primes = sieve [2 ..]
where
sieve = fix (\ rec (p : ns) ->
p : rec [n | n <- ns
, n `rem` p /= 0])
λ x . x
ネーターです。今日はどうですか?
Y-CombinatorとFactorial関数のJavaScript実装を以下に示します(Douglas Crockfordの記事、http://javascript.crockford.com/little.htmlから入手可能)。
function Y(le) {
return (function (f) {
return f(f);
}(function (f) {
return le(function (x) {
return f(f)(x);
});
}));
}
var factorial = Y(function (fac) {
return function (n) {
return n <= 2 ? n : n * fac(n - 1);
};
});
var number120 = factorial(5);
Yコンビネータは、フラックスコンデンサの別名です。
私はClojureとSchemeの両方でYコンビネーターに一種の「バカガイド」を書いて、自分がそれに慣れるのを手助けしました。彼らは「リトル・スキーマー」の素材に影響されています
スキーム:https : //gist.github.com/z5h/238891
またはClojure:https : //gist.github.com/z5h/5102747
どちらのチュートリアルもコメントが散在しているコードであり、お気に入りのエディターにカットアンドペーストできる必要があります。
コンビネーターの初心者として、Mike Vanierの記事を見つけました(Nicholas Mancusoに感謝)が本当に役立つと思いました。私の理解を文書化することに加えて、私は要約を書きたいと思います、それが他の人に役立つかもしれないならば、私はとても嬉しいでしょう。
例として階乗を使用して、次のalmost-factorial
関数を使用して数値の階乗を計算しますx
。
def almost-factorial f x = if iszero x
then 1
else * x (f (- x 1))
上記の疑似コードでalmost-factorial
は、関数f
と数値を受け取りますx
(almost-factorial
カリー化されているため、関数を受け取りf
、1 値関数を返すと見なすことができます)。
のalmost-factorial
階乗をx
計算する場合、階乗の計算x - 1
を機能するように委任し、f
その結果を累積しますx
(この場合、(x-1)の結果にxを乗算します)。
見ることができるalmost-factorial
取り込む安っぽい(わずか数まで計算することができる階乗関数のバージョンx - 1
)と返しあまり安っぽい(数まで計算階乗のバージョンx
)。このフォームのように:
almost-factorial crappy-f = less-crappy-f
階乗の少ないバージョンの階乗を繰り返し渡すと、almost-factorial
最終的に目的の階乗関数が得られますf
。それは次のように考えることができます:
almost-factorial f = f
それがalmost-factorial f = f
意味するのf
は、関数の不動点ですalmost-factorial
。
これは、上記の関数の関係を見るのに非常に興味深い方法であり、私にとってはああの瞬間でした。(もしまだならフィックスポイントのマイクの投稿を読んでください)
一般化するために、我々は非再帰的機能fn
(私たちのほとんど-階乗など)を、私たちはその持っている固定小数点機能fr
(当社Fなど)、そしてどのようなY
あなたが与える時に行うことはあるY
fn
、Y
の修正点関数を返しますfn
。
つまり、要約すると(fr
パラメータを1つだけ取ると仮定して簡略化され、再帰x
的にx - 1
、x - 2
...に縮退します):
fn
:def fn fr x = ...accumulate x with result from (fr (- x 1))
、これはほとんど、便利な機能-私たちが使用することはできませんが、fn
上で直接x
、それは非常にすぐに有用であろう。この非再帰fn
は、関数fr
を使用して結果を計算しますfn fr = fr
、fr
の修正点でfn
、fr
ある便利な funciton、我々は使用することができるfr
上x
、当社の結果を得るためにY fn = fr
、Y
関数の固定小数点を返し、ほぼ有用な関数を有用なものにY
変えますfn
fr
Y
(含まれていません)の導出はスキップしY
て理解にいきますY
。Mike Vainerの投稿には多くの詳細があります。
Y
Y
次のように定義されます(ラムダ微積分形式):
Y f = λs.(f (s s)) λs.(f (s s))
s
関数の左側の変数を置き換えると、
Y f = λs.(f (s s)) λs.(f (s s))
=> f (λs.(f (s s)) λs.(f (s s)))
=> f (Y f)
つまり、の結果(Y f)
はの不動点ですf
。
(Y f)
ですか?署名を依存しf
、(Y f)
簡素化するために、任意のアリティの関数とすることができる、のは想定してみましょう(Y f)
だけ私たちの階乗関数のように、一つのパラメータを取ります。
def fn fr x = accumulate x (fr (- x 1))
以降fn fr = fr
、継続
=> accumulate x (fn fr (- x 1))
=> accumulate x (accumulate (- x 1) (fr (- x 2)))
=> accumulate x (accumulate (- x 1) (accumulate (- x 2) ... (fn fr 1)))
再帰計算(fn fr 1)
は、最も内側が基本ケースであり、計算でfn
使用さfr
れない場合に終了します。
Y
もう一度見て:
fr = Y fn = λs.(fn (s s)) λs.(fn (s s))
=> fn (λs.(fn (s s)) λs.(fn (s s)))
そう
fr x = Y fn x = fn (λs.(fn (s s)) λs.(fn (s s))) x
私にとって、このセットアップの魔法の部分は次のとおりです。
fn
そしてfr
相互に依存します:内側でfr
「ラップ」fn
し、fr
計算x
に使用されるたびに、「スポーン」(「リフト」?)し、fn
それに計算を委任しますfn
(それ自体fr
を渡しx
ます)。一方、より小さな問題の結果の計算にfn
依存しfr
て使用fr
しますx-1
。fr
定義に使用されている時点ではfn
(その操作でfn
使用する場合fr
)、実数fr
はまだ定義されていません。fn
、実際のビジネス・ロジックを定義します。基づいてfn
、Y
作成するfr
-特定の形式のヘルパー機能は-の計算を容易にするためfn
に、再帰的方法。Y
現時点でこの方法を理解するのに役立ちました。
ところで、私は「ラムダ計算による関数型プログラミング入門」という本も非常に優れていることを知りました。私はそれを一部に過ぎず、この本に頭を悩ませることができなかったという事実Y
が私をこの投稿に導きました。
Nicholas Mancusoの回答で言及された記事(これは読む価値があります)から編集され た元の質問に対する回答と、その他の回答です。
Yコンビネーターとは何ですか?
Y-combinatorは「関数型」(またはより高次の関数—他の関数を操作する関数)であり、単一の引数を取ります。これは再帰的ではない関数であり、次の関数のバージョンを返します再帰的。
やや再帰的な=)ですが、より詳細な定義:
コンビネータ—自由変数のない単なるラムダ式です。
自由変数—バインドされた変数ではない変数です。
バインドされた変数—引数の1つとしてその変数名を持つラムダ式の本体内に含まれる変数。
これについて考えるもう1つの方法は、combinatorがラムダ式であり、combinatorの名前をその定義がある場所でその定義に置き換えることができ、すべてがまだ機能することです(combinatorが無限ループに入る場合)ラムダ本体内のそれ自体への参照を含みます)。
Y-combinatorは固定小数点コンビネーターです。
関数の固定小数点は、関数によってそれ自体にマップされる関数のドメインの要素です。
つまり、あるc
関数の固定小数点であるf(x)
場合f(c) = c
、この手段f(f(...f(c)...)) = fn(c) = c
コンビネーターはどのように機能しますか?
以下の例では、強い型付けと動的な型付けを想定しています。
遅延(通常順)Y結合子:
この定義は、遅延評価(また、遅延、必要に応じて呼び出す)を使用する言語に適用されます—値が必要になるまで式の評価を遅らせる評価戦略。
Y = λf.(λx.f(x x)) (λx.f(x x)) = λf.(λx.(x x)) (λx.f(x x))
つまり、特定の関数f
(非再帰関数)の場合、対応する再帰関数はλx.f(x x)
、最初にを計算し、次にこのラムダ式をそれ自体に適用することで取得できます。
厳密な(適用順序)Y結合子:
この定義は、厳密な(また、熱心で貪欲な)評価を持つ言語に適用されます—式が変数にバインドされるとすぐに評価される評価戦略。
Y = λf.(λx.f(λy.((x x) y))) (λx.f(λy.((x x) y))) = λf.(λx.(x x)) (λx.f(λy.((x x) y)))
それはその性質において怠惰なものと同じです、それはλ
ラムダのボディ評価を遅らせる追加のラッパーを持っているだけです。このトピックに多少関係のある別の質問をしました。
彼らは何のために良いのですか?
盗まれたがから借りクリスAmmermanによって解答問題の関数の実際の作業からそれを分離することにより、その実装を抽象化し、そして、Yコンビネータの一般化再帰:。
Yコンビネーターにはいくつかの実用的なアプリケーションがありますが、それは主に理論的な概念であり、それを理解することで全体的なビジョンが広がり、おそらく分析スキルと開発者スキルが向上します。
それらは手続き型言語で役立ちますか?
マイク・バニエで述べた:「多くの静的型付け言語でYコンビネータを定義することは可能であるが、(少なくとも、私が見てきた例で)Yコンビネータ自体はdoesnのため、このような定義は通常、いくつかの非自明なタイプのハックが必要ですt単純な静的型があります。それはこの記事の範囲を超えているので、これ以上は触れません
そして、Chris Ammermanが述べたように、ほとんどの手続き型言語には静的型付けがあります。
だから、これに答えてください。
y-combinatorは匿名の再帰を実装します。だから代わりに
function fib( n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }
できるよ
function ( fib, n ){ if( n<=1 ) return n; else return fib(n-1)+fib(n-2) }
もちろん、y-combinatorは名前による呼び出し言語でのみ機能します。これを通常の値による呼び出し言語で使用する場合は、関連するz-combinatorが必要になります(y-combinatorは分岐/無限ループします)。
これに答える最良の方法は、JavaScriptなどの言語を選択することです。
function factorial(num)
{
// If the number is less than 0, reject it.
if (num < 0) {
return -1;
}
// If the number is 0, its factorial is 1.
else if (num == 0) {
return 1;
}
// Otherwise, call this recursive procedure again.
else {
return (num * factorial(num - 1));
}
}
関数内の関数の名前を使用しないように書き直しますが、それでも再帰的に呼び出します。
関数名factorial
が表示される唯一の場所は、呼び出しサイトです。
ヒント:関数の名前は使用できませんが、パラメーターの名前は使用できます。
問題を解決します。調べないでください。それを解くと、y-combinatorがどのような問題を解決するかがわかります。