関数型プログラミングの定義
「Clojureの喜び」の紹介には次のように書かれています。
関数型プログラミングは、定型化されていない計算用語の1つです。100人のプログラマーに定義を求めると、おそらく100の異なる答えを受け取るでしょう...
関数型プログラミングは、関数の適用と構成を懸念し、容易にします...言語が関数型と見なされるためには、関数の概念がファーストクラスでなければなりません。ファーストクラスの関数は、他のデータと同じように保存、受け渡し、および返すことができます。このコア概念を超えて、[FPの定義には、純度が含まれる]、不変性、再帰、遅延、および参照の透明性が含まれます。
Scala 2nd Editionでのプログラミング p。10の定義は次のとおりです。
関数型プログラミングは、2つの主なアイデアに基づいています。最初のアイデアは、関数がファーストクラスの値であるということです...関数を引数として他の関数に渡したり、関数から結果として返したり、変数に保存したりすることができます...
関数型プログラミングの第2の主な考え方は、プログラムの操作では、所定のデータを変更するのではなく、入力値を出力値にマップすることです。
最初の定義を受け入れた場合、コードを「機能」させるために必要なことは、ループを裏返しにすることだけです。2番目の定義には不変性が含まれます。
ファーストクラス機能
現在、バスオブジェクトから乗客のリストを取得し、それを反復処理して、各乗客の銀行口座をバス運賃の金額だけ減らしているとします。これと同じアクションを実行する機能的な方法は、1つの引数の関数を取るforEachPassengerと呼ばれるメソッドをBusに持つことです。その後、バスは乗客に対して繰り返し実行されますが、これは最もよく達成され、乗車料金を請求するクライアントコードは関数に入れられ、forEachPassengerに渡されます。出来上がり!関数型プログラミングを使用しています。
必須:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
機能的(匿名関数またはScalaの「ラムダ」を使用):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
より甘いScalaバージョン:
myBus = myBus.forEachPassenger(_.debit(fare))
ファーストクラス以外の関数
あなたの言語が一流の関数をサポートしていない場合、これは非常に見苦しくなります。Java 7以前では、次のような「機能オブジェクト」インターフェースを提供する必要があります。
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
次に、Busクラスは内部反復子を提供します。
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
最後に、匿名関数オブジェクトをバスに渡します。
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
Java 8では、ローカル変数を匿名関数のスコープに取り込むことができますが、以前のバージョンでは、そのような変数をfinalとして宣言する必要があります。これを回避するには、MutableReferenceラッパークラスを作成する必要があります。上記のコードにループカウンターを追加できる整数固有のクラスを次に示します。
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
このさでも、内部反復子を提供することにより、プログラム全体に広がるループから複雑で繰り返しのロジックを排除することが有益な場合があります。
このさはJava 8で修正されましたが、ファーストクラス関数内でのチェック済み例外の処理は依然として非常にugく、Javaはすべてのコレクションで可変性の仮定を保持しています。これにより、FPに関連することが多い他の目標に到達できます。
不変性
ジョシュ・ブロックのアイテム13は「不変性を好む」です。反対に一般的なゴミの話にもかかわらず、OOPは不変オブジェクトで実行でき、そうすることではるかに良くなります。たとえば、Javaの文字列は不変です。不変の文字列を作成するには、StringBuffer、OTOHが可変である必要があります。バッファの操作など、一部のタスクには本質的に可変性が必要です。
純度
各関数は少なくともメモ可能である必要があります-同じ入力パラメータを指定する場合(実際の引数以外に入力がない場合)、グローバル状態の変更などの「副作用」を引き起こさずに毎回同じ出力を生成し、I / O、または例外をスローします。
関数型プログラミングでは、「仕事を成し遂げるには通常、何らかの悪が必要だ」と言われています。通常、100%の純度は目標ではありません。副作用を最小限に抑えることです。
結論
実際、上記のすべてのアイデアの中で、不変性は、コードを簡素化するための実用的なアプリケーション(OOPかFPかにかかわらず)で最大の勝利です。関数をイテレータに渡すことは、2番目に大きな勝利です。Javaの8ラムダのマニュアルには理由の最良の説明があります。再帰はツリーの処理に最適です。怠azineでは、無限のコレクションを扱うことができます。
JVMが好きなら、ScalaとClojureをご覧になることをお勧めします。どちらも関数型プログラミングの洞察に富んだ解釈です。ScalaはややCに似た構文でタイプセーフですが、実際にはHaskellと共通の構文をCと同じように持っています。Clojureはタイプセーフではなく、Lispです。最近、特定のリファクタリングの問題に関して、Java、Scala、Clojureの比較を投稿しました。 Game of Lifeを使用したLogan Campbellの比較には、Haskellと型付きClojureも含まれます。
PS
Jimmy Hoffaは、私のBusクラスは変更可能であると指摘しました。オリジナルを修正するのではなく、この質問の対象となるリファクタリングの種類を正確に示していると思います。これは、Busの各メソッドをファクトリーにして新しいBusを作成し、Passengerの各メソッドをファクトリーにして新しいPassengerを作成することで修正できます。したがって、すべてに戻り値の型を追加しました。つまり、Consumerインターフェイスの代わりにJava 8のjava.util.function.Functionをコピーします。
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
それからバスで:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
最後に、無名関数オブジェクトは、変更された状態(新しい乗客を含む新しいバス)を返します。これは、p.debit()が新しい不変のPassengerを元よりも少ない金額で返すことを前提としています。
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
命令型言語をどのように機能させたいかについて独自の決定を下し、関数型言語を使用してプロジェクトを再設計する方が良いかどうかを判断できるようになりました。ScalaまたはClojureでは、コレクションおよびその他のAPIは、関数型プログラミングを簡単にするように設計されています。どちらも非常に優れたJava相互運用機能を備えているため、言語を組み合わせて使用できます。実際、Javaの相互運用性のために、Scalaは最初のクラス関数を、Java 8機能インターフェースとほぼ互換性のある匿名クラスにコンパイルします。詳細については、Scala in Depthのセクションを参照してください。1.3.2。