web-dev-qa-db-ja.com

Java 8では、メソッド参照式または関数インターフェイスの実装を返すメソッドを使用するのが文体的に優れていますか?

Java 8は function interfaces の概念に加えて、機能的なインターフェースを取るように設計された多くの新しいメソッドを追加しました。これらのインターフェースのインスタンスは、 メソッド参照式 を使用して簡潔に作成できます(例:SomeClass::someMethod)および ラムダ式 (例:(x, y) -> x + y)。

同僚と私は、どちらのフォームを使用するのが最適かについて意見が異なります(この場合、「最適」は、「最も読みやすい」と「標準的な慣行に最も近い」に要約されます。同等)。具体的には、次のすべてが当てはまる場合です。

  • 問題の関数は単一のスコープの外では使用されません
  • インスタンスに名前を付けると、読みやすさが向上します(たとえば、何が起こっているのかが一目でわかるほど単純なロジックではありません)。
  • ある形式が他の形式よりも好ましい理由は他にありません。

この問題についての私の現在の意見は、プライベートメソッドを追加し、それをメソッド参照で参照する方が良いアプローチであるというものです。このように機能が使用されるように設計されているように感じられ、メソッド名とシグネチャを介して何が行われているのかを簡単に伝えることができます(たとえば、「boolean isResultInFuture(Result result)」は、ブール値を返すことを明確に示しています)。また、クラスの将来の拡張で同じチェックを利用したいが、機能インターフェースラッパーが必要ない場合は、privateメソッドをより再利用可能にします。

私の同僚の好みは、インターフェースのインスタンスを返すメソッドを用意することです(たとえば、「Predicate resultInFuture()」)。私には、この機能の使用方法が完全ではないように感じられ、少し不格好に感じられ、名前を付けることで意図を実際に伝えるのが難しいように思われます。

この例を具体的にするために、異なるスタイルで記述された同じコードを次に示します。

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

1つのアプローチの方が望ましいか、言語設計者の意図に沿ったものか、または読みやすいかについて、公式または準公式のドキュメントまたはコメントはありますか?公式の情報源を除いて、1つがより良いアプローチになる理由は明確にありますか?

11
M. Justin

関数型プログラミングの観点から、あなたとあなたの同僚が話し合っているのはポイントフリースタイル、より具体的にはetaリダクション。次の2つの割り当てを検討してください。

_Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;
_

これらは操作上同等であり、最初のスタイルはpointfulスタイルと呼ばれ、2番目のスタイルはpoint freeスタイル。 「ポイント」という用語は、後者の場合には欠落している名前付き関数の引数(これはトポロジーに由来する)を指します。

より一般的には、すべての関数Fに対して、与えられたすべての引数を取り、それらを変更せずにFに渡し、Fの結果を返すラッパーラムダはF自体と同じです。ラムダを削除してFを直接使用する(上記のポイントフルスタイルからポイントフリースタイルに移行する)は、etaリダクションと呼ばれ、 ラムダ計算

Etaリダクションによって作成されないポイントフリーの式があり、その最も古典的な例は関数構成です。タイプAからタイプBへの関数と、タイプBからタイプCへの関数が与えられた場合、それらをタイプAからタイプCへの関数にまとめることができます。

_public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}
_

次に、Result deriveResult(Foo foo)メソッドがあるとします。述語は関数なので、最初にderiveResultを呼び出し、次に派生結果をテストする新しい述語を作成できます。

_Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);
_

composeimplementationはラムダでポイントフルスタイルを使用しますが、usecomposeを定義してisFoosResultInFutureを定義すると、引数を指定する必要がないため、ポイントフリーになります。

ポイントフリープログラミングはtacitプログラミングとも呼ばれ、関数定義から無意味な(意図されていない)詳細を削除することで可読性を向上させる強力なツールになります。 Java 8は、Haskellのようなより多くの関数型言語ほど完全にポイントフリープログラミングをサポートしていませんが、経験則は常にeta-reductionを実行することです。ラムダを使用する必要はありません。ラップする関数と異なる動作はありません。

8
Jack

現在の回答はどちらも、クラスがprivate boolean isResultInFuture(Result result)メソッドまたはprivate Predicate<Result> resultInFuture()メソッドを持つ必要があるかどうかという質問の核心に実際には対処していません。それらはほぼ同等ですが、それぞれに長所と短所があります。

最初のアプローチは、メソッド参照を作成することに加えて、メソッドを直接呼び出す場合により自然です。たとえば、このメソッドを単体テストする場合、最初のアプローチを使用する方が簡単かもしれません。最初のアプローチのもう1つの利点は、メソッドがその使用方法を気にする必要がなく、呼び出しサイトから切り離されることです。後でクラスを変更してメソッドを直接呼び出す場合、この分離は非常に小さな効果をもたらします。

最初のアプローチは、このメソッドをさまざまな機能インターフェイスまたはインターフェイスのさまざまなユニオンに適応させたい場合にも優れています。たとえば、メソッド参照をPredicate<Result> & Serializable型にする必要がある場合がありますが、これは2番目のアプローチでは実現する柔軟性がありません。

一方、述語でより高次の演算を実行する場合は、2番目の方法がより自然です。 Predicate にはいくつかのメソッドがあり、メソッド参照で直接呼び出すことはできません。これらの方法を使用する場合は、2番目の方法が優れています。

最終的に、決定は主観的です。しかし、Predicateでメソッドを呼び出すつもりがない場合は、最初のアプローチを優先する傾向があります。

5

Java=コードはもう記述しませんが、私は生活のために関数型言語で記述し、関数型プログラミングを学んでいる他のチームもサポートします。ラムダには読みやすさの微妙なスイートスポットがあります。それらの一般的に受け入れられているスタイルは、インライン化できるときに使用することですが、後で使用するために変数に割り当てる必要があると感じた場合は、おそらくメソッドを定義するだけです。

はい、人々は常にチュートリアルで変数にラムダを割り当てます。これらは、それらがどのように機能するかを完全に理解するのに役立つチュートリアルにすぎません。比較的まれな状況(if式を使用して2つのラムダから選択するなど)を除いて、実際にはそのようには使用されません。

つまり、@ JimmyJamesのコメントの例のように、ラムダがインライン化後に簡単に読み取れるほど短い場合は、次のようにします。

people = sort(people, (a,b) -> b.age() - a.age());

これを、ラムダの例をインライン化しようとしたときの読みやすさと比較してください。

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

あなたの特定の例として、私は個人的にプルリクエストでフラグを立て、その長さのために名前付きメソッドにプルするように依頼します。 Javaは厄介で冗長な言語であるため、おそらく独自の言語固有の慣行は他の言語とは異なるかもしれませんが、それまではラムダを短くインラインにしてください。

4
Karl Bielefeldt