関数式プログラミング思想:結合と組合せ


オブジェクト向けプログラミングは,変動部分をカプセル化することでコードを分かりやすくし,関数式プログラミングは変動部分を最小化することでコードを分かりやすくする.Michael Feathers,Working with Legacy Codeという本の著者は毎日特定の抽象でコード作業を行い、この抽象はあなたの脳に浸透し、問題を解決する方法に影響を与えます.この文章シリーズの目標の一つは,典型的な問題をどのように関数的に見るかを説明することである.本文と次の文章について、私は再構築とそれに伴う抽象的な影響を通じてコードの再利用問題を解決します.オブジェクト向けの目的の1つは、パッケージングとステータス操作をより容易にすることであり、その抽象的な傾向は、通常の問題を解決するためにステータスを使用することであり、これは複数のクラスとインタラクションを使用することを意味する.これは、Michael Feathersの前述の「変動部」である.関数式プログラミングは,構造を結合して変動を最小化するのではなく,各部分を組み合わせることによって試みられているが,これは微妙な概念であり,その経験が主に対象言語に現れている開発者にとっては,あまり理解しにくい.構造のコード再利用コマンド式(特に)オブジェクト向けのプログラミングスタイルは、構築ブロックとして構造とメッセージを使用する.オブジェクト向けのコードを再利用するには、オブジェクトコードを別のクラスに抽出し、継承を使用してアクセスする必要があります.コードの再利用とその影響を説明するために、コード構造とスタイルを説明するために使用されたデジタル分類器のバージョンを再提案します.この分類器は、正数が余裕であるか、完璧であるか、欠けているかを決定します.デジタル因子の総和が数字の2倍より大きい場合、それは余裕であり、総和が数字の2倍に等しい場合、それは完璧です.そうでなければ(合計が数字の2倍未満の場合)欠けています.また、正数の因子を使用して素数であるかどうかを決定するコードを作成することもできます(定義は、1より大きい整数であり、その因子は1とそれ自体しかありません).この2つの問題はいずれも数値の因子に依存するため、コードの再利用スタイルを説明するために再構築される良いオプションのケースでもあります.インベントリ1は、コマンドスタイルを用いて作成されたデジタル分類器:インベントリ1を示す.コマンド式デジタル分類器
import java.util.HashSet;  
import java.util.Iterator; 
import java.util.Set;   
import static java.lang.Math.sqrt;  

public class ClassifierAlpha {  

    private int number;  

    public ClassifierAlpha(int number) {    
        this.number = number;  
    }  
   
    public boolean isFactor(int potential_factor) {   
        return number % potential_factor == 0;     
    }  
   
    public Set factors() {     
        HashSet factors = new HashSet();     
        for (int i = 1; i <= sqrt(number); i++){    
            if (isFactor(i)) {    
                factors.add(i);  
                factors.add(number / i);    
            }    
        }
        return factors;
    }  
 
    static public int sum(Set factors) {   
        Iterator it = factors.iterator();    
        int sum =  0;  
        while (it.hasNext())  
            sum += (Integer) it.next();   
        return sum;  
    }
  
    public boolean isPerfect() {  
        return sum(factors()) - number == number;    
    }  
   
    public boolean isAbundant() {  
        return sum(factors()) - number > number;  
    }  
  
    public boolean isDeficient() {      
        return sum(factors()) - number < number;  
    }      
} 

リスト2のコードは、素数を検出するスキームを示している.
リスト2.素数テスト、コマンドで作成
import java.util.HashSet;  
import java.util.Set;  
import static java.lang.Math.sqrt; 

public class PrimeAlpha {  

    private int number; 
     public PrimeAlpha(int number) {
        this.number = number;  
    }  

    public boolean isPrime() {  
        Set primeSet = new HashSet(){{          
            add(1); 
            add(number);
        }}; 
        return number > 1 && factors().equals(primeSet);  
    }

    public boolean isFactor(int potential_factor) {  
        return number % potential_factor == 0;  
    }  

    public Set factors() {  
        HashSet factors = new HashSet();  
        for (int i = 1; i <= sqrt(number); i++){
            if (isFactor(i)) {  
                factors.add(i);  
                factors.add(number / i);  
            }
        }
        return factors;  
    }  
}

インベントリ2には、まずisPrime()メソッドの初期化コードが尋常ではない点に注意すべき点がいくつか示されており、これはインスタンス初期化器の例である.リスト2の興味深い他の部分はisFactor()およびfactors()メソッドである.これらは(リスト1の)ClassifierAlphaクラスの対応する部分と同じであり、2つのソリューションを別々に実現する自然な結果であり、同じ機能を使用していることに気づくことができます.
再構築による重複除外
このような繰り返しの解決策は、リスト3に示すように、単一のFactorsクラスを使用してコードを再構築することである.
リスト3.一般再構成後の因子抽出コード
import java.util.Set;
import static java.lang.Math.sqrt;
import java.util.HashSet;

public class FactorsBeta {
    protected int number;

    public FactorsBeta(int number) {
        this.number = number;
    }

    public boolean isFactor(int potential_factor) {
        return number % potential_factor == 0;
    }

    public Set getFactors() {
        HashSet factors = new HashSet();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }

}


インベントリ3のコードは、抽出スーパークラス(Extract Superclass)という再構成手法を用いた結果であり、2つの抽出手法はnumberというメンバー変数を用いているため、スーパークラスにも置かれていることに注意が必要である.この再構築を実行すると、IDEはアクセス(アクセサペア、保護範囲など)の処理方法を尋ね、numberをクラスに追加し、構造関数を作成して値を設定するprotectedという役割ドメインを選択しました.
重複するコードを削除すると、デジタル分類器と素数テスタの両方が簡単になります.リスト4は、再構成後のデジタル分類器を示し、リスト5は、再構成後素数測定器を示す. 
リスト4.再構築後に簡略化されたデジタル分類器
import java.util.Iterator;   
import java.util.Set;  
 
public class ClassifierBeta extends FactorsBeta {  
  
    public ClassifierBeta(int number) {  
         super(number);    
    }  
 
    public int sum() {  
         Iterator it = getFactors().iterator();        
        int sum =0 ;     
        while (it.hasNext())     
            sum += (Integer) it.next();     
        return sum;     
    }  
   
    public boolean isPerfect() {     
        return sum() - number == number;     
    }     
    public boolean isAbundant() {  
          return sum() - number > number;  
    }  
   
    public boolean isDeficient() {  
       return sum() - number < number;  
    }  
  
} 

リスト5.再構成後に簡略化された素数測定器
import java.util.HashSet;
import java.util.Set;

public class PrimeBeta extends FactorsBeta {
    public PrimeBeta(int number) {
        super(number);
    }

    public boolean isPrime() {
        Set primeSet = new HashSet() {{
            add(1);
            add(number);
        }};
        return getFactors().equals(primeSet);
    }
} 

再構築時にnumberメンバーに選択したアクセスオプションがどれであるかにかかわらず、クラス間のネットワーク関係を処理する必要があります.通常、これは良いことです.問題の一部を独立させることができますが、親クラスを変更する際に不利な結果をもたらします.これは結合(coupling)によってコードを再利用する例である:numberドメインという共有状態とスーパークラスのgetFactors()メソッドによって2つの要素(本例ではクラス)を結合する.すなわち,この手法が機能するのは,言語に組み込まれた結合規則を利用するためである.オブジェクト向けに結合されたインタラクション方式(たとえば、メンバー変数にアクセスする方法を継承することによって)が定義されているため、物事がどのようにインタラクションするかについての事前定義されたスタイルがあります.これは問題ありません.一貫した方法で動作を推理することができるからです.私を誤解しないでください.私は継承の使用が悪い考えであることを暗示しているわけではありません.逆に、オブジェクト向けの言語で過度に使用され、結果的に別のより良い特性を持つ抽象に取って代わったという意味です.
コンビネーション経由のコード再利用
この一連の第2の部分では、リスト6に示すように、Javaで記述されたデジタル分類器の関数バージョンを示します.
リスト6.デジタル分類器のより関数化されたバージョン
public class FClassifier {
    static public boolean isFactor(int number, int potential_factor) {
        return number % potential_factor == 0;
    }

    static public Set factors(int number) {
        HashSet factors = new HashSet();
        for (int i = 1; i <= sqrt(number); i++)
            if (isFactor(number, i)) {
                factors.add(i);
                factors.add(number / i);
            }
        return factors;
    }

    public static int sumOfFactors(int number) {
        Iterator it = factors(number).iterator();
        int sum = 0;
        while (it.hasNext())
            sum += it.next();
        return sum;
    }

    public static boolean isPerfect(int number) {
        return sumOfFactors(number) - number == number;
    }

    public static boolean isAbundant(int number) {
        return sumOfFactors(number) - number > number;
    }

    public static boolean isDeficient(int number) {
        return sumOfFactors(number) - number < number;
    }
} 

素数テスタの関数式バージョン(純粋な関数を使用し、共有状態がない)もあります.このバージョンのisPrime()メソッドはリスト7に示されています.残りのコードは、リスト6の同じネーミング方法のコードと同じである.
リスト7.素数テスタの関数バージョン
public static boolean isPrime(int number) {  
  
    Set factorsfactors = factors(number);  
   
    return number > 1      
           && factors.size() == 2
           && factors.contains(1)
           && factors.contains(number);     
}  

コマンドバージョンで行ったように、繰り返しコードを独自のFactorsクラスに抽出し、可読性に基づいてfactorsメソッドの名前をofに変更しました.図8に示すように、
インベントリ8関数式の再構築後のFactorsクラス
import java.util.HashSet;    
import java.util.Set;   
import static java.lang.Math.sqrt; 
   
public class Factors {    
    static public boolean isFactor(int number, int potential_factor) {     
        return number % potential_factor == 0;     
    }
    
    static public Set of(int number) {   
        HashSet factors = new HashSet();     
        for (int i = 1; i <= sqrt(number); i++)    
            if (isFactor(number, i)) {     
                factors.add(i);     
                factors.add(number / i);     
            }     
        return factors;     
    }   
} 

関数バージョンのすべてのステータスがパラメータとして渡されるため、抽出されたコンテンツには共有ステータスがありません.このクラスを抽出すると、関数式の分類器と素数テスタを再構成して使用することができます.インベントリ9は、再構成された分類器を示している.
リスト9.再構築されたデジタル分類器
public class FClassifier {

    public static int sumOfFactors(int number) {
        Iterator it = Factors.of(number).iterator();
        int sum = 0;
        while (it.hasNext())
            sum += it.next();
        return sum;
    }

    public static boolean isPerfect(int number) {
        return sumOfFactors(number) - number == number;
    }

    public static boolean isAbundant(int number) {
        return sumOfFactors(number) - number > number;
    }

    public static boolean isDeficient(int number) {
        return sumOfFactors(number) - number < number;
    }
} 

リスト10は、再構成された素数テスタを示している.
リスト10.再構成後の素数測定器
import java.util.Set;

public class FPrime {
    public static boolean isPrime(int number) {
        Set factors = Factors.of(number);
        return number > 1 && factors.size() == 2 && factors.contains(1) && factors.contains(number);
    }
}

2番目のバージョンをより関数化するために特別なライブラリや言語を使用していないことに注意してください.逆に、結合式のコード再利用ではなく組み合わせを使用することで、これを行いました.インベントリ9とインベントリ10はいずれもFactorsクラスを使用しているが、その使用は完全に個別のメソッドの内部に含まれている.
結合と組合せの違いは細かく重要であり,このような簡単な例では,現れるコード構造スケルトンを見ることができる.しかし、最終的に大規模なコードライブラリを再構築すると、オブジェクト向け言語での再利用メカニズムの一つであるため、結合はどこにもありません.複雑な結合構造の理解が困難なため、オブジェクト向け言語の再利用性が損なわれ、有効な再利用をオブジェクト-リレーションシップマッピングやコンポーネントライブラリなどの明確に定義された技術分野に限定し、明らかに構造化されたJavaコードを少量書いた場合(例えば、ビジネスアプリケーションで作成したコード)、このようなレベルの再利用は使用できません.
このような方法で、再構築中にIDEによって提供されるコンテンツを通知し、遠慮なく拒否し、組み合わせを使用して代替するコマンドバージョンを改善することができます.
終わりの言葉
より関数化されたプログラミング者として考えることは、符号化の様々な側面を異なる方法で考えることを意味する.コード再利用は明らかに開発の目標であり,命令式抽象は関数式プログラマとは異なる方法でこの問題を解決する傾向にある.この部分では,継承結合方式とパラメータ経由の組合せ方式の2つの方式をコード再利用と比較した.