Haskellの典型的なOOP問題を解決する例

典型的なタスク、通常「OOP-eshnyh」と見なされるタスクの1つを考えてください。 同じ構造を持たないデータ(オブジェクト)のリスト(科学的には異種のリスト)があり、さらに、それぞれに対して同じアクションを実行する必要があります-単純なものの場合、それぞれを特定の関数に渡すことができます。 最初に思い浮かぶのはGUI要素ですが、たとえば、それらは適切ではありません。大きなパッケージを接続する必要があり、コードが多くのスペースを占有します。これはHaskellのOOPエッセンスとは関係ありません。



グラフィックプリミティブ(長方形と円)に単純化できます。 しかし、グラフィックの表示も注意をそらすでしょう。 おそらく、私はまだそれを単純化するでしょう。 最終アクションを端末へのメッセージの出力とします。例えば



paint rectangle, Rect {left = 10, top = 20, right = 600, bottom = 400}





paint circle, radius=150 and centre=(50,300)







そして、親愛なる読者は想像力を結び付けます。



そのため、図を説明する2種類のデータを定義します( 注:問題を解決する方法は多数あります。この記事へのコメントにいくつかの選択肢があります )。

 data Rect = Rect { left :: Int , top :: Int , right :: Int , bottom :: Int } deriving Show data Circle = Circle { x :: Int , y :: Int , radius :: Int }
      
      





次に、それらを組み合わせて異種リストにする方法を決定する必要があります。 代数データ型(ATD)による統合

 data Figures = RectFigure Rect | CircleFigure Circle
      
      





望ましくない。 呼び出しごとにデザイナーを検索する必要があることに加えて、ADTでは、新しい図形を追加するたびにデザイナーを変更する必要があります。 C ++基本クラス、OOP階層で、子孫を追加するときに変更を加える必要がありますか? 適切に設計されたものは必要ありません。 ええ、Haskellではもっと良くなるはずです!



Haskellには既に型クラスの継承と型クラスのインスタンス化があり、これも継承と考えることができます。

これは、例として思いついた「ねじれ」を伴う基本クラスです。

 class Paint a where paint:: a -> Handle -> IO () paint o handle = hPutStrLn handle $ "paint " ++ say o ++ " S=" ++ show ( circumSquare o ) say:: a -> String --     circumSquare:: a -> Int --   .   
      
      





型の各インスタンスの外部関数は、このクラスに直接実装されているpaint :: a-> Handle-> IO()を呼び出します。 グラフィックコンテキストへのポインタやキャンバスの代わりに、簡略化された「描画」関数はファイルハンドルを受け入れます。 文字列「ペイント」、 say関数(仮想関数のメカニズムをシミュレート)から受け取る出力オブジェクトの説明、および記述された長方形の領域を表示します。 なぜ面積ですか? さらに、なぜそれが必要なのかがわかります。



便利なRecordWildCards拡張機能を接続し、型の基本クラスインスタンスを説明します。

 instance Paint Rect where say r = "rectangle, " ++ show r circumSquare (Rect {..}) = ( right - left ) * ( bottom - top ) instance Paint Circle where say (Circle {..}) = "circle, radius=" ++ show radius ++ " and centre=(" ++ show x ++ "," ++ show y ++ ")" circumSquare (Circle {..}) = (2*radius)^2
      
      





これまでのところ、すべてがシンプルです。 Circleでは派生ショーを使用せず、「手動で線」を形成したので、したかったのです。 残りは特別なものではありません。 異なるタイプを1つのリストに結合することは残ります。 これを行うには、 ExistentialQuantification拡張機能を使用します。これにより、特定のタイプのインスタンス(インスタンス)の関数をデータと組み合わせることができます。 これを行うには、単純なヘルパータイプを作成する必要があります。

 data Figure = forall a. Paint a => Figure a
      
      





「スペル」 forall a。 Paint aは、特定の型aのデータとともに、この型のPaintクラスの関数もラップされることを意味します(もちろん、コンパイラーは、Figureコンストラクターの引数型がPaintクラスのインスタンスであることを要求します)。

すべて一緒に
 {-# LANGUAGE ExistentialQuantification, RecordWildCards #-} import System.IO import Control.Monad class Paint a where paint:: a -> Handle -> IO () paint o handle = hPutStrLn handle $ "paint " ++ say o ++ " S=" ++ show ( circumSquare o ) say:: a -> String --     circumSquare:: a -> Int --   .    data Rect = Rect { left :: Int , top :: Int , right :: Int , bottom :: Int } deriving Show instance Paint Rect where say r = "rectangle, " ++ show r circumSquare (Rect {..}) = ( right - left ) * ( bottom - top ) data Circle = Circle { x :: Int , y :: Int , radius :: Int } instance Paint Circle where say (Circle {..}) = "circle, radius=" ++ show radius ++ " and centre=(" ++ show x ++ "," ++ show y ++ ")" circumSquare (Circle {..}) = (2*radius)^2 data Figure = forall a. Paint a => Figure a lst :: [Figure] lst = [Figure (Rect 10 20 600 400), Figure (Circle 50 300 150)] main = forM_ lst $ \ (Figure obj) -> paint obj stdout
      
      







たとえば、三角形を追加するのは簡単です。 非常に似ているものを追加するのは興味深いことです。その実装はコードの重複につながり、重複コードを除外しようとします。



角丸長方形を取ります。 例の重複コードは、記述された長方形の面積の計算です。

Haskellは(OOP言語とは異なり)構造を含むデータ型の構築、拡張(OOP-eshny継承による)を許可しません。 新しい構造に長方形を記述する構造を埋め込む必要があります。

 data Roundrect = Roundrect { baseRect :: Rect , roundR :: Int } instance Paint Roundrect where say (Roundrect {..}) = "round rectangle, " ++ show baseRect ++ " and roundR=" ++ show roundR circumSquare (Roundrect {..}) = circumSquare baseRect
      
      





すべてがすばらしいように思えます。インスタンスPaint Rectのコードを使用して、 インスタンスPaint Roundrectに新しい関数を実装します 。 しかし、実際のプロジェクトではRectから42の継承があり、 Rect 28では、 Rect型とその継承の両方で同じことを行う関数が定義されていると想像してください。 私は次のような関数を何度も書かなければなりません

 circumSquare (Roundrect {..}) = circumSquare baseRect -- …. funN (TypeM {..}) = funN baseRect
      
      





退屈です。 Paintクラスの中間インスタンスの作成を要求します。このインスタンスには、すべての継承に共通のコードが実装され、個別のクラスに実装されていても一意です。 {-#LANGUAGE TypeFamilies#-}を使用してオンになっているデータファミリーを使用して両方のクラスを接続します(もちろん、 タイプファミリーもオンになっています)。

長方形のファミリーを定義します。

 data family RectFamily a
      
      





そして、このファミリーを使用するクラス

 class PaintRect a where getRect :: RectFamily a -> Rect rectSay :: RectFamily a -> String
      
      





クラスでは、私が約束したように、各長方形のユニークな機能が実装されます。 getRectは、型で非表示になっている場合は常に 、四角形の座標を返します。 rectSayは、以前に定義された四角形の発言です。



これで、ファミリーのPaintクラスのインスタンスになりました。反対に、関数はすべての長方形で同一です。

 instance PaintRect a => Paint (RectFamily a) where say = rectSay circumSquare w = let (Rect {..}) = getRect w in ( right - left ) * ( bottom - top )
      
      





ご覧のとおり 、上記のrectSayを呼び出すだけです。 そして、記述された長方形の面積は、すべての長方形で同じように計算されます(少なくとも例ではそうです)。



形状のタイプごとに、新しいコンストラクター(この場合はRectWrap)の名前を考え出す必要があります。

 data instance RectFamily Rect = RectWrap Rect instance PaintRect Rect where getRect (RectWrap r) = r rectSay (RectWrap r) = "rectangle, " ++ show r
      
      





Rectの場合、すべてがシンプルです。 getRectは、 RectWrapからデプロイされたRect自体を返します。 rectSay関数も簡単です。 ところで、それは書くことができ、どのように

  rectSay w = "rectangle, " ++ show (getRect w)
      
      





Roundrectは少し複雑です。

 data instance RectFamily Roundrect = RoundrectWrap Roundrect instance PaintRect Roundrect where getRect (RoundrectWrap r) = baseRect r rectSay (RoundrectWrap (Roundrect {..})) = "round rectangle, " ++ show baseRect ++ " and roundR=" ++ show roundR
      
      





最後に、すべて一緒に、少しくしでした。 たとえば、Figureタイプのコンストラクター関数が追加されました。

完全な最終コード
 {-# LANGUAGE ExistentialQuantification, RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} import System.IO import Control.Monad class Paint a where paint:: a -> Handle -> IO () paint o handle = hPutStrLn handle $ "paint " ++ say o ++ " S=" ++ show ( circumSquare o ) say:: a -> String --     circumSquare:: a -> Int --   .    data Figure = forall a. Paint a => Figure a data Rect = Rect { left :: Int , top :: Int , right :: Int , bottom :: Int } deriving Show data family RectFamily a class PaintRect a where getRect :: RectFamily a -> Rect rectSay :: RectFamily a -> String instance PaintRect a => Paint (RectFamily a) where say = rectSay circumSquare w = let (Rect {..}) = getRect w in ( right - left ) * ( bottom - top ) data instance RectFamily Rect = RectWrap Rect instance PaintRect Rect where getRect (RectWrap r) = r rectSay w = "rectangle, " ++ show (getRect w) mkRect:: Int -> Int -> Int -> Int -> Figure mkRect ltrb = Figure $ RectWrap (Rect ltrb) data Circle = Circle { x :: Int , y :: Int , radius :: Int } instance Paint Circle where say (Circle {..}) = "circle, radius=" ++ show radius ++ " and centre=(" ++ show x ++ "," ++ show y ++ ")" circumSquare (Circle {..}) = (2*radius)^2 mkCircle:: Int -> Int -> Int -> Figure mkCircle xyr = Figure $ Circle xyr --       .  .  data Roundrect = Roundrect { baseRect :: Rect , roundR :: Int } data instance RectFamily Roundrect = RoundrectWrap Roundrect instance PaintRect Roundrect where getRect (RoundrectWrap r) = baseRect r rectSay (RoundrectWrap (Roundrect {..})) = "round rectangle, " ++ show baseRect ++ " and roundR=" ++ show roundR mkRoundrect:: Int -> Int -> Int -> Int -> Int -> Figure mkRoundrect ltrb rr = Figure $ RoundrectWrap $ Roundrect (Rect ltrb) rr --    . lst :: [Figure] lst = [ mkRect 10 20 600 400, mkCircle 50 300 150, mkRoundrect 30 40 500 200 5 ] --    . main = forM_ lst $ \ (Figure obj) -> paint obj stdout
      
      








All Articles