🐕

Javaでもう一度学び直すオブジェクト指向プログラミングを関数型プログラミングで考え直す 〜第4章〜

2021/03/24に公開

この記事は前々回前回に引き続き、Software Design 2021年3月号に掲載されているJavaでもう一度学び直すオブジェクト指向プログラミングと言う記事を関数型プログラミング言語Elmで考えたらどうなるだろう?と考えてみた記事です。

元記事4章の内容は、あるクラスが具象クラスに依存してしまう問題をインターフェースを用いることで、依存関係の逆転をすることで設計に柔軟性を持たせようと言う内容でした(例によって雑誌の内容は明記しないので、雑誌を購入して読んでいただくか、依存関係逆転の原則等で調べていただくことをお勧めします)。結論から述べると、関数型プログラミングではインターフェースを用いることなく、ほぼ今までの知識で同様のことを実現できます。(ただし、DIなどの機能を備えていない関数型プログラミング言語の方が多いので、その点は劣る箇所だと思われます。) これぞまさに関数型プログラミングと呼べる部分なので少し難しいかもしれませんが、頑張って理解をしてみてください。

今回のコード全体です(前回のプロジェクトを拡張しています)。

具体的な型に依存している関数

インターフェースに当たる物で抽象的な関数を定義する前に、まずは具体的な型に依存している状態の例を見てみましょう。以下は整数のリストから最小値を見つけるための関数です。アルゴリズムは単純で、listを昇順でソートし先頭の値を取得します。この時、戻り値がInt型ではなくMaybe Int型となっている理由は空のリストが与えられた場合、先頭の値が取得できないからです。取得ができた場合、Just n, 取得できない場合、Nothingと言う値になります。値があるかないかを型で表現でき、その後デフォルト値を渡さない限りIntが取り出せないので、とても型安全な仕組みですね!(Elmは実行時例外が基本起きない言語です)

minimum : List Int -> Maybe Int
minimum list =
    List.head <| List.sort list
    
minimumValue = minumum [5, 3, 1, 4, 2] -- == Just 1

このとき、minimumはList.sortが扱える型に依存しており、さらにはシグネチャより整数のリストしか受け取れない抽象どの低い関数となってしまっています。

では、抽象度を1段階上げるにはどうすればいいでしょうか?ヒントは、List.sort関数のシグネチャに秘密があります。sort関数のシグネチャは以下のようになっております。comparableとは、制約付き型変数と言って、ある型の集合からなる変数です。comparableには、IntかFloat,Char,Stringなどの型が代入できます。

sort : List comparable -> List comparable

つまり、minimumのシグネチャを以下のように変えれば、IntかFloat,Char,Stringなどの型を持つリストを渡せるようになります。

minimum : List comparable -> Maybe comparable
minimum list =
    List.head <| List.sort list
    
minimumValue = minumum ['b', 'c', 'a', 'd'] -- == Just 'a'

それでは、次が本番です。並び替えができる(インターフェースを持つ)任意の型の最小値を求める関数minimumを定義するにはどのようにすればいいのでしょうか?

高階関数を利用したインターフェースの再現

sort関数では、comparableと言う限られた型のみしか並び替えができないと言う問題がありました。任意の型の並び替えができる関数として、sortWith関数があります。sortWith関数のシグネチャは以下のようになります。aは任意の型を表す型変数になります。Javaで言うT型のジェネリクスと同義です。シグネチャを見ると(a -> a -> Order)の部分が括弧で囲われているのがわかるでしょうか?これは単体の型ではなく、関数を表す括弧になります。ガイドにわかりやすい説明があるので詳しく知りたい方はどうぞ。このように関数を受け取る関数を高階関数と言います。

sortWith : (a -> a -> Order) -> List a -> List a

Orderは、大小関係を表すカスタム型となっています。compareble型であればcompare関数で計算することができます。

compare 1 2   -- LT(左の値の方が小さいですよ)
compare 2 2   -- EQ(左右の値は等しいですよ)
compare 3 2   -- GT(左の値の方が大きいですよ)

Javaがお詳しい型にはもうお気づきでしょうか?(a -> a -> Order)は、Comparatorインターフェース#compareと全く同じ定義なのです。

int compare(T o1, T o2)

もっと、Javaに寄せてみましょう。(a -> a -> Order)Comparatorと言う名前を与えてあげれば真に等しいインターフェースとなります。

type alias Comparator a =
    a -> a -> Order

インターフェースを手に入れたので、minimumは晴れて、任意の型の最小値を探す関数に抽象度を進化させることができるのです!もう皆さんには、minimum インターフェース インターフェースを実装した具体の型と言う引数の順番に見えているはずです。関数の柔軟性に気づいていただけたでしょうか?

minimum : Comparator a -> List a -> Maybe a
minimum compF list =
    List.head <| List.sortWith compF list

それではテストコードで例を紹介しましょう。整数の最小値を並び替えるには、intComparatorと言う関数(インターフェース)を必要とします。compare関数がその役割そのものを表すので実態はそれだけです。これだけでは、ただコードが複雑になってしまっただけですが、minimumは整数リストと言う具象型に依存した関数からインターフェースに依存した関数となり、依存関係が逆転しています。そのため信号機を表した型Signalなどの最小値も大小関係を定義することで呼び出すことに成功しています。

intComparator : Comparator Int
intComparator =
    compare


type Signal
    = Green
    | Yellow
    | Red


signalComparator : Comparator Signal
signalComparator s1 s2 =
    case ( s1, s2 ) of
        ( Green, Yellow ) ->
            GT

        ( Green, Red ) ->
            GT

        ( Yellow, Red ) ->
            GT

        ( Yellow, Green ) ->
            LT

        ( Red, Green ) ->
            LT

        ( Red, Yellow ) ->
            LT

        ( _, _ ) ->
            EQ


addTest : Test
addTest =
    describe "#minimum"
        [ describe "[4, 1, 3, 2]のうち、compare関数を基準だと、"
            [ test "最小値は1" <|
                \_ ->
                    [ 4, 1, 3, 2 ]
                        |> minimum intComparator
                        |> Expect.equal (Just 1)
            ]
        , describe "[Green, Yellow, Red]のうち、Green > Yellow > Red の順であれば、"
            [ test "Redが最小値となる" <|
                \_ ->
                    [ Green, Yellow, Red ]
                        |> minimum signalComparator
                        |> Expect.equal (Just Red)
            ]
        ]

このような高階関数はとても多く利用されており、もはやどんな言語でも日常的に使っているのではないか?と思われるList#mapもその一つです。これは任意の型aを任意の型bに変換するメソッドa2b(仮のメソッド名です)を持つインターフェースを実装したa型を受け取る関数mapと読むことができるはずです。

map : (a -> b) -> List a -> List b

map sqrt [1,4,9] == [1,2,3]

map not [True,False,True] == [False,True,False]

Elmと言う言語とフレームワーク

鋭い読者の皆様は、おいおいインターフェースは単一のメソッドではなく複数のメソッドを持つことができて、その実装をアプリケーション開発者にさせることでフレームワークが作れることが真の強みだろう。とおっしゃるかもしれません。もちろんフレームワークのような使い方が高階関数とレコード(JavaScriptのオブジェクトのような機能です。)を用いることでできます。

ところで、Elmは関数型プログラミングの他にWebアプリケーションに特化したアーキテクチャ(The Elm Architecture)を内包したフレームワークであると言う側面があります。そのため、アプリケーションエンドポイントとなるmain関数は次のような使われ方をします。Browser.sandboxはinit, view, updateと言う値や関数を引数として要求をします。これはまさに、Elmがインターフェースの実装を要求するフレームワークと言うことに他なりません。

main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }
    
 type alias Model =
    { count : Int }


initialModel : Model
initialModel =
    { count = 0 }


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+1" ]
        , div [] [ text <| String.fromInt model.count ]
        , button [ onClick Decrement ] [ text "-1" ]
        ]
sandbox :
    { init : model
    , view : model -> Html msg
    , update : msg -> model -> model
    }
    -> Program () model msg

まとめ

この記事では、具体的な型を要求する関数から型変数を使った抽象度の上げ方、さらに関数を受け取る関数である高階関数を使った抽象度の上げ方を学びました。これによりインターフェースを利用したJavaのプログラミングと同等の効果が得られることを実感できたはずです。より抽象的なことを要求される汎用プログラミングを扱う場合には、HaskellやPureScriptなどの言語知識が要求されますが、今回Elmで学んだような知識はそれらを扱うための土台として必須になっていきます。また、Webフロントを扱う実用言語としては十分強力なため、是非試してみてはいかがでしょうか?また、OOP以外のパラダイムもシンプルで強力であることを実感し、プログラマとしての可能性を広げてみてはいかがでしょうか?

次の記事です

Discussion