機能的なJavaScriptコードをどのように_read_できますか?


9

JavaScriptでの関数型プログラミングの基礎となる基本概念のいくつか/多く/ほとんどを学んだと思います。ただし、私は関数コード、特に自分で作成したコードを明確に読み取ることができず、誰かが役立つヒント、ヒント、ベストプラクティス、用語などを教えてくれるかどうか疑問に思っています。

以下のコードを見てください。私はこのコードを書きました。これは、say {a:1, b:2, c:3, d:3}との間の2つのオブジェクト間のパーセント類似性を割り当てることを目的としてい{a:1, b:1, e:2, f:2, g:3, h:5}ます。Stack Overflowでこの質問に答えてコードを作成しました。ポスターがどの程度の類似性について尋ねているのか正確にはわからなかったため、4つの異なる種類を用意しました。

  • 2番目にある1番目のオブジェクトのキーの割合、
  • 重複を含む、2番目にある1番目のオブジェクトの値のパーセント
  • 重複が許可されていない、2番目に見つかった1番目のオブジェクトの値の割合、および
  • 2番目のオブジェクトにある1番目のオブジェクトの{key:value}ペアの割合。

私は合理的に命令型のコードから始めましたが、これは関数型プログラミングに適した問題であることをすぐに理解しました。特に、私が比較しようとしている機能のタイプ(例えば、キーや値など)を定義する上記の4つの戦略のそれぞれについて関数または3つを抽出できれば、残りのコードを繰り返し可能な単位に減らす(言葉の遊びを許す)ことができます。あなたはそれを乾いた状態に保ちます。そこで関数型プログラミングに切り替えました。結果にはかなり誇りに思っており、それは適度にエレガントだと思います。

ただし、コードを自分で作成し、構築中にコードのすべての部分を理解したとしても、今振り返ると、特定の半行の読み方と読み方の両方に少し戸惑い続けています。コードの特定の半行が実際に行っていることを「グロッ」と言います。私は、すぐにスパゲッティの混乱に分解するさまざまな部分を接続するために精神的な矢を作ることに気づきました。

だから、誰かが私に、コードのより複雑なビットのいくつかを、簡潔で、私が読んでいるものの私の理解に貢献する方法で「読む」方法を教えてもらえますか?私を最も引き付ける部分は、複数の太い矢印が連続している部分、および/または複数の括弧が連続している部分だと思います。繰り返しになりますが、最終的にはロジックを理解することができますが、関数型JavaScriptプログラミングの行をすばやく明確かつ直接「取り込む」ためのより良い方法があります(私はそう思います)。

以下のコード行、または他の例を自由に使用してください。ただし、私からの最初の提案が必要な場合は、以下にいくつか示します。かなりシンプルなものから始めます。コードの終わり近くから、これがパラメーターとして関数に渡されますobj => key => obj[key]。それをどのように読んで理解しますか?より長い例は、最初から近い1つの完全な関数ですconst getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));。最後のmap部分は特に私を捕まえます。

ノート、この時点で時間に私はしてくださいないなどハスケルやシンボリック抽象記法やカリー化の基礎への参照を探している私は何をしています探しているコードの行を見ながら、私は口を静かにできることを、英語の文章です。具体的にそれについて具体的に言及している参考文献があればすばらしいですが、私はいくつかの基本的な教科書を読みに行くべきだと言う答えも探していません。私はそれを実行し、私は(少なくともかなりの量の)ロジックを取得しました。また、私は完全な答えは必要ありません(そのような試みは歓迎されますが)。そうでなければ面倒なコードの単一の特定の行を読むためのエレガントな方法を提供する短い答えでさえいただければ幸いです。

この質問の一部は次のとおりだと思います:関数コードを左から右へ、上から下へ直線的に読み取ることできますか?それとも、明らかに線形ではないコードのページにスパゲッティのような配線の精神的な画像を作成することを強いられていますか?それを行う必要がある場合でも、コードを読み取る必要があるので、線形テキストを取得してスパゲッティを配線するにはどうすればよいでしょうか。

任意のヒントをいただければ幸いです。

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25

回答:


18

この特定の例はあまり読みにくいため、ほとんどの場合それを読むのは困難です。意図された犯罪はなく、インターネットで見つけた非常に多くのサンプルもそうではありません。多くの人は週末に関数型プログラミングで遊んでいるだけで、実際に本番関数コードを長期間維持する必要はありません。私はそれを次のように書きます:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

何らかの理由で、多くの人々は、関数コードには大きなネストされた式の特定の審美的な「見た目」があるべきだという考えを頭に持っています。私のバージョンはセミコロンがすべて付いた命令型コードに多少似ていますが、すべてが不変なので、必要に応じてすべての変数を置き換えて1つの大きな式を取得できます。実際、スパゲッティバージョンと同じように「機能的」ですが、読みやすさが向上しています。

ここでは、式は非常に小さな断片に分割され、ドメインにとって意味のある名前が付けられています。ネストはmapObj、名前付き関数などに一般的な機能を組み込むことで回避されます。ラムダは、コンテキストで明確な目的を持つ非常に短い関数用に予約されています。

読みにくいコードに出会った場合は、簡単になるまでリファクタリングしてください。少し練習が必要ですが、それだけの価値があります。関数型コードは、命令型と同様に読みやすくすることができます。実際、通常はより簡潔であるため、多くの場合はそれ以上です。


間違いなく犯罪はとられません!私はまだ私が知っていることを維持しますが、いくつかの関数型プログラミングについての事を、多分についての質問での私の主張どのくらいの I knowが少しオーバー述べました。私は実は比較的初心者です。ですから、この私の特定の試みが、このように簡潔で明確でありながら機能的な方法でどのように書き換えられるかを見ると、金のように見えます...ありがとう。私はあなたの書き直しを注意深く研究します。
アンドリューウィレムス2017

1
長いチェーンやメソッドのネストを使用すると、不要な中間変数が排除されると聞いたことがあります。対照的に、あなたの答えは、適切な名前の中間変数を使用して、チェーン/ネストを中間のスタンドアロンステートメントに分割します。この場合、あなたのコードはもっと読みやすいと思いますが、あなたがどれほど一般的にしようとしているのかと思います。長いメソッドチェーンやディープネスティングは、多くの場合、または常に常に回避すべきアンチパターンであると言っていますか?それとも、それらが大きな利点をもたらす時があるのですか?そして、その質問に対する答えは、関数型コーディングと命令型コーディングで異なりますか?
アンドリューウィレムス2017

3
中間変数を削除すると明確さが増す場合がある特定の状況があります。たとえば、FPでは、配列へのインデックスはほとんど必要ありません。また、中間結果に優れた名前がない場合もあります。しかし、私の経験では、ほとんどの人は反対の方向に過ちを犯しがちです。
カールビーレフェルト

6

私はJavascriptで多くの高機能な作業を行っていません(これはそうだと思います-機能的なJavascriptについて話すほとんどの人はマップ、フィルター、および削減を使用している可能性があります、コードは独自の高レベル関数を定義してます)それよりも少し高度ですが)私はHaskellでそうしました、そして私は経験の少なくとも一部は翻訳すると思います。私が学んだことをいくつか紹介します。

関数のタイプを指定することは本当に重要です。Haskellでは、関数のタイプを指定する必要はありませんが、定義にタイプを含めると、はるかに読みやすくなります。JavaScriptは同じ方法で明示的な型指定をサポートしていませんが、型定義をコメントに含めない理由はありません。たとえば、次のとおりです。

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

このような型定義を扱う際に少し練習することで、関数の意味がより明確になります。

命名は重要であり、おそらく手続き型プログラミングよりも重要です。多くの関数型プログラムは、規則に重きをおいた非常に簡潔なスタイルで記述されています(たとえば、 'xs'はリスト/配列であり、 'x'はその中のアイテムであるという規則は非常に広まっています)。簡単に言えば、より詳細な名前を付けることをお勧めします。あなたが使用した特定の名前を見ると、「getX」は一種の不透明なので、「getXs」も実際にはあまり役に立ちません。「getXs」を「applyToProperties」のような名前にします。「getX」はおそらく「propertyMapper」になります。"getPctSameXs"は "percentPropertiesSameWith"( "with"になります。)。

別の重要なことは、慣用的なコード書くことです。構文a => b => some-expression-involving-a-and-bを使用してカレー関数を作成していることに気づきました。これは興味深いものであり、状況によっては役立つかもしれませんがここではカリー化された関数の恩恵を受けることは何もせず、代わりに従来の複数引数関数を使用する方がより慣用的なJavaScriptになります。こうすることで、何が起こっているのかが一目でわかりやすくなります。またconst name = lambda-expressionfunction name (args) { ... }代わりに使用する方が慣用的な関数の定義にも使用しています。私はそれらが意味的にわずかに異なることを知っていますが、それらの違いに頼らない限り、可能であればより一般的なバリアントを使用することをお勧めします。


5
タイプ+1!言語にそれらがないからといって、それらについて考える必要がないという意味ではありません。ECMAScriptのいくつかのドキュメンテーションシステムには、関数のタイプを記録するためのタイプ言語があります。いくつかのECMAScript IDEにも型言語があり(通常、主要なドキュメントシステムの型言語も理解しています)、それらの型注釈を使用して、基本的な型チェックとヒューリスティックヒンティングを実行することもできます。
イェルクWミッターク

型定義、意味のある名前、イディオムを使用して...ありがとうございます!考えられる多くのコメントのほんの一部です。カレー関数として特定の部分を書くつもりはありませんでした。私が書いている間に私のコードをリファクタリングしたとき、それらはちょうどそのように進化しました。それがいかに不要であったかがわかります。これらの2つの関数のパラメーターを1つの関数の2つのパラメーターにマージするだけでも、意味がわかるだけでなく、短いビットがすぐに読みやすくなります。
アンドリューウィレムス2017

@JörgWMittag、型の重要性に関するコメントと、あなたが書いた他の回答へのリンクに感謝します。私はWebStormを使用していますが、あなたの他の回答を読んだ方法によると、WebStormはjsdocのような注釈を解釈する方法を知っています。私はあなたのコメントから、jsdocとWebStormを一緒に使用して、命令コードだけでなく機能に注釈を付けることができると想定していますが、それをさらに詳しく知る必要があります。私は以前にjsdocで遊んだことがあり、WebStormと私はそこで協力できることを知っているので、その機能/アプローチをさらに使用することを期待しています。
Andrew Willems 2017

@Julesは、ちょうど私が上記の私のコメントにまで言及していたカリーどの機能を明確にする:あなたが示唆されるように、それぞれのインスタンスobj => key => ...に簡略化することができ(obj, key) => ...、後でので、getX(obj)(key)またのように簡略化することができますがget(obj, key)。対照的に、別のカリー化された関数は(getX, filter = vals => vals) => (objA, objB) => ...、少なくとも記述されている残りのコードのコンテキストでは、簡単に単純化することはできません。
アンドリューウィレムス2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.