典型的なタスク、通常「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
型の各インスタンスの外部関数は、このクラスに直接実装されている
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
たとえば、三角形を追加するのは簡単です。 非常に似ているものを追加するのは興味深いことです。その実装はコードの重複につながり、重複コードを除外しようとします。
角丸長方形を取ります。 例の重複コードは、記述された長方形の面積の計算です。
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
退屈です。
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