我希望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。
答案 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)