界面
首先,您需要问“我的要求是什么?”。让我们用简单的英语说明我们想要画布做什么(这些是我根据你的问题的猜测):
- 有些画布上可以放置形状
- 有些画布上可以放置文字
- 有些画布根据颜料改变了它们的用途
- 我们还不知道颜料是什么,但对于不同的画布它们会有所不同
现在我们将这些想法转化为 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
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
看起来像一个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
with 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
's mappend :: a -> a -> a
. A Monoid
需要同一性和关联性。假设在画布上进行了绘图操作而不影响画布是否合理?听起来很有道理。假设以相同顺序完成的三个绘图操作执行相同的操作,即使前两个一起执行,然后第三个,或者如果执行第一个,然后第二个和第三个一起执行,是否合理? ?同样,这对我来说似乎很合理。这建议我们可以写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])
最后,让我们考虑一些交互式的东西,它根据外部的东西决定绘制什么:
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 http://www.haskellforall.com/2013/06/from-zero-to-cooperative-threads-in-33.html:
- 当您听到“指令序列”时,您应该想到:“monad”。
- 当你用“扩展”来限定它时,你应该想到:“monad 转换器”。
现在我们意识到我们的画布实现很可能是一个 monad 转换器。我们可以回到我们的界面,并更改它,使每个类都是一个 monad 的类,类似于 Transformer 的MonadIO
class 和 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 ()
重新审视示例
现在我们所有的示例绘图操作都是一些未知的操作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 ()]
我们还可以用油漆做一个例子。由于我们不知道会存在什么油漆,因此它们都必须从外部提供(作为示例的参数):
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)
实施
如果您可以让您的代码使用各种绘画来绘制点、框、线和文本而不引入抽象,我们可以更改它以实现第一部分中的接口。