【问题标题】:how to filter sum types in Haskell如何在 Haskell 中过滤总和类型
【发布时间】:2022-10-12 18:11:19
【问题描述】:

例如

data CampingStuff = Apple String Int Bool 
                  | Banana String Int 
                  | Pineapple String String
                  | Table String Bool Int
                  | Chairs String Int Int Int 

我想要一个查询功能

pickStuff :: [CampingStuff] ->  ??? -> [CampingStuff]

??? 我想通过 Apple 然后 pickStuff 将过滤掉所有的东西,比如

Apple "Jane" ...
Apple "Jack" ...
Apple "Lucy" ...

我能想到的事情就像

pickStuffs stuffs dummyStuff
  = filter 
      (\x ->
        (x == dummyStuff)
      stuffs

pickStuffs stuffs (Apple "" 0 False)

instance Eq CampingStuff where
  compare (Apple name1 number1 b1) (Apple name2 number2 b2)
     = True

它的缺点是:

  • 将额外参数传递给虚拟值并不优雅,也没有任何意义"" 0 False
  • 它必须实现 Eq 类型类(Apple、Table、Chair)中的所有值构造函数
  • 它不可扩展,因为将来我想过滤掉 Janes 的所有苹果
    • 喜欢这个(Apple "Jane" _ _)

感谢您阅读本文并感谢您对如何通过Data Constructor 过滤此[CampingStuff] (如Apple/Table)提供任何帮助?

【问题讨论】:

  • String Int 在每个替代方案中的重复对我来说看起来并不健康。

标签: haskell


【解决方案1】:

问题是不饱和的构造函数不能真正按值进行比较。您可以对它们做的唯一事情是调用它们或对它们进行模式匹配。因此,如果你想要一个测试 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

您需要为 asAppleasBanana 等提供单独的函数。这看起来很麻烦,我并不完全不同意,但实际上人们并不真正需要大量这样的函数。按照我之前描述的那样做通常会更好:延迟模式匹配,直到你知道如何处理结果。

【讨论】:

  • 如果Apple 有额外的字段,如 "Apple String Int Bool" ,Bool 隐含 organic or not 怎么办?和其他Table 有额外的字段,如大小? "Table String Int Int" ..那么CampingStuff 可能不是最好的覆盖所有情况?
【解决方案2】:

对于您想要的功能,您可以创建诸如

isApple :: CampingStuff -> Bool
isApple Apple{} = True
isApple _ = False

然后使用filter isApple。当您想按 Jane 进行过滤时,您可以为每种类型添加另外 5 个函数,例如 isAppleFrom :: String -> CampingStuff -> Boolfilter (isAppleFrom "Jane")

另一种方法如下:

data StuffType = AppleT | BananaT | PineappleT | TableT | ChairsT deriving Eq
data Query = ByStuff StuffType | ByName String deriving Eq

pickStuff :: [CampingStuff] -> [Query] -> [CampingStuff]
pickStuff xs qs = filter cond xs
  where
    cond :: CampingStuff -> Bool
    cond x = all (q -> case (q, x) of
      (ByStuff AppleT, Apple{}) -> True
      ...other pairs...
      (ByName name1, Apple name2 _) -> name1 == name2
      ...
      _ -> False) qs

也就是说,将查询与数据类型分开。上面是一个例子,可能写得更好。

【讨论】:

    猜你喜欢
    • 2019-09-04
    • 2014-12-17
    • 2011-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-24
    相关资源
    最近更新 更多