将正确性约束直接编码到Haskell类型系统中?

时间:2013-04-25 05:29:21

标签: haskell

在阅读由matt写的article时,我看到了下面的信息。

  

经验丰富的程序员擅长将正确性约束直接编码到Haskell类型系统中

有人可以解释这句话的含义或提供一个简短的例子吗?

2 个答案:

答案 0 :(得分:18)

这可以涵盖真正各种不同的技术。最简单的基本上是不可避免的:如果你想要一个可以为null的值,这可能取决于可变状态或用户输入,你必须用类型系统标记它。这是MaybeSTIO分别执行的操作。因此,如果您有上述三种类型之一的内容,您知道它必须是一个不能为空的引用透明值。

上述技巧对语言非常重要,基本上是不可避免的。但是,还有其他方法可以使用类型系统来提高安全性和正确性,这些方法更有趣。

一个有用的例子是阻止SQL注入。 SQL注入是Web应用程序中的常见问题 - 对于基本概念,请查看this XKCD cartoon。我们实际上可以使用类型系统来确保传递给数据库的任何字符串都已经过清理。基本思想是为“原始”字符串创建一个新类型:

newtype Raw a = Raw a

然后,确保从用户获取输入的所有函数都返回Raw值而不是普通字符串。最后,您只需要一个清理功能:

sanitize :: Raw String -> String

由于普通函数接受String而不是Raw,因此您将无法意外地传入未经过类型化的字符串。因为我们使用Raw定义了newtype,所以它根本没有运行时开销。

Yesod,一个主要的Haskell Web框架,使用类似这样的技术来防止SQL注入。它还有一些其他很酷的方法,比如使用类型系统来防止数据库中的链接断开;你应该看看它。

在最极端的情况下,您甚至可以使用类型系统来确保矩阵的大小合适。这是一个非常简单的方法。首先,我们需要类型级别的数字:

data Z
data S n

(我们在Peano Arithmetic处使用type level。)这个想法很简单:Z为0,S是后继函数,所以{{1 }是1,S Z是2,依此类推。

我们现在可以编写一个安全矩阵乘法函数:

S (S Z)

此函数仅允许您在内部维度匹配时将矩阵相乘,并确保生成的矩阵在其类型中具有正确的维度。

Type-safe matrix multiplication有更多详情。

答案 1 :(得分:7)

嗯,在我看来,像你所看到的那些博览会经常无法传达的事情之一是,这种正确的类型通常是不会“自然地”发生的东西,而是来自使用技术来设计您的类型,这些技术对于来自其他语言的程序员来说并不明显。我最喜欢的一个例子来自the Haskell Wiki page on phantom types;如果您查看该页面上的第1部分,他们就会有这个示例(IMO应该是newtype声明,而不是data):

data FormData a = FormData String

a在做什么?好吧,它的作用是人为地使FormData "foo" :: FormData ValidatedFormData "foo" :: FormData Unvalidated,尽管它们“真的”相同,现在有不兼容的类型,因此你可以强制你的代码不要混合一个和另一个。好吧,让我不再重复页面所说的内容,它相对容易阅读(至少第1节)。

我在其中一个开关项目中使用的一个更复杂的例子:OLAP超立方体可以看作是一种不是由整数索引索引而是由数据模型对象索引的数组喜欢人,日,产品线等:

-- | The type of Hypercubes.
data Hypercube point value = ...

-- | Access a data point in a hypercube.
get :: Eq point => Hypercube point value -> point -> value

-- | This is totally pseudocode...
data Salesperson = Mary | Joe | Irma deriving Eq
data Month = January | February | ... | December deriving Eq
data ProductLine = Widget | Gagdet | Thingamabob

-- Pseudo-example: compute sales numbers grouped by Salesperson, Month and
-- ProductLine for the combinations specified as the "frame"
salesResult :: HyperCube (Salesperson, Month, ProductLine) Dollars 
salesResult = execute salesQuery frame
    where frame = [Joe, Mary] `by` [March, April] `by` [Widgets, Gadgets]
          salesQuery = ...

-- Read from salesResult how much Mary sold in Widgets on April.
example :: Dollars
example = get salesResult (Mary, April, Widgets)

我希望这比我担心的更有意义。无论如何,这个例子的问题是以下问题:get的类型,如此处所示,允许您让Hypercube告诉您它没有的点的值: / p>

badExample :: Dollar
badExample = get salesResult (Irma, January, Thingamabob)

一种可能的解决方案是使get操作返回Maybe value而不仅仅value。但我们实际上可以做得更好;我们可以设计一个API,其中Hypercube只能询问它包含的值。密钥类似于FormData示例,但是更复杂的变体。首先我们介绍这种幻像类型:

data Cell tag point = Cell { getPoint :: point } deriving Eq

现在我们重新制定Hypercubeget以对标签敏感。我实际上要在这个重新制定的例子中使它更具体。我们从这开始:

{-# LANGUAGE ExistentialTypes #-}

data AuxCube tag point value = 
    AuxCube { getFrame :: [Cell tag point]
            , get :: Cell tag point -> value }

-- This is using a type system extension called ExistentialTypes:
data Hypercube point value = forall tag. Hypercube (AuxCube tag point value)

-- How to use one of these cubes.  Suppose we have:
salesResult :: Hypercube (Salesperson, Month, ProductLine) Dollars 
salesResult = execute salesQuery points
    where points = [Joe, Mary] `by` [March, April] `by` [Widgets, Gadgets]
          salesQuery = ...

-- Now to read values, we have to do something like this:
example = case salesResult of
              Hypercube (AuxCube frame getter) -> getter (head frame)

如果使用ExistentialTypes让您感到困惑,我很抱歉,但为了简短说明,本例中的内容基本上是每个Hypercube包含一个AuxCube唯一的匿名标记类型参数,因此现在没有两个Hypercube可以具有相同类型的Cell。因此,如果我们使用模块系统来阻止调用者构建Cell,则调用者不可能向Hypercube询问Cell它没有值。

致谢:I learned this technique by asking here in Stack Overflow