オブジェクト指向プログラムを機能的なものにリファクタリングする方法は?


26

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

私が本当に見つけたいのは、自明ではないプログラムのOOP実装、その制限、およびそれを機能的なスタイルにリファクタリングする方法についての詳細な議論です。アルゴリズムやデータ構造だけでなく、いくつかの異なる役割と側面を持つもの-ビデオゲームかもしれません。ちなみに、私はTomas PetricekによるReal-World Functional Programmingを読みましたが、もっと欲しいと思っています。


6
可能だとは思いません。すべてを再設計(および書き換え)する必要があります。
ブライアンチェン

18
-1、この投稿は、OOPと機能スタイルが反対であるという誤った仮定によって偏っています。それらはほとんど直交する概念であり、私見はそうではないという神話です。「機能的」は「手続き的」よりも反対であり、両方のスタイルをOOPと組み合わせて使用​​できます。
ドックブラウン

11
@ DocBrown、OOPは可変状態に過度に依存しています。ステートレスオブジェクトは、現在のOOP設計プラクティスにうまく適合しません。
SKロジック

9
@ SK-logic:キーはステートレスオブジェクトではなく、不変オブジェクトです。また、オブジェクトが可変であっても、特定のコンテキスト内で変更されない限り、システムの機能部分で頻繁に使用できます。さらに、オブジェクトとクロージャーは互換性があることを知っていると思います。したがって、これはすべて、OOPと「機能」が相反しないことを示しています。
ドックブラウン

12
@DocBrown:言語構成は直交していると思いますが、考え方は衝突する傾向があります。OOPの人々は「オブジェクトとは何か、どのようにコラボレーションするのか」と尋ねる傾向があります。機能的な人々は、「私のデータは何で、どのようにそれを変換したいのですか?」これらは同じ質問ではなく、異なる答えにつながります。また、質問を読み間違えたと思います。「OOPよだれとFPルール、OOPを取り除く方法」ではなく、「OOPを取得し、FPを取得しません。OOPプログラムを機能的なものに変換する方法はありますか?いくつかの洞察?」。
マイケルショー

回答:


31

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

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


この回答の努力、組織、明確なコミュニケーションに感謝します。しかし、私は技術のいくつかでわずかな問題を取る必要があります。冒頭で言及したキーの1つは関数の構成です。これは、オブジェクト内に関数を大きくカプセル化しても目的が得られない理由に戻ります。関数がオブジェクト内にある場合、そのオブジェクトに作用する必要があります。そして、それがそのオブジェクトに作用する場合、それは内部を変更しているに違いありません。今、私は誰もが参照透明性や不変性を必要としませんが、それは場所にオブジェクトを変更した場合、それはもはやそれを返す必要が許します
ジミー・ホッファ

そして、関数が値を返さないとすぐに、その関数は突然他の人と合成できなくなり、関数合成のすべての抽象化を失います。関数でオブジェクトを適切に変更してからオブジェクトを返すことができますが、これを行う場合は、関数がオブジェクトをパラメータとして取得し、その親オブジェクトの範囲から解放するだけではどうですか?親オブジェクトから解放されると、他のタイプでも機能するようになります。これは、FPのもう1つの重要な部分であるタイプ抽象化です。あなたのforEachPasengerは...乗客に対して動作します
ジミー・ホッファ

1
マップして削減するものを抽象化し、これらの関数がオブジェクトを含むことに拘束されない理由は、パラメトリック多相性によって無数のタイプで使用できるようにするためです。FPを実際に定義し、価値を持たせるのは、OOP言語にはないこれらのさまざまな抽象化の大火です。それは怠惰、参照透明性、不変性、あるいはHM型システムは、FPを作成するために必要であることはありませんが、これらのものは、一般的な機能を抽象オーバータイプの機能構成のための目的と言語作成のかなり副作用です
ジミー・ホッファ

@JimmyHoffaあなたは私の例に対して非常に公正な批判をしました。Java8 Consumerインターフェースによって可変性に魅了されました。また、FPのchouser / fogusの定義には不変性が含まれていなかったため、後でOdersky / Spoon / Vennersの定義を追加しました。元の例を残しましたが、下部の「PS」セクションの下に新しい不変バージョンを追加しました。それは醜いです。しかし、オリジナルの内部を変更するのではなく、オブジェクトに作用して新しいオブジェクトを生成する機能を示していると思います。素晴らしいコメント!
グレンピーターソン

1
この会話はホワイトボードで継続されます:chat.stackexchange.com/transcript/message/11702383#11702383
グレンペターソン

12

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

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

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

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

編集

ここにあるように、JavaScriptで真に不変のオブジェクトを作成することはできません。あなたがコードを呼び出す人を熱心に管理している場合は、現在のオブジェクトを変更するのではなく、常に新しいオブジェクトを作成することでそれを行うことができます。努力する価値はありませんでした。

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


+1まさにあなたが何をしようとしているのかにもよりますが、これはおそらく、「リファクタリング」を超えた設計変更を行わずに本当にできる限りです。
エヴィカトス

@Evicatos:JavaScriptが不変状態をよりよくサポートしていれば、私のソリューションはClojureのような動的関数型言語で得られるのと同じくらい機能的だと思います。リファクタリングだけでなく、何かを必要とするものの例は何ですか?
ダニエルカプラン

可変状態を取り除くことは資格があると思います。私はそれが単に言語のより良いサポートの問題だとは思いません。可変から不変に移行するには基本的に常に基本的な書き換えを必要とする基本的なアーキテクチャの変更が必要だと思います。ただし、リファクタリングの定義に応じてYmmv。
エヴィカトス

@Evicatosは私の編集を見る
ダニエルカプラン

1
はい@tieTYT、それはJSがとても可変であることについては悲しいですが、少なくとも、Clojureのは、JavaScriptにコンパイルすることができます:github.com/clojure/clojurescript
GlenPeterson

3

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

コードのリファクタリングは、「既存のコード本体を再構築し、外部の動作を変更せずに内部構造を変更するための規律あるテクニック」と定義されています。

特定のものをより機能的にすることもできますが、その中核にはオブジェクト指向プログラムがまだあります。別のパラダイムに適応するために、ほんの少しだけ変更することはできません。


良い最初の印は、参照の透明性を追求することです。これができたら、関数型プログラミングの最大50%の利点を得ることができます。
ダニエル・グラッツァー

3

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

純粋に機能的なレトロゲーム

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 フォローアップ

要約は次のとおりです。

著者は、副作用のあるメインループを提案しています(副作用はどこかで発生するはずですよね?)

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


2

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も参照してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.