防止无意中使用不同的类型类实例

时间:2018-04-12 20:17:56

标签: haskell typeclass

我希望ZipList'的应用实例中出现以下行为:

zipListApplyTest = fs <*> xs
  where fs = ZipList' [negate, id]
        xs = ZipList' [1..5]

-- Result: ZipList' [-1,2]

这是我的第一次尝试:

newtype ZipList' a = ZipList' [a]
                   deriving (Eq, Show)

instance Functor ZipList' where
  fmap f (ZipList' xs) = ZipList' $ fmap f xs

instance Applicative ZipList' where
  pure x = ZipList' [x]

  ZipList' (f:fs) <*> ZipList' (x:xs) =
     ZipList' $ f x : (fs <*> xs) -- <-- the bug is here
  ZipList' [] <*> _ = ZipList' []
  _ <*> ZipList' []  = ZipList' []

-- Unexpected result: ZipList' [-1,2,3,4,5]

经过一番头疼之后,我意识到在ZipList'的应用实例中,我意外地使用了错误的<*>

在标有the bug is here的行中,我应用了属于内置列表类型<*>的{​​{1}},而不应用[] <*>递归。

这就是为什么第二个函数ZipList'应用于列表的其余部分,而不是仅应用于第二个元素id

这产生了预期的结果:

2

是否有编译器标记,语言习惯用法或其他技术可以防止此错误或者更容易发现?

我正在进行GHC 8.2.2。

4 个答案:

答案 0 :(得分:12)

我们可以这样做:

{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
-- at very top of file ^
-- ...
-- pick whatever names/operators you want
-- synonym signatures are given in GADT-like syntax
-- ZCons decomposes a ZipList' a into an a and a ZipList' a
-- (assuming it succeeds). This is the syntax even for pattern synonyms that
-- can only be used as patterns
-- (e.g. pattern Fst :: a -> (a, b); pattern Fst a <- (a, _)).
pattern ZCons :: a -> ZipList' a -> ZipList' a
-- xs needs to be a ZipList', but it's only a [a], so we uglify this synonym
-- by using the newtype wrapper as a view
pattern ZCons x xs <- ZipList' (x:(ZipList' -> xs))
-- views aren't in general invertible, so we cannot make this an automatically
-- bidirectional synonym (like ZNil is). We can give an explicit version
  where ZCons x (ZipList' xs) = ZipList' $ x:xs
-- simple enough that we can use one definition for both pattern and expression
pattern ZNil :: ZipList' a
pattern ZNil = ZipList' []
{-# COMPLETE ZNil, ZCons #-}
-- ZNil and ZCons cover all ZipLists

instance Applicative ZipList' where
  pure x = ZipList' $ repeat x
  -- these are bidirectional
  (ZCons f fs) <*> (ZCons x xs) = ZCons (f x) (fs <*> xs)
  _ <*> _ = ZNil

答案 1 :(得分:2)

作为AJFarmar答案的变体,您可以保留ZipList'内部利用[a]列表的定义,而是声明模式同义词假装 type被声明为

data ZipList' a = ZipCons a (ZipList' a) | ZipNil

通过这种方式,如果你限制自己使用这些&#34;假装&#34;构造函数在编写实例时,不会无意中涉及列表。

{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
{-# OPTIONS -Wall #-}
module ZipList where

newtype ZipList' a = ZipList' { unZipList' :: [a] }
                   deriving (Eq, Show)

这是模式同义词。我们需要在这里小心一点,因为我们需要根据需要将列表转换为zip列表。

pattern ZipCons :: a -> ZipList' a -> ZipList' a
pattern ZipCons x xs <- ZipList' (x : (ZipList' -> xs))
  where ZipCons x xs = ZipList' (x : unZipList' xs)

pattern ZipNil :: ZipList' a
pattern ZipNil = ZipList' []

我们可以保留functor实例,利用Functor []实例。在这里,我们确实想要调用列表fmap。否则,我们可以使用&#34;假装&#34;构造函数,但我们必须重新实现它。

instance Functor ZipList' where
  fmap f (ZipList' xs) = ZipList' $ fmap f xs

最后,applicative实例只能使用伪装​​构造函数。

instance Applicative ZipList' where
  pure x = ZipCons x ZipNil

  ZipCons f fs <*> ZipCons x xs = ZipCons (f x) (fs <*> xs)
  _            <*> _            = ZipNil

对我来说,使用模式同义词的一个主要缺点是穷举检查器容易混淆,触发虚假警告。上面,如果我们将_ <*> _案例替换为涉及ZipNil的两个明显案例,我们会触发警告。

(更新:HTNV使用COMPLETE pragma来静音警告,看起来非常好!我不知道这一点。)

除此之外,模式同义词允许提供一个非常优雅的界面。我希望它们在Haskell生态系统中得到更频繁的使用。

答案 2 :(得分:1)

不,你不能阻止这一般。您必须仔细编写实例。

话虽如此,解决此问题的一种快速方法是重新定义您的数据类型;而不是将其放在列表中,而是创建一个新的列表类型:

data Ziplist a = Nil | Cons a (Ziplist a)
-- (Instances etc follow)

这避免了这种错误的可能性。但是,这不一定是最好的主意,因为这需要函数重写等等。

你可以写测试。哪个最好。所以写测试。 {I}我知道,HSpec是最常用的测试框架,因此这是一个很好的起点。

答案 3 :(得分:1)

这不是一般答案,但在这个特殊情况下,我认为最好和最简单的方法就是使用zipWith,这就是base库的作用。

instance Applicative ZipList where
  pure x = ZipList (repeat x)
  ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)