问题是不饱和的构造函数不能真正按值进行比较。您可以对它们做的唯一事情是调用它们或对它们进行模式匹配。因此,如果你想要一个测试 Apple 的函数,它必须与测试 Banana 的函数完全不同——它们不能共享任何代码,因为它们必须与一组不同的模式进行比较。
如果您重构类型以消除明显的重复,这一切都会容易得多,从而为您留下饱和值的构造函数。生成的 Eq 实例是比较类型所需的全部内容:
data StuffType = Apple | Banana | Pineapple | Table | Chairs deriving Eq
data CampingStuff = Stuff { stuffType :: StuffType
, owner :: String
, quantity :: Int
}
然后,您可以通过组合几个函数轻松编写CampingStuff -> Bool 类型的函数。
hasType :: StuffType -> CampingStuff -> Bool
hasType t s = stuffType s == t
并使用它来过滤列表:
pickStuff :: StuffType -> [CampingStuff] -> [CampingStuff]
pickStuff = filter . hasType
在 cmets 中,您会问:如果我的构造函数不是全部统一的,所以我无法将所有内容提取到包含枚举的产品类型中怎么办?
我认为,在这种情况下,您不会对 pickStuff 函数的结果感到满意,无论它是如何实现的。让我们想象一个更简单的类型:
data Color = Red | Green
data Light = Off | On Color
现在,您可能希望过滤[Light],使其仅包含打开的灯,而不管它们的颜色如何。好的,我们可以实现它。我们甚至不用担心泛化,因为类型太小了:
ons :: [Light] -> [Light]
ons = filter on
where on Off = False
on (On _) = True
现在你有lights :: [Light],你可以得到onLights = ons lights :: [Light]。惊人。接下来你会用onLights 做什么?也许您想计算每种颜色的数量:
import qualified Data.Map as M
colorCounts :: [Light] -> M.Map Color Int
colorCounts = M.fromListWith (+) . map getColor
where getColor (On c) = (c, 1)
colorCounts 有一个问题:它假定所有的灯都亮着,但在类型系统中并不能保证这一点。所以你可能会不小心调用colorCounts ls 而不是colorCounts (ons ls),它会编译,但在运行时会给你一个错误。
当您知道如何处理结果时,最好只进行模式匹配。在这里,这是colorCounts 的内部:只需为Off 添加一个案例,并使用mapMaybe 而不是map,这样您就有机会丢弃您不喜欢的值:
colorCounts' :: [Light] -> M.Map Color Int
colorCounts' = M.fromListWith (+) . mapMabye getColor
where getColor (On c) = Just (c, 1)
getColor Off = Nothing
相同的参数都适用于更复杂的类型:在您准备好处理之前不要对值进行模式匹配全部您可能会找到的信息。
当然,处理此类信息的一种方法是将其放入仅包含您想要的信息的新类型中。所以你可以很好地写一个函数
colorsOfOnLights :: [Light] -> [Color]
colorsOfOnLights = mapMaybe go
where go Off = Nothing
go (On c) = Just c
这样,您不可能将“过滤器”函数的输入与输出混淆:输出显然与原始Light 类型分离,其值只能来自On 灯。您可以通过为每个构造函数提取一个新的产品类型来为您的 CampingStuff 类型做同样的事情:
data CampingStuff = Apple AppleData
| Banana BananaData
-- ...
data AppleData = AppleData String Int Bool
data BananaData = BananaData String Int
-- ...
asApple :: CampingStuff -> Maybe AppleData
asApple (Apple ad) = Just ad
asApple _ = Nothing
apples :: [CampingStuff] -> [AppleData]
apples = mapMaybe asApple
您需要为 asApple 和 asBanana 等提供单独的函数。这看起来很麻烦,我并不完全不同意,但实际上人们并不真正需要大量这样的函数。按照我之前描述的那样做通常会更好:延迟模式匹配,直到你知道如何处理结果。