web-dev-qa-db-ja.com

OOプログラムを機能的なものにリファクタリングする方法は?

関数型のプログラムを作成する方法に関するリソースを見つけるのが困難です。オンラインで私が見つけた最も進んだトピックは、構造タイピングを使用してクラス階層を削減することでした。ほとんどは、map/fold/reduce/etcを使用して命令ループを置き換える方法を扱っています。

私が本当に見つけたいのは、重要なプログラムのOOP実装、その制限、および関数スタイルでそれをリファクタリングする方法です。アルゴリズムだけでなく、またはデータ構造ですが、いくつかの異なる役割と側面を持つもの-ビデオゲームかもしれませんが、私はTomas PetricekによるReal-World Functional Programmingを読んだことがありましたが、それ以上のものが必要です。

27
Asik

関数型プログラミングの定義

Clojureの喜び の紹介では、次のように述べています。

関数型プログラミングは、アモルファスな定義を持つ計算用語の1つです。 100人のプログラマにその定義を尋ねると、おそらく100種類の答えが返ってきます...

関数型プログラミングに関心があり、アプリケーションと関数の構成を容易にする...言語が関数型であると見なされるためには、関数の概念がファーストクラスである必要があります。ファーストクラスの関数は、他のデータと同様に、保存、受け渡し、および返すことができます。このコアコンセプトを超えて、[FPの定義には次のものが含まれる可能性があります]純粋性、不変性、再帰、遅延、参照の透明性。

Scala第2版)でのプログラミング p。10の定義は次のとおりです。

関数型プログラミングは、2つの主要なアイデアによって導かれます。最初のアイデアは、関数はファーストクラスの値であるということです...関数を引数として他の関数に渡したり、関数の結果として返したり、変数に格納したりできます...

関数型プログラミングの2番目の主なアイデアは、プログラムの操作で、データを適切に変更するのではなく、入力値を出力値にマップすることです。

最初の定義を受け入れる場合、コードを「機能させる」ために必要なことは、ループを裏返すだけです。 2番目の定義には不変性が含まれます。

ファーストクラスの機能

現在、バスオブジェクトから乗客のリストを取得し、それを繰り返して、バスの運賃の額だけ各乗客の銀行口座を減らしたとします。これと同じアクションを実行する機能的な方法は、バスにメソッドを置くことです。1つの引数の関数を取るforEachPassengerと呼ばれることもあります。その後、バスはその乗客を反復しますが、それが最善の方法であり、乗車料金を請求するクライアントコードが関数に入れられて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で修正されましたが、ファーストクラス関数内でのチェック済み例外の処理は依然として非常に醜く、Javaは依然としてすべてにおいて可変性の仮定を持っていますこれは、FPに関連することが多い他の目標に私たちを導きます:

不変性

Josh Blochの項目13は「不変性を優先する」です。反対の一般的なゴミの話にもかかわらず、OOPは不変オブジェクトで行うことができ、そうすることではるかに良くなります。たとえば、Javaの文字列は不変です。StringBuffer、OTOHは不変の文字列を構築するために変更可能である必要があります。バッファの操作などの一部のタスクには、本質的に変更可能性が必要です。

純度

各関数は少なくともメモ化可能である必要があります-同じ入力パラメーターを指定した場合(実際の引数以外に入力がない場合)、グローバル状態の変更やIの実行などの「副作用」を引き起こすことなく、毎回同じ出力を生成する必要があります/ O、または例外をスローします。

関数型プログラミングでは、「通常、作業を行うにはある程度の悪が必要です」と言われています。 100%の純度は一般的に目標ではありません。副作用を最小限に抑えることです。

結論

実際、上記のすべてのアイデアの中で、不変性は、OOPであろうとFPであろうと、コードを単純化するための実用的なアプリケーションの面で最大の単一の勝利でした。イテレータに関数を渡すことは、2番目に大きな勝利です。 Java 8 Lambdasのドキュメント が理由を最もよく説明しています。再帰は、ツリーの処理に最適です。怠惰では、無限のコレクションを操作できます。

JVMが気に入った場合は、ScalaとClojureをご覧ください。どちらも関数型プログラミングの洞察に富んだ解釈です。Scalaはタイプセーフで、 Cに似た構文ですが、Haskellと共通の構文がCと同じですが、Clojureはタイプセーフではなく、LISPです。最近、 comparison of Java、ScalaとClojure 1つの特定のリファクタリング問題に関して Logan Campbellの比較 Game of Lifeを使用すると、Haskellと型付きClojureも含まれます。

PS

Jimmy Hoffaは、私のBusクラスは変更可能であることを指摘しました。オリジナルを修正するのではなく、これはこの質問に関するリファクタリングの種類を正確に示すと思います。これは、バスの各メソッドをファクトリーにして新しいバスを生成し、パッセンジャーの各メソッドをファクトリーにして新しいパッセンジャーを生成することで修正できます。したがって、すべてに戻り値の型を追加しました。これは、Consumerインターフェースの代わりにJava 8's 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 を参照してください。

31
GlenPeterson

私はこれを「達成」する個人的な経験をしています。結局、純粋に機能的なものは思いつきませんでしたが、満足できるものを思いつきました。ここに私がそれをした方法があります:

  • すべての外部状態を関数のパラメーターに変換します。 EG:オブジェクトのメソッドがxを変更する場合は、メソッドが_this.x_を呼び出す代わりにxを渡されるようにします。
  • オブジェクトから動作を削除します。
    1. オブジェクトのデータを公開します
    2. すべてのメソッドを、オブジェクトが呼び出す関数に変換します。
    3. オブジェクトを呼び出すクライアントコードで、オブジェクトデータを渡して新しい関数を呼び出します。 EG:x.methodThatModifiesTheFooVar()fooFn(x.foo)に変換します
    4. オブジェクトから元のメソッドを削除します
  • 可能な限り多くの反復ループをmapreducefilterなどの高次関数で置き換えます。

変更可能な状態を取り除くことができませんでした。私の言語(JavaScript)ではそれはあまりにも慣用的ではありませんでした。ただし、すべての状態を渡したり返したりすることで、every関数をテストできます。これは、OOPとは異なります。この場合、状態の設定に時間がかかりすぎたり、依存関係を分離するには、多くの場合、最初に製品コードを変更する必要があります。

また、私は定義について間違っている可能性がありますが、私の機能は参照上透過的であると思います。同じ入力を与えられた場合、私の機能は同じ効果を持つでしょう。

編集する

こちらをご覧ください 、JavaScriptで真に不変のオブジェクトを作成することはできません。勤勉で誰がコードを呼び出すかを制御している場合は、現在のオブジェクトを変更するのではなく、常に新しいオブジェクトを作成することでそれを行うことができます。それは私にとって努力の価値がありませんでした。

ただし、Java これらの手法を使用できます を使用している場合は、クラスを不変にします。

13
Daniel Kaplan

プログラムを完全にリファクタリングすることは実際には可能ではないと思います。正しいパラダイムで再設計して再実装する必要があります。

「既存のコード本体を再構築し、外部動作を変更せずに内部構造を変更するための統制のとれた手法」として定義されたコードリファクタリングを見てきました。

特定の機能をより機能的にすることもできますが、その中心にはまだオブジェクト指向プログラムがあります。小さなパラダイムを変更して別のパラダイムに適合させることはできません。

3
Uri

この一連の記事はまさにあなたが望むものだと思います:

完全に機能するレトロゲーム

http://prog21.dadgum.com/23.htmlパート1

http://prog21.dadgum.com/24.htmlパート2

http://prog21.dadgum.com/25.htmlパート3

http://prog21.dadgum.com/26.htmlパート4

http://prog21.dadgum.com/37.htmlフォローアップ

要約は次のとおりです。

著者は副作用のあるメインループ(副作用はどこかで発生するはずですよね?).

もちろん、実際のプログラムを作成するときは、いくつかのプログラミングスタイルを組み合わせて使用​​し、それぞれが最も役立つ場所を使用します。ただし、グローバル変数のみを使用して、最も機能的/不変の方法でプログラムを記述し、最もスパゲッティの方法でプログラムを記述してみるのは良い学習経験です。

3
marcus

OOPおよびFPには、コードを編成するための2つの反対のアプローチがあるため、おそらくすべてのコードを裏返しにする必要があります。

OOPはタイプ(クラス)を中心にコードを編成します。異なるクラスが同じ操作(同じシグネチャを持つメソッド)を実装できます。その結果、OOPは、操作のセットがあまり変更されず、新しいタイプを頻繁に追加できる場合に適しています。たとえば、各ウィジェットに固定セットがあるGUIライブラリを考えますメソッド(hide()show()Paint()move()など)ですが、ライブラリが拡張されると新しいウィジェットを追加できます。OOP=では、新しい型を追加するのは簡単です(特定のインターフェイスの場合):新しいクラスを追加し、そのすべてのメソッドを実装するだけです(ローカルコードの変更)。一方、 、インターフェイスに新しい操作(メソッド)を追加するには、そのインターフェイスを実装するすべてのクラスを変更する必要がある場合があります(継承によって作業量を減らすことができます)。

FPは操作(関数)を中心にコードを編成します。各関数は、さまざまなタイプをさまざまな方法で処理できるいくつかの操作を実装します。これは通常、パターンマッチングまたはその他のメカニズムを介して型をディスパッチすることによって実現されます。結果として、FPは、タイプのセットが安定していて、新しい操作がより頻繁に追加される場合により適切です。たとえば、固定フォーマットの画像フォーマット(GIF、JPEGなど)と実装するアルゴリズム。各アルゴリズムは、イメージのタイプに応じて異なる動作をする関数によって実装できます。新しい関数を追加するのは、新しい関数(ローカルコードの変更)を実装するだけでよいので簡単です。新しいアルゴリズムの追加フォーマット(タイプ)をサポートするには、これまでに実装したすべての関数を変更する必要があります(ローカルではない変更)。

結論:OOPとFPは、コードの編成方法が根本的に異なり、OOPデザインをFP設計では、これを反映するようにすべてのコードを変更する必要があります。ただし、興味深い演習になる可能性があります。mikemayによって引用されたSICPブックの以下の 講義ノート も参照してください。特にスライド13.1.5から13.1.10まで。

2
Giorgio