7

下面似乎工作......但它似乎很笨拙。

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

现在,似乎首选的方法就是简单地拥有

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 ... }

然而...

我们喜欢在整个绘图过程中保留颜料并对其进行变异,因为它们的创建和销毁成本很高。绘画可能只是一个像 [bgColor red, fgColor blue, font "Tahoma"] 之类的列表,或者它可能是指向绘画系统使用的内部结构的指针(这是对 windows GDI 的抽象,但最终会抽象在 direct2d 和 coregraphics 上),它们具有“绘制”对象,我不想一遍又一遍地重新创建然后绑定。

在我看来,存在主义的美妙之处在于它们可以不透明地包裹一些东西以抽象它,我们可以将它保存在某个地方,将它拉回来,无论如何。当您部分应用时,我认为存在您部分应用的东西现在“卡在”容器内的问题。这是一个例子。说我有一个像这样的油漆对象

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

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

4

1 回答 1

9

界面

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

  • 有些画布上可以有形状
  • 有些画布上可以放文字
  • 有些画布会根据颜料改变他们的所作所为
  • 我们还不知道颜料是什么,但是对于不同的画布它们会有所不同

现在我们将这些想法转化为 Haskell。Haskell 是一种“类型优先”的语言,所以当我们谈论需求和设计时,我们可能正在谈论类型。

  • 在 Haskell 中,当我们在谈论类型时看到“some”这个词时,我们会想到类型类。例如,show该类说“某些类型可以表示为字符串”。
  • 当我们谈论我们还不知道的东西时,在谈论需求时,这是一种我们还不知道它是什么的类型。那是一个类型变量。
  • “穿上它们”似乎意味着我们将拥有一块画布,在上面放一些东西,然后再拥有一块画布。

现在我们可以为这些要求中的每一个编写类:

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

类型变量c只使用一次,显示为c -> c. 这表明我们可以通过替换来使这些更c -> c通用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

现在PaintCanvas看起来class在 Haskell 中有问题。类型系统很难弄清楚类中发生了什么

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

我会通过改变PaintCanvas以利用TypeFamilies扩展来缓解这种情况。

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

现在,让我们为我们的界面整理所有内容,包括形状和文本的数据类型(修改为对我有意义):

{-# 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

一些例子

本节将介绍对有用画布的附加要求,除了我们已经制定的要求。它类似于我们在画布类中替换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会很完美。我能想到的提供 this 的最简单的 Haskell 类是Monoid a's mappend :: a -> a -> a. AMonoid需要身份和关联性。假设在画布上进行绘图操作而使它们保持不变是否合理?这听起来很合理。假设三个绘图操作以相同的顺序完成,即使前两个一起执行,然后是第三个,或者如果第一个执行,然后第二个和第三个一起执行,这是否合理? ? 再一次,这对我来说似乎很合理。这建议我们可以写成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])

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

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

这不太有效,因为我们没有实例,(Monad m) => Monoid (m ())所以它ex可以工作。我们可以Data.Semigroup.Monad从 reducers 包中使用,或者自己添加一个,但这会使我们陷入不连贯的实例中。将 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]

但是类型系统不能完全确定第一个draw单元与第二个单元相同。我们在这里遇到的困难表明了额外的要求,我们一开始还不能完全确定:

  • 画布扩展了现有的操作序列,提供绘图、书写文本等操作。

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

  • 当你听到“指令序列”时,你应该想:“monad”。
  • 当你用“扩展”来限定它时,你应该认为:“单子变压器”。

现在我们意识到我们的画布实现很可能是一个单子转换器。我们可以回到我们的界面,改变它,使每个类都是一个 monad 的类,类似于转换器的MonadIO类和 mtl 的 monad 类。

界面,重新审视

{-# 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 ()

例子,重温

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

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 ()]

我们也可以使用油漆做一个例子。由于我们不知道将存在哪些油漆,它们都必须在外部提供(作为示例的参数):

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)

一个实现

如果你可以让你的代码在不引入抽象的情况下使用各种绘画来绘制点、框、线和文本,我们可以将其更改为实现第一节中的接口。

于 2013-09-28T17:48:18.733 回答