web-dev-qa-db-ja.com

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

JavaScriptでの関数型プログラミングの基礎となる基本概念のいくつか/多く/ほとんどを学んだと思います。しかし、私は具体的に読み取り関数コード、私が記述したコードでさえも問題を抱えており、助けて。

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

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

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

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

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

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

この時点では、私はHaskellへの参照やシンボリックな抽象記法やカレーの基礎などを探しているnotではないことに注意してください。私が探しているものamは、コード行を見ながら黙って口にできる英語の文章です。具体的に正確に対応している参照がある場合それは素晴らしいですが、基本的な教科書を読みに行くべきだという答えも探していません。私はそれを実行し、私は(少なくともかなりの量の)ロジックを取得しました。また、私は完全な答えは必要ありません(そのような試みは歓迎されますが)。そうでなければ面倒なコードの単一の特定の行を読むためのエレガントな方法を提供する短い答えでさえいただければ幸いです。

Can関数コードを直線的に読むことさえできると思います、左から右へ、そして上から下へ?それとも、コードのページにスパゲッティのような配線のメンタルな絵を作成することを余儀なくされていますか?線形ではないそして、1つする必要がある場合でも、コードを読み取る必要があるので、線形テキストを取得してスパゲッティを配線するにはどうすればよいでしょうか。

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

_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
_
9
Andrew Willems

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

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などの一般的な機能を名前付き関数に組み込むことで回避されます。ラムダは、コンテキスト内で明確な目的を持つ非常に短い関数用に予約されています。

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

18
Karl Bielefeldt

私は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'はその中のアイテムであるという規則はvery 普及していますが、そのスタイルを簡単に理解しない限り、より詳細な名前を付けることをお勧めします。あなたが使用した特定の名前を見ると、「getX」は一種の不透明なので、「getXs」も実際にはあまり役に立ちません。私は「getXs」を「applyToProperties」のようなものと呼び、「getX」はおそらく「propertyMapper」になるでしょう。 "getPctSameXs"は "percentPropertiesSameWith"になります( "with"は別の規則の1つです-プレフィルタリング関数は他の操作の前に適用されることを示しています。 zipWith などのHaskell標準関数を参照してください)。

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

6
Jules