web-dev-qa-db-ja.com

機能的コレクションと手続き的コレクションの処理の大きな違い

関数の受け渡しに関する実証的研究を計画しています。具体的には、ラムダ、つまり匿名関数、または矢印関数です。現在、手続き型/命令型プログラミングよりも関数型またはオブジェクト指向のアプローチが非常に好まれていますが、その優位性に関する実証的な証拠はほとんどないようです。 高次プ​​ログラミング¹の方が良い理由についてはさまざまな主張がありますが、統計的に有意な違いが生じる可能性のあるケースを構築するのは難しいです。

コードはより表現力があり、-whatに指示し、-howには指示しない

このような主張は、主観的および美的観点から見ればニースですが、レバレッジを得るためには、生産性または保守性の経験的な違いにマッピングする必要があります。

現在、JavaのStream APIに焦点を当てています。Javaの記述方法に大きな変化があったためです。業界では、これには大幅な書き換え、従業員トレーニングの需要、およびIDEの更新が伴い、以前よりもはるかに優れているという証拠はありませんでした。

私の考えでは、ラムダベースの実装では、より良い結果が得られない可能性があります。参加者が言語APIに加えてストリームAPIを知っている必要があることを考慮しても、次のようになります。

// loop-based
public int inactiveSalaryTotal(List<Employee> employees) {
    int total = 0;
    for (Employee employee : employees) {
        if (!employee.isActive()) {
            total += employee.getSalary();
        }
    }
    return total;
}

// lambda-based
public int inactiveSalaryTotal(List<Employee> employees) {
    return employees.stream()
                   .filter(e -> !e.isActive())
                   .mapToInt(Employee::getSalary)
                   .sum();
}

私は個人的にストリームの収集に関する利点を疑っていますが、平均的なJava=開発者は、特定のタスクに取り残されないようにAPIの表面を十分に知っていることを疑っています。

// loop-based
public Map<String, Double> averageSalaryByPosition(List<Employee> employees) {
    Map<String, List<Employee>> groups = new HashMap<>();
    for (Employee employee : employees) {
        String position = employee.getPosition();
        if (groups.containsKey(position)) {
            groups.get(position).add(employee);
        } else {
            List<Employee> group = new ArrayList<>();
            group.add(employee);
            groups.put(position, group);
        }
    }
    Map<String, Double> averages = new HashMap<>();
    for (Map.Entry<String, List<Employee>> group : groups.entrySet()) {
        double sum = 0;
        List<Employee> groupEmployees = group.getValue();
        for (Employee employee : groupEmployees) {
            sum += employee.getSalary();
        }
        averages.put(group.getKey(), sum / groupEmployees.size());
    }
    return averages;
}

// lambda-based
public Map<String, Double> averageSalaryByPosition(List<Employee> employees) {
    return employees.stream().collect(
            groupingBy(Employee::getPosition, averagingInt(Employee::getSalary))
    );
}

特定の質問

ラムダベースのコレクション処理が、理解時間、書き込み時間、変更の容易さ、またはバグの修正やカウントなどの特定のタスクの実行にかかる時間の点で、手続き型処理(ループ)よりも優れている典型的なケースを構築できますか?特定の呼び出しまたはパラメーター。 LOCはここで私が探しているものではなく、時間で測定できるものなので、パフォーマンスがどのように向上するかについても非常に興味があります。最終的には平均Java開発者実行時のパフォーマンスに関しては、パフォーマンスの向上も明確に意味されていません。

例としては、疑似コードや、Java、JavaScript、Scala、C#、Kotlinなどの両方のパラダイムをサポートする言語を使用できます。


¹関数の受け渡しに重点を置いて、OOPおよびFPがこの目的のためにいくらか同型であると想定しています(型システムは別として))あなたはオブジェクトを関数のタプルとして見ることができるからです。オブジェクトが他のオブジェクトを受け入れて返すことができるので、基本的に高次の関数が得られます

5
nilo

lOCは私がここで探しているものではありません

しかし、なぜ?少なくともLOCはあなたがすべきここで探しているものです。コードの行数は真に信頼性の高い保守性の指標にはなりませんが、5行で20行を読み取るのに時間がかかることに反対するのは難しいでしょう。プログラミングは書くことだけではありません。 readingについてもです。

「矢印」の有用性を判断する非常にドラフトの経験的な方法を紹介します。

これがコピーされたコードですほぼ逐語的ですが、マイナーな変更を加えただけで、おそらく世界にすべての違いが生まれます。これは、最終的に「矢印スタイル」のメソッドがいかに役立つかを知るために使用するものです。これは実験的な素材です!

// loop-based
public Map<String, Double> whatDoesThisFunctionDo(List<Employee> employees) {
    Map<String, List<Employee>> groups = new HashMap<>();
    for (Employee employee : employees) {
        String position = employee.getPosition();
        if (groups.containsKey(position)) {
            groups.get(position).add(employee);
        } else {
            List<Employee> group = new ArrayList<>();
            group.add(employee);
            groups.put(position, group);
        }
    }
    Map<String, Double> averages = new HashMap<>();
    for (Map.Entry<String, List<Employee>> group : groups.entrySet()) {
        double sum = 0;
        List<Employee> groupEmployees = group.getValue();
        for (Employee employee : groupEmployees) {
            sum += employee.getSalary();
        }
        averages.put(group.getKey(), sum / groupEmployees.size());
    }
    return averages;
}

// lambda-based
public Map<String, Double> whatDoesThisFunctionDo(List<Employee> employees) {
    return employees.stream().collect(
            groupingBy(Employee::getPosition, averagingInt(Employee::getSalary))
    );
}

一部の人に最初の機能を見せて、彼らが意図した意味を理解するのにかかる平均時間を測定します。次に、2番目の関数を一部のother(明らかに)人に示し、全体の理解にかかる平均時間を再度測定します。関数に適切な名前を付けるように依頼します。結果を比較します。理解度改善率を計算します。たぶん、あなたはそこでいくつかの統計的に有意な違いを得ることができます!

コードはより表現力があり、方法ではなく何をすべきかを伝えます

はい、そうです!

このような主張は、主観的および美的観点から見ればニースですが、レバレッジを得るためには、生産性または保守性の経験的な違いにマッピングする必要があります。

まあ、5つのメソッド、または10のメソッドを持つクラスがある場合は、上記で得られた比率(これは、プログラマーに示した特定の関数に基づく、経験に基づくだけです)を適用します。違いはすぐに重要になる傾向があります!


外の世界から、投稿した機能の名前を読んで「要点をつかむ」。しかし、これは、あなたや誰もが誰か、どこかで、名前からではなく、内容から「ポイントを獲得する」必要があるという事実から注意をそらすべきではありません。誰かもproduceコンテンツを作成する必要があります。誰かがreviewコンテンツをしなければなりません。少ない労力typing、少ない労力thinking、少ない労力reviewing、少ない余地bugsについて美的または主観的なものは何もありません。 =、少ない新規参入者のトレーニング労力など。これらは生産性に直接つながり、その結果、費やしたリソースと反比例します。

これはあなたが探している答えではないかもしれませんが、私見では、客観的で重要な測定可能な短く、シンプルで表現力のあるがあります。

11
Vector Zita

あなたはすでに受け入れて答えましたが、これはちょっとした付記ですが、JavaのストリームAPIに本当に満足したことはありません。デザインは非常にOO中心であり、(皮肉なことに)多少手続き型であるため、非常に醜い(そして判読不能な)コードIMOにつながる可能性があります。同時に導入された関数参照に関する機能は、そのノイズの多くを回避できるため、残念です。同じ理由で、クリーンアップする再利用可能な関数を作成するのは非常に簡単です。これを導入すると、最初の例は次のようになります。

public int inactiveSalaryTotal(List<Employee> employees) {
  return sum(map(filter(employees, Employee::isActive), Employee::getSalary);
}

これにより、真の関数型言語での処理に少し似たものが得られます。より簡潔で理解しやすいと思います。これらは、ニーズに応じて定義できるさまざまな方法があります。必要に応じて、実装例をいくつか提供できます。

3
JimmyJames

一般的な注意として、「関数型」コード(またはJavaストリーム)を使用するコード)は、「命令型」コードよりも常に優れていると言うのは誤りです。 (またはJavaループを使用するコード)。場合によってははるかに優れていますが、場合によってはコードが読みにくくなることがあります。詳細な分析については、項目45(ストリームの慎重な使用)を参照してください。 )「Effective Java」の第3版。

とはいえ、最初のループの例では改善できなかった最初のストリームの例を簡単に改善できるケースを見つけるのは簡単です。青髪の従業員の総給与を計算する関数と、緑髪、オレンジ髪などの従業員の同様の関数も必要だとします。命令型バージョンでは、コピーと貼り付けの悪夢に終わるでしょうが、機能型バージョンでは、追加の関数引数としてフィルタリング述語を渡すだけで済みます。

public int salaryTotal(List<Employee> employees, Predicate<Employee> condition) {
    return employees.stream()
                    .filter(condition)
                    .mapToInt(Employee::getSalary)
                    .sum();
}

もちろん、一般的なコメントで指摘されているように、述語を手続き型/命令型のメソッドに渡すこともできますが、関数型(高階関数)になります。関数型プログラミングのエッセンスは、小さくて明らかに正しい関数を1つの複雑な機能に組み合わせる宣言的で読みやすい方法を見つけることであり、Stream APIは1つの方法にすぎませんこれを達成するために。

もう1つの例は、将来のイベントの処理です。将来雇用されるすべての従業員を処理することを想像してみてください:未知の、潜在的に無限のイベントのリスト。ループで繰り返すことはできませんが、リアクティブストリームライブラリを使用して宣言的な方法で処理することはできます。同じことは、命令コードを使用して非常に判読できなくなります。

2
lbalazscs