クロックフォードの「カレー」メソッドで「これ」が無効になる理由はありますか?
-
20-12-2019 - |
質問
ダグラス・クロックフォードの著書「JavaScript:良い部分」のコードを提供しています。 curry
関数と引数を受け取り、既に追加された引数を含むその関数を返すメソッド (どうやら、これは実際にはそうではありません) 「カレー」とはどういう意味ですか, 、しかし、の例です 「部分申請」)。これは、彼が作成した他のカスタム コードなしで動作するように私が変更したコードです。
Function.prototype.curry = function(){
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;
return function() {
// context set to null, which will cause `this` to refer to the window
return that.apply(null, args.concat(slice.apply(arguments)));
};
};
それで、あなたが持っているなら、 add
関数:
var add = function(num1, num2) {
return num1 + num2;
};
add(2, 4); // returns 6
すでに 1 つの引数を持つ新しい関数を作成できます。
var add1 = add.curry(1);
add1(2); // returns 3
それはうまくいきます。しかし、私が知りたいのは、なぜ彼が設定したのかということです this
に null
?期待される動作は、カリー化されたメソッドが元のメソッドと同じであり、同じものを含むということではないでしょうか。 this
?
私のバージョンのカレーは次のようになります。
Function.prototype.myCurry = function(){
var slice = [].slice,
args = slice.apply(arguments),
that = this;
return function() {
// context set to whatever `this` is when myCurry is called
return that.apply(this, args.concat(slice.apply(arguments)));
};
};
例
var calculator = {
history: [],
multiply: function(num1, num2){
this.history = this.history.concat([num1 + " * " + num2]);
return num1 * num2;
},
back: function(){
return this.history.pop();
}
};
var myCalc = Object.create(calculator);
myCalc.multiply(2, 3); // returns 6
myCalc.back(); // returns "2 * 3"
ダグラス・クロックフォードのやり方でやろうとすると:
myCalc.multiplyPi = myCalc.multiply.curry(Math.PI);
myCalc.multiplyPi(1); // TypeError: Cannot call method 'concat' of undefined
私のやり方でやると:
myCalc.multiplyPi = myCalc.multiply.myCurry(Math.PI);
myCalc.multiplyPi(1); // returns 3.141592653589793
myCalc.back(); // returns "3.141592653589793 * 1"
しかし、ダグラス・クロックフォードが自分のやり方でやったのであれば、おそらく彼には正当な理由があるのではないかと私は感じています。私には何が欠けているのでしょうか?
解決
理由1 - 一般的な解決策を提供するのは簡単ではありません
問題はあなたの解決策が一般的ではないということです。発信者が新しい関数を任意のオブジェクトに割り当てていないか、それを完全に異なるオブジェクトに割り当てると、multiplyPi
関数は機能を停止します。
var multiplyPi = myCalc.multiply.myCurry(Math.PI);
multiplyPi(1); // TypeError: this.history.concat is not a function
.
だから、Crockfordのどちらもあなたの解決策も機能が正しく使用されることを保証することができます。その後、curry
関数が "関数"ではなく "Methods"でのみ機能し、this
をnull
に設定してそれを強制的に設定することがより簡単かもしれません。 Crockfordはその本では言及していないので、私たちは推測するかもしれません。
理由2 - 機能が説明されている
「Crockfordはなぜこれを使用しなかったのか」を尋ねる場合 - 非常に可能性の高い答えは次のとおりです。「実証事項に関して重要ではありませんでした」 Crockfordはこの例を使用しています。 関数。サブ章のcurry
の目的は次のとおりです。
- その関数がオブジェクトであることを示すために を作成して操作することができます
- クロージャーの別の使用法を実証する
- 引数を操作できる方法を示すために。
オブジェクトを含む一般的な使用方法については、この章の目的ではありませんでした。不可能でもない場合は問題があるので(理由1を参照)、実際に働くかどうかにかかわらず問題を引き起こす可能性があるがある場合は、が登場することができます。あなたの場合: - ))。
結論
それは言った、私はあなたがあなたの解決策に完全に自信を持っていることができると思います! CrockFordsの決定に従って、null
をthis
にリセットすることを決定するための特に理由はありません。 あなたの解決策は特定の状況下でのみ機能し、100%クリーンではありません。その後、「オブジェクト指向」解決策を清掃するには、オブジェクトに依頼して、そのメソッドが同じオブジェクト内に留まるようにするためにそのメソッドをそれ自体の中でその方法のクローンを作成するように依頼します。
他のヒント
読者は注意してください、あなたは恐ろしいことになります。
JavaScript のカリー化、関数、部分アプリケーション、オブジェクト指向に関しては、話すべきことがたくさんあります。この回答はできるだけ短くするつもりですが、議論すべきことはたくさんあります。したがって、私は記事をいくつかのセクションに構成し、すべてを読むのが待ちきれない人のために、各セクションの最後に各セクションを要約しました。
1.カレーにするか否か
ハスケルについて話しましょう。Haskell では、すべての関数がデフォルトでカリー化されます。たとえば、 add
Haskell では次のように関数します。
add :: Int -> Int -> Int
add a b = a + b
型シグネチャに注目してください Int -> Int -> Int
?だということだ add
かかります Int
型の関数を返します Int -> Int
それは今度は Int
そして、 Int
. 。これにより、Haskell の関数を部分的に簡単に適用できます。
add2 :: Int -> Int
add2 = add 2
同じ関数を JavaScript で使用すると、見た目は醜くなります。
function add(a) {
return function (b) {
return a + b;
};
}
var add2 = add(2);
ここでの問題は、JavaScript の関数がデフォルトではカリー化されていないことです。手動でカレーをかける必要があり、面倒です。したがって、部分適用 (別名) を使用します。 bind
) その代わり。
レッスン1: カリー化は、関数を部分的に適用しやすくするために使用されます。ただし、これは関数がデフォルトでカリー化されている言語でのみ有効です (例:ハスケル)。関数を手動でカリー設定する必要がある場合は、代わりに部分適用を使用することをお勧めします。
2.関数の構造
Haskell にはアンカリー関数も存在します。これらは「通常の」プログラミング言語の関数のように見えます。
main = print $ add(2, 3)
add :: (Int, Int) -> Int
add(a, b) = a + b
次のコマンドを使用すると、関数をカリー形式から非カリー形式に変換したり、その逆に変換したりできます。 uncurry
そして curry
それぞれ Haskell の関数です。Haskell のアンカリー関数は引数を 1 つだけ取ります。ただし、その引数は複数の値の積です (つまり、ある 製品の種類).
同様に、JavaScript の関数も引数を 1 つだけ受け取ります (まだ引数を認識していないだけです)。その引数は製品タイプです。の arguments
関数内の値は、その製品タイプの表現です。これを例示すると、 apply
製品タイプを受け取り、それに関数を適用する JavaScript のメソッド。例えば:
print(add.apply(null, [2, 3]));
JavaScript の上記の行と Haskell の次の行の類似点がわかりますか?
main = print $ add(2, 3)
への割り当てを無視する main
それが何のためにあるのか分からない場合。それは当面のトピックとは無関係です。重要なことはタプルです。 (2, 3)
Haskell では配列と同型です [2, 3]
JavaScriptで。このことから何を学べるでしょうか?
の apply
JavaScript の関数は関数の適用 (または $
) ハスケルでは:
($) :: (a -> b) -> a -> b
f $ a = f a
型の関数を取ります a -> b
そしてそれを type の値に適用します a
type の値を取得するには b
. 。ただし、JavaScript のすべての関数はデフォルトではカリー化されていないため、 apply
関数は常に積の型を取ります (つまり、配列) を 2 番目の引数として使用します。つまり、 type の値は a
は実際には JavaScript の製品タイプです。
レッスン 2: JavaScript のすべての関数は、積の型である 1 つの引数のみを取ります (つまり、の arguments
価値)。これが意図されたものなのか偶然なのかは推測の域を出ません。ただし、重要な点は、数学的にはすべての関数が 1 つの引数のみを取ることを理解していることです。
数学的には、関数は次のように定義されます。 射: a -> b
. 。type の値を取ります a
type の値を返します b
. 。射は引数を 1 つだけ持つことができます。複数の引数が必要な場合は、次のいずれかを実行できます。
- 別の射を返します (つまり、
b
は別の射です)。これはカレー作りです。ハスケルはこれを行います。 - 定義する
a
複数のタイプの製品であること (すなわち、a
は製品タイプです)。JavaScript がこれを行います。
2 つの関数のうち、私は部分的な適用が簡単になるカリー関数を好みます。「アンカリー」関数の部分的な適用はより複雑です。言っておきますが、難しいことではありませんが、より複雑なだけです。これが、私が JavaScript よりも Haskell を好む理由の 1 つです。関数はデフォルトでカリー化されます。
3.OOP が重要ではない理由
JavaScript のオブジェクト指向コードをいくつか見てみましょう。例えば:
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filter(odd).length;
function odd(n) {
return n % 2 !== 0;
}
ここで、これがどのようにオブジェクト指向なのか疑問に思うかもしれません。関数型コードのように見えます。結局のところ、Haskell でも同じことができます。
oddities = length . filter odd $ [0..9]
それにもかかわらず、上記のコードはオブジェクト指向です。配列リテラルはメソッドを持つオブジェクトです filter
新しい配列オブジェクトを返します。次に、単純にアクセスします length
新しい配列オブジェクトの。
このことから何を学べるでしょうか?オブジェクト指向言語での操作の連鎖は、関数型言語で関数を作成することと同じです。唯一の違いは、関数コードが逆方向に読み取られることです。その理由を見てみましょう。
JavaScript では、 this
パラメータが特殊です。これは関数の仮パラメータとは別のものであるため、関数で値を個別に指定する必要があります。 apply
方法。なぜなら this
仮パラメータの前にある場合、メソッドは左から右に連鎖します。
add.apply(null, [2, 3]); // this comes before the formal parameters
もし this
仮パラメータの後に来る場合、上記のコードは次のようになります。
var oddities = length.filter(odd).[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
apply([2, 3], null).add; // this comes after the formal parameters
あまり良くないですよね?では、なぜ Haskell の関数は逆方向に読み取るのでしょうか?答えはカレーです。Haskell の関数にも "this
"パラメータ。ただし、JavaScript とは異なり、 this
Haskell のパラメータは特別なものではありません。さらに、引数リストの最後に来ます。例えば:
filter :: (a -> Bool) -> [a] -> [a]
の filter
関数は述語関数と this
list を作成し、フィルターされた要素のみを含む新しいリストを返します。では、なぜ、 this
パラメータは最後ですか?部分的な適用が容易になります。例えば:
filterOdd = filter odd
oddities = length . filterOdd $ [0..9]
JavaScript では次のように書きます。
Array.prototype.filterOdd = [].filter.myCurry(odd);
var oddities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].filterOdd().length;
さて、どれを選びますか?逆から読むことにまだ不満があるなら、お知らせがあります。次のように「逆方向適用」と「逆方向合成」を使用して、Haskell コードを前方に読み込むようにできます。
($>) :: a -> (a -> b) -> b
a $> f = f a
(>>>) :: (a -> b) -> (b -> c) -> (a -> c)
f >>> g = g . f
oddities = [0..9] $> filter odd >>> length
これで、両方の長所を利用できるようになりました。コードは順方向に読み取られ、カリー化の利点をすべて享受できます。
問題がたくさんあります this
関数型言語では起こらないこと:
- の
this
パラメータは特殊化されています。他のパラメータとは異なり、単純に任意のオブジェクトに設定することはできません。したがって、使用する必要がありますcall
別の値を指定するにはthis
. - JavaScript で部分的に関数を適用したい場合は、次のように指定する必要があります。
null
の最初のパラメータとしてbind
. 。同様に、call
そしてapply
.
オブジェクト指向プログラミングは何の関係もありません this
. 。実際、Haskell でもオブジェクト指向コードを書くことができます。Haskell は実際にはオブジェクト指向プログラミング言語であり、その点では Java や C++ よりもはるかに優れているとまで言えます。
レッスン 3: 関数型プログラミング言語は、ほとんどの主流のオブジェクト指向プログラミング言語よりもオブジェクト指向です。実際、JavaScript のオブジェクト指向コードは、関数型スタイルで記述した方が (確かに読みにくくなりますが) 優れています。
JavaScript のオブジェクト指向コードの問題は、 this
パラメータ。私の謙虚な意見では、 this
パラメータは、仮パラメータと何ら異なる扱いをすべきではありません (Lua はこれを正しく理解しています)。問題点 this
それは:
- 設定する方法はありません
this
他の仮パラメータと同様に。使用する必要がありますcall
その代わり。 - 設定する必要があります
this
にnull
でbind
機能を部分的にのみ適用したい場合。
余談ですが、この記事の各セクションが前のセクションよりも長くなっていることに今気づきました。したがって、次の (そして最後の) セクションをできるだけ短くすることを約束します。
4.ダグラス・クロックフォードを擁護
ここまでで、JavaScript のほとんどが壊れているので、代わりに Haskell に移行する必要があると私が考えていることを理解したはずです。私は、Douglas Crockford も関数型プログラマーであり、JavaScript を修正しようとしていると信じたいと思っています。
彼が関数型プログラマーであることをどうやって知ることができますか?彼は次のような男です。
- と同等の機能を普及させた
new
キーワード (別名Object.create
)。まだ実行していない場合は、実行する必要があります の使用をやめてくださいnew
キーワード. - という概念を説明しようとしました モナドと生殖腺 JavaScript コミュニティへ。
とにかく、クロックフォードは無効になったと思う this
の中に curry
彼はどれほど悪いことを知っているので機能します this
は。以外の値に設定するのは冒涜になります。 null
「JavaScript:良い部分」。彼は一度に 1 つの機能を使って世界をより良い場所にしていると思います。
無効化することで this
クロックフォードは、それに依存するのをやめるように迫っています。
編集: Bergi のリクエストに応じて、オブジェクト指向を記述するためのより機能的な方法を説明します。 Calculator
コード。クロックフォードのものを使用します curry
方法。まずは、 multiply
そして back
機能:
function multiply(a, b, history) {
return [a * b, [a + " * " + b].concat(history)];
}
function back(history) {
return [history[0], history.slice(1)];
}
ご覧のとおり、 multiply
そして back
関数はどのオブジェクトにも属しません。したがって、どの配列でも使用できます。特にあなたの Calculator
class は文字列リストの単なるラッパーです。したがって、別のデータ型を作成する必要さえありません。したがって、次のようになります。
var myCalc = [];
これで Crockford を使用できるようになります curry
部分適用の方法:
var multiplyPi = multiply.curry(Math.PI);
次に作成します。 test
に機能する multiplyPi
1 つずつ減らして前の状態に戻すには、次のようにします。
var test = bindState(multiplyPi.curry(1), function (prod) {
alert(prod);
return back;
});
構文が気に入らない場合は、次のように切り替えることができます。 ライブスクリプト:
test = do
prod <- bindState multiplyPi.curry 1
alert prod
back
の bindState
関数は bind
状態モナドの関数。次のように定義されます。
function bindState(g, f) {
return function (s) {
var a = g(s);
return f(a[0])(a[1]);
};
}
それでは、テストしてみましょう:
alert(test(myCalc)[0]);
ここでデモをご覧ください: http://jsfiddle.net/5h5R9/
ところで、このプログラム全体が次のように LiveScript で記述されていれば、より簡潔になるでしょう。
multiply = (a, b, history) --> [a * b, [a + " * " + b] ++ history]
back = ([top, ...history]) -> [top, history]
myCalc = []
multiplyPi = multiply Math.PI
bindState = (g, f, s) -->
[a, t] = g s
(f a) t
test = do
prod <- bindState multiplyPi 1
alert prod
back
alert (test myCalc .0)
コンパイルされた LiveScript コードのデモをご覧ください。 http://jsfiddle.net/5h5R9/1/
では、このコードはどのようにオブジェクト指向になっているのでしょうか?ウィキペディアの定義 オブジェクト指向プログラミング として:
オブジェクト指向プログラミング (OOP) は、データ フィールド (オブジェクトを記述する属性) とメソッドと呼ばれる関連プロシージャを持つ「オブジェクト」として概念を表すプログラミング パラダイムです。オブジェクトは通常はクラスのインスタンスであり、相互に対話してアプリケーションやコンピューター プログラムを設計するために使用されます。
この定義によれば、Haskell のような関数型プログラミング言語はオブジェクト指向です。その理由は次のとおりです。
- Haskell では概念を次のように表します。 代数データ型 これらは本質的に「ステロイド上のオブジェクト」です。ADT には、0 個以上のデータ フィールドを持つことができる 1 つ以上のコンストラクターがあります。
- Haskell の ADT には関連する関数があります。ただし、主流のオブジェクト指向プログラミング言語とは異なり、ADT は関数を所有しません。代わりに、関数は ADT に特化しています。ADT はさらにメソッドを追加することに積極的であるため、これは実際には良いことです。Java や C++ などの従来の OOP 言語では、それらは閉じられています。
- ADT は、Java のインターフェイスに似たタイプクラスのインスタンスにすることができます。したがって、継承、分散、サブタイプのポリモーフィズムは依然として存在しますが、はるかに煩わしくない形式になります。例えば
Functor
のスーパークラスですApplicative
.
上記のコードもオブジェクト指向です。この場合のオブジェクトは、 myCalc
これは単なる配列です。それには次の 2 つの機能が関連付けられています。 multiply
そして back
. 。ただし、これらの関数は所有していません。ご覧のとおり、「関数型」オブジェクト指向コードには次の利点があります。
- オブジェクトはメソッドを所有しません。したがって、新しい関数をオブジェクトに関連付けることは簡単です。
- カリー化により部分的な適用が簡単になります。
- 汎用プログラミングを促進します。
それで、それが役に立ったことを願っています。
しかし、私が知りたいのは、なぜこれを null に設定したのかということです。
特に理由はありません。おそらく彼は単純化したかったのでしょう。カリー化または部分的に適用することに意味がある関数のほとんどは、次のような OOP メソッドではありません。 this
. 。より機能的なスタイルでは、 history
追加される配列は、関数の別のパラメーター (場合によっては戻り値) になります。
期待される動作は、カリー化されたメソッドが元のメソッドと同じであること (同じ this を含む) ではないでしょうか。
はい、実装はより合理的ですが、部分的に適用された関数を使用する場合、その関数を正しいコンテキストで (オブジェクトに再割り当てする場合のように) 呼び出す必要があるとは考えられないかもしれません。
そういった方は、こちらをご覧ください。 bind
方法 特定の機能を含む部分的なアプリケーション用の Function オブジェクトの this
-価値。
これの値は、楽しさへの呼び出しに提供されます。これに注意してください 方法によって見られる実際の値ではないかもしれません:メソッドがAの場合 非厳密モードコードの機能、NULLおよび未定義の場合は置き換えられます グローバルオブジェクトとプリミティブ値がボックス化されます。
したがって、メソッドが非厳密モードで、最初の引数がnull
またはundefined
の場合、そのメソッドの内部にthis
はWindow
を参照します。厳密モードでは、これはnull
またはundefined
です。私はこのfidsle
さらにnull
oR undefined
を渡しても、this
をまったく参照しない場合は害はありません。それが、Crockfordが彼の例でnull
を使用して、物事を過度に重複しないようにしました。