存在反模式,如何避免 [英] Existential antipattern, how to avoid

查看:121
本文介绍了存在反模式,如何避免的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述



  data Point = Point Int Int 
数据Box = Box Int Int
data Path = Path [Point]
data Text = Text

data Color = Color Int Int Int
data WinPaintContext = WinPaintContext Graphics。 Win32.HDC

class CanvasClass vc paint where
drawLine :: vc - >油漆 - >点 - >点 - > IO()
drawRect :: vc - >油漆 - >框 - > IO()
drawPath :: vc - >油漆 - >路径 - > IO()

class(CanvasClass vc paint)=> TextBasicClass vc paint where
basicDrawText :: vc - >油漆 - >点 - >字符串 - > IO()

实例CanvasClass WinPaintContext WinPaint其中
drawLine =未定义
drawRect =未定义
drawPath =未定义

实例TextBasicClass WinPaintContext WinPaint其中
basicDrawText(WinPaintContext a)= winBasicDrawText a

op :: CanvasClass vc paint => vc - >框 - > IO()
操作画布_ =做
basicDrawText画布WinPaint(点30 30)Hi

open :: IO()
open = do
makeWindow(Box 300 300)op

winBasicDrawText :: Graphics.Win32.HDC - > WinPaint - >点 - >字符串 - > IO()
winBasicDrawText hdc _(Point xy)str = do
Graphics.Win32.setBkMode hdc Graphics.Win32.tRANSPARENT
Graphics.Win32.setTextColor hdc(Graphics.Win32.rgb 255 255 0)
Graphics.Win32.textOut hdc 20 20 str
return()
$ b $ windowsOnPaint ::(WinPaintContext - > Box - > IO()) - >
Graphics.Win32.RECT - >
Graphics.Win32.HDC - >
IO()
windowsOnPaint fctct hdc = f(WinPaintContext hdc)(方框30 30)

makeWindow :: Box - > (WinPaintContext - > Box - > IO()) - > IO()
makeWindow(Box w h)onPaint =
Graphics.Win32.allocaPAINTSTRUCT $ \ lpps - >做
hwnd< - createWindow wh(wndProc lpps(windowsOnPaint onPaint))
messagePump hwnd

现在,看起来最好的方法是简单地拥有

 数据Canvas = Canvas {
drawLine :: Point - >点 - > IO(),
drawRect :: Box - > IO(),
drawPath :: Path - > IO()
}

hdc2Canvas :: Graphics.Win32.HDC - >油漆 - > IO(Canvas)
hdc2Canvas hdc paint = Canvas {drawLine = winDrawLine hdc paint ...}

但是......



我们喜欢在绘画过程中保留绘画并改变它们,因为它们创建和销毁的代价很高。一个paint可以是一个像[bgColor red,fgColor blue,字体Tahoma]之类的列表,或者它可以是一个指向内部结构的指针,这个指针是绘制系统使用的(这是对GDI的抽象,但最终会抽象通过direct2d和coregraphics),它们具有绘画对象,我不想一遍又一遍地重新创建,然后绑定。



我心中存在的美是他们可以不透明地包装某些东西来抽象它,并且我们可以将它保存在某个地方,然后将其拉回来,无论如何。当你部分申请时,我认为存在的问题是你部分申请的东西现在卡在容器内。这是一个例子。假设我有一个绘图对象,例如:

  data Paint = Paint {
setFg :: Color - > IO(),
setBg :: Color - > IO()
}

我可以在何处放置指针?当我将Paint赋予Canvas中的某个函数时,他如何获得指针?什么是设计此API的正确方法?

解决方案

界面 b

首先,您需要问我的要求是什么?。让我们用简单的英语陈述我们想要一个画布做什么(这些是根据你的问题的猜测):

$ ul

  • 一些画布可以有形状放在它们上

  • 一些画布上可以放置文字

  • 一些画布根据颜色改变它们的作用

  • 我们不知道现在有什么颜料,但是对于不同的画布,它们会有所不同



  • 现在我们翻译这些想法进入Haskell。 Haskell是一种类型优先的语言,所以当我们谈论需求和设计时,我们可能在谈论类型。




    • 在Haskell,当我们在谈论类型时看到some这个词时,我们想到了类型类。例如, show 类表示某些类型可以表示为字符串。

    • 当我们谈论某些事情时,我们不会在谈论需求时,我们还不清楚,这是一种我们不知道它是什么的类型。这是一个类型变量。

    • 放在它们上面似乎意味着我们需要一个画布,在其上放置一些东西,然后再次放置画布。



    现在我们可以为每个需求编写类:

      class ShapeCanvas c其中-c是Canvas的类型
    draw :: Shape - > c - > c

    class TextCanvas c其中
    write :: Text - > c - > c

    class PaintCanvas p c其中 - p是Paint的类型
    load :: p - > c - > c

    仅使用类型变量 c 一次,出现为 c - > ç。这表明我们可以通过替换 c - > c with c

      class ShapeCanvas c其中-c是画布的类型
    draw :: Shape - > c

    class TextCanvas c其中
    write :: Text - > c

    class PaintCanvas p c其中 - p是绘画的类型
    load :: p - > c

    现在 PaintCanvas 看起来像一个 class 在Haskell中存在问题。类型系统很难找出像

    这样的类中发生了什么。

      class隐式地ab其中
    convert :: b - > a

    我通过更改 PaintCanvas 利用 TypeFamilies 扩展名。

      class PaintCanvas c where 
    类型Paint c :: * - (Paint c)是类型为c
    的画布的Paint类型load ::(Paint c) - > c

    现在,让我们将所有界面放在一起,包括形状和文本的数据类型对我有意义):

      { - #LANGUAGE TypeFamilies# - } 

    模块数据。 Canvas(
    Point(..),
    Shape(..),
    Text(..),
    ShapeCanvas(..),
    TextCanvas(..) ),
    PaintCanvas(..)
    )其中

    数据点=点Int Int

    数据Shape =点点
    | Box Point Point
    | Path [Point]

    data Text =文本点字符串

    类ShapeCanvas c其中 - c是Canvas的类型
    draw :: Shape - > c

    class TextCanvas c其中
    write :: Text - > c

    类PaintCanvas c其中
    类型Paint c :: * - (Paint c)是类型c
    的画布的Paint类型load ::(Paint c) - > c

    一些示例

    除了我们已经制定的内容之外,本节还将介绍对有用的画布的额外要求。这是我们在取代 c - >时所遗失的类似物。在画布类中使用 c



    让我们从第一个示例代码开始, op 。使用我们的新界面,它很简单:

      op ::(TextCanvas c)=> c 
    op = write $ Text(Point 30 30)Hi

    让我们做一个比较复杂的例子。如何绘制X的东西?我们可以做出X的第一笔划。

      ex ::(ShapeCanvas c)=> c 
    ex = draw $ Path [Point 10 10,Point 20 20]

    但是我们无法为横向笔画添加另一个 Path 。我们需要一些方法将两个绘图步骤放在一起。类型为 c - > c - > c 将是完美的。我能想到的最简单的Haskell类是 Monoid a mappend :: a - > a - >一个。一个 Monoid 需要一个身份和关联性。假设画布上有绘画操作会使它们保持原样,这是否合理?这听起来很合理。假设三个绘图操作按照相同的顺序执行,即使前两个一起执行,然后执行第三个绘图操作,或者如果第一个执行,然后第二个和第三个一起执行,是否合理?再次,这对我来说似乎很合理。这表明我们可以将 ex 写为:

      ex ::(Monoid c,ShapeCanvas c)=> c 
    ex =(draw $ Path [Point 10 10,Point 20 20])`mappend`(draw $ Path [Point 10 20,Point 20 10])

    最后,让我们考虑一些交互式的东西,它决定了根据外部的东西绘制什么:

    <$ p $ (MonadIO m,ShapeCanvas(m()),TextCanvas(m()))=> m()
    randomDrawing = do
    index< - liftIO。 getStdRandom $ randomR(0,2)
    选项!! index
    where options = [op,ex,return()]

    相当有效,因为我们没有(Monad m)=>的实例。 Monoid(m())使得 ex 可以工作。我们可以使用reducer包中的 Data.Semigroup.Monad ,或者自己添加一个,但这会让我们处于不连贯的情况。将ex改为:

      ex ::(Monad m,ShapeCanvas(m()))=> ; m()
    ex = do
    draw $ Path [Point 10 10,Point 20 20]
    draw $ Path [Point 10 20,Point 20 10]


    $ b

    但是类型系统不能完全弄清楚,从第一个的单元绘制与第二个单位相同。我们在这里遇到的困难提出了额外的要求,我们首先不能完全明白:


    • 画布扩展了现有的操作顺序,操作绘图,书写文字等。



    直接从 http://www.haskellforall.com/2013/06/from-zero-to-cooperative-threads- in-33.html


    • 当您听到指令序列时,您应该考虑:monad。 li>
    • 当您使用extend限定时,您应该考虑:monad transformer。


    现在我们意识到我们的画布实现很可能将成为一个monad变换器。我们可以回到我们的界面,并改变它,以便每个类都是monad类,类似于变换器的 MonadIO 类和mtl的monad类。 p>

    接口,重新访问

      { - #LANGUAGE TypeFamilies# - } 

    模块Data.Canvas(
    Point(..),
    Shape(..),
    Text(..),
    ShapeCanvas(..),
    TextCanvas(..),
    PaintCanvas(..)
    )其中

    数据点= Point Int Int

    data Shape =点数
    | Box Point Point
    |路径[Point]

    data Text = Text Point String

    class Monad m => ShapeCanvas m其中-c是Canvas的类型
    draw :: Shape - > m()

    class Monad m => TextCanvas m其中
    write :: Text - > m()

    class Monad m => PaintCanvas m其中
    类型Paint m :: * - (Paint c)是类型为c
    的画布的Paint类型load ::(Paint m) - > m()

    示例,重新审视
    $ b

    现在我们所有的示例绘图操作都是在一些未知的 Monad m中的动作:

      op ::(TextCanvas m)=> m()
    op = write $ Text(Point 30 30)Hi

    ex ::(ShapeCanvas m)=> m()
    ex = do
    draw $ Path [Point 10 10,Point 20 20]
    draw $ Path [Point 10 20,Point 20 10]


    randomDrawing ::(MonadIO m,ShapeCanvas m,TextCanvas m)=> m()
    randomDrawing = do
    index< - liftIO。 getStdRandom $ randomR(0,2)
    选项!! index
    where options = [op,ex,return()]

    我们也可以制作一个使用油漆的例子。由于我们不知道哪种颜料会存在,因此它们都必须在外部提供(作为该例子的参数):

      checkerBoard ::(ShapeCanvas m,PaintCanvas m)=> Paint m  - > Paint m  - > m()
    checkerBoard红色黑色=
    do
    加载红色
    绘制$ Box(点10 10)(点20 20)
    绘制$ Box(点20 20 )(点30 30)
    加载黑色
    画出$ Box(Point 10 20)(Point 20 30)
    draw $ Box(Point 20 10)(Point 30 20)

    执行

    如果您可以使代码在不引入抽象的情况下使用各种颜色绘制点,框,线和文本,我们可以将其更改为从第一部分实现接口。

    The below seems to work... but it seems clumsy.

    data Point = Point Int Int
    data Box = Box Int Int
    data Path = Path [Point]
    data Text = Text
    
    data Color = Color Int Int Int
        data WinPaintContext = WinPaintContext Graphics.Win32.HDC
    
    class CanvasClass vc paint where
        drawLine :: vc -> paint -> Point -> Point -> IO ()
        drawRect :: vc -> paint -> Box -> IO ()
        drawPath :: vc -> paint -> Path -> IO ()
    
    class (CanvasClass vc paint) => TextBasicClass vc paint where
        basicDrawText :: vc -> paint -> Point -> String -> IO ()
    
    instance CanvasClass WinPaintContext WinPaint where
        drawLine = undefined
        drawRect = undefined
        drawPath = undefined
    
    instance TextBasicClass WinPaintContext WinPaint where
        basicDrawText (WinPaintContext a) = winBasicDrawText a
    
    op :: CanvasClass vc paint => vc -> Box -> IO ()
    op canvas _ = do
        basicDrawText canvas WinPaint (Point 30 30) "Hi"
    
    open :: IO ()
    open = do
        makeWindow (Box 300 300) op
    
    winBasicDrawText :: Graphics.Win32.HDC -> WinPaint -> Point -> String -> IO ()
    winBasicDrawText hdc _ (Point x y) str = do
        Graphics.Win32.setBkMode hdc Graphics.Win32.tRANSPARENT
        Graphics.Win32.setTextColor hdc (Graphics.Win32.rgb 255 255 0)
        Graphics.Win32.textOut hdc 20 20 str
        return ()
    
    windowsOnPaint :: (WinPaintContext -> Box -> IO ()) ->
                      Graphics.Win32.RECT ->
                      Graphics.Win32.HDC ->
                      IO ()
    windowsOnPaint f rect hdc = f (WinPaintContext hdc) (Box 30 30)
    
    makeWindow :: Box -> (WinPaintContext -> Box -> IO ()) -> IO ()
    makeWindow (Box w h) onPaint =
      Graphics.Win32.allocaPAINTSTRUCT $ \ lpps -> do
      hwnd <- createWindow w h (wndProc lpps (windowsOnPaint onPaint))
      messagePump hwnd
    

    Now, what seems to be the preferred way is to just simply have

    data Canvas = Canvas {
        drawLine :: Point -> Point -> IO (),
        drawRect :: Box -> IO (),
        drawPath :: Path -> IO ()
    }
    
    hdc2Canvas :: Graphics.Win32.HDC -> Paint -> IO ( Canvas )
    hdc2Canvas hdc paint = Canvas { drawLine = winDrawLine hdc paint ... }
    

    HOWEVER...

    We like to keep paints around and mutate them throughout the drawing process, as they're expensive to create and destroy. A paint could just be a list like [bgColor red, fgColor blue, font "Tahoma"] or something, or it could be a pointer to an internal structure the paint system uses (this is an abstraction over windows GDI, but will ultimately abstract over direct2d and coregraphics), which have "paint" objects which I don't wanna recreate over and over and then bind over.

    The beauty of existentials in my mind is that they can opaquely wrap something to abstract over it, and we can save it somewhere, pull it back, whatever. When you partially apply, I think there is the problem that the thing you've partially applied is now "stuck inside" the container. Here's an example. Say I have a paint object like

    data Paint = Paint {
        setFg :: Color -> IO () ,
        setBg :: Color -> IO ()
    }
    

    Where can I place the pointer? When I give the Paint to some function in Canvas, how does he get the pointer? What's the right way to design this API?

    解决方案

    The Interface

    First, you need to ask "What are my requirements?". Let's state in plain English what we want a canvas to do (these are my guesses based on your question):

    • Some canvases can have shapes put on them
    • Some canvases can have text put on them
    • Some canvases change what they do based on a paint
    • We don't know what paints are yet, but they will be different for different canvases

    Now we translate these ideas into Haskell. Haskell is a "types-first" language, so when we are talking about requirements and design, we are probably talking about types.

    • In Haskell, when we see the word "some" while talking about types, we think of type classes. For example, the show class says "some types can be represented as strings".
    • When we talk about something we don't know about yet, while talking about requirements, that's a type where we don't know what it is yet. That's a type variable.
    • "put on them" seems to mean that we'd take have a canvas, put something on it, and have a canvas again.

    Now we could write classes for each of these requirements:

    class ShapeCanvas c where -- c is the type of the Canvas
        draw :: Shape -> c -> c
    
    class TextCanvas c where
        write :: Text -> c -> c
    
    class PaintCanvas p c where -- p is the type of Paint
        load :: p -> c -> c
    

    The type variable c is only used once, appearing as c -> c. This suggests we could make these more general by replacing c -> c with c.

    class ShapeCanvas c where -- c is the type of the canvas
        draw :: Shape -> c
    
    class TextCanvas c where
        write :: Text -> c
    
    class PaintCanvas p c where -- p is the type of paint
        load :: p -> c
    

    Now PaintCanvas looks like a class that is problematic in Haskell. It's hard for the type system to figure out what's going on in classes like

    class Implicitly a b where
        convert :: b -> a
    

    I'd alleviate this by changing PaintCanvas to take advantage of the TypeFamilies extension.

    class PaintCanvas c where 
        type Paint c :: * -- (Paint c) is the type of Paint for canvases of type c
        load :: (Paint c) -> c
    

    Now, let's put together everything for our interface, including your data types for shapes and text (modified to make sense to me):

    {-# LANGUAGE TypeFamilies #-}
    
    module Data.Canvas (
        Point(..),
        Shape(..),
        Text(..),
        ShapeCanvas(..),
        TextCanvas(..),
        PaintCanvas(..)
    ) where
    
    data Point = Point Int Int
    
    data Shape = Dot Point
               | Box Point Point 
               | Path [Point]
    
    data Text = Text Point String
    
    class ShapeCanvas c where -- c is the type of the Canvas
        draw :: Shape -> c
    
    class TextCanvas c where
        write :: Text -> c
    
    class PaintCanvas c where 
        type Paint c :: * -- (Paint c) is the type of Paint for canvases of type c
        load :: (Paint c) -> c
    

    Some Examples

    This section will introduce an additional requirement for useful canvases besides those we have already worked out. It is the analog of what we lost when we replaced c -> c with c in the canvas classes.

    Let's start with your first example code, op. With our new interface it's simply:

    op :: (TextCanvas c) => c
    op = write $ Text (Point 30 30) "Hi"
    

    Let's make a slightly more complicated example. How about something that draws an "X"? We can make the first stroke of the "X"

    ex :: (ShapeCanvas c) => c
    ex = draw $ Path [Point 10 10, Point 20 20]
    

    But we have no way to add another Path for the cross stroke. We need some way to put two drawing steps together. Something with type c -> c -> c would be perfect. The simplest Haskell class I can think of that provides this is Monoid a's mappend :: a -> a -> a. A Monoid requires an identity and associativity. Is it resonable to assume that there's a drawing operation on canvases that leaves them untouched? That sounds quite reasonable. Is it reasonable to assume that three drawing operations, done in the same order, do the same thing even if the first two are performed together, and then the third, or if the first is performed, and then the second and third are performed together? Again, that seems quite reasonable to me. This suggest we can write ex as:

    ex :: (Monoid c, ShapeCanvas c) => c
    ex = (draw $ Path [Point 10 10, Point 20 20]) `mappend` (draw $ Path [Point 10 20, Point 20 10])
    

    Finally, let's consider something interactive, that decides what to draw based on something external:

    randomDrawing :: (MonadIO m, ShapeCanvas (m ()), TextCanvas (m ())) => m ()
    randomDrawing = do
        index <- liftIO . getStdRandom $ randomR (0,2)
        choices !! index        
        where choices = [op, ex, return ()]
    

    This doesn't quite work, because we don't have an instance for (Monad m) => Monoid (m ()) so that ex will work. We could use Data.Semigroup.Monad from the reducers package, or add one ourselves, but that puts us in incoherent instances land. It'd be easier to change ex to:

    ex :: (Monad m, ShapeCanvas (m ())) => m ()
    ex = do
        draw $ Path [Point 10 10, Point 20 20]
        draw $ Path [Point 10 20, Point 20 10]
    

    But the type system can't quite figure out that the unit from the first draw is the same as the unit from the second. Our difficulty here suggests additional requirements, that we couldn't quite put our finger on at first:

    • Canvases extend existing sequences of operations, providing operations for drawing, writing text, etc.

    Stealing directly from http://www.haskellforall.com/2013/06/from-zero-to-cooperative-threads-in-33.html:

    • When you hear "sequence of instructions" you should think: "monad".
    • When you qualify that with "extend" you should think: "monad transformer".

    Now we realize our canvas implementation is most likely going to be a monad transformer. We can go back to our interface, and change it so that each of the classes is a class for a monad, similar to transformers' MonadIO class and mtl's monad classes.

    The Interface, revisited

    {-# LANGUAGE TypeFamilies #-}
    
    module Data.Canvas (
        Point(..),
        Shape(..),
        Text(..),
        ShapeCanvas(..),
        TextCanvas(..),
        PaintCanvas(..)
    ) where
    
    data Point = Point Int Int
    
    data Shape = Dot Point
               | Box Point Point 
               | Path [Point]
    
    data Text = Text Point String
    
    class Monad m => ShapeCanvas m where -- c is the type of the Canvas
        draw :: Shape -> m ()
    
    class Monad m => TextCanvas m where
        write :: Text -> m ()
    
    class Monad m => PaintCanvas m where 
        type Paint m :: * -- (Paint c) is the type of Paint for canvases of type c
        load :: (Paint m) -> m ()
    

    Examples, revisited

    Now all of our example drawing operations are actions in some unknown Monad m:

    op :: (TextCanvas m) => m ()
    op = write $ Text (Point 30 30) "Hi"
    
    ex :: (ShapeCanvas m) => m ()
    ex = do
        draw $ Path [Point 10 10, Point 20 20]
        draw $ Path [Point 10 20, Point 20 10]
    
    
    randomDrawing :: (MonadIO m, ShapeCanvas m, TextCanvas m) => m ()
    randomDrawing = do
        index <- liftIO . getStdRandom $ randomR (0,2)
        choices !! index        
        where choices = [op, ex, return ()]
    

    We can also make an example using paint. Since we don't know what paints will exist, they all have to be provided externally (as arguments to the example):

    checkerBoard :: (ShapeCanvas m, PaintCanvas m) => Paint m -> Paint m -> m ()
    checkerBoard red black = 
        do
            load red
            draw $ Box (Point 10 10) (Point 20 20)
            draw $ Box (Point 20 20) (Point 30 30)
            load black
            draw $ Box (Point 10 20) (Point 20 30)
            draw $ Box (Point 20 10) (Point 30 20)
    

    An Implementation

    If you can make your code work to draw points, boxes, lines and text using various paints without introducing abstraction, we can change it to implement the interface from the first section.

    这篇关于存在反模式,如何避免的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆