【问题标题】:Test if a value matches a constructor测试一个值是否与构造函数匹配
【发布时间】:2014-08-30 23:09:27
【问题描述】:

假设我有这样的数据类型:

data NumCol = Empty |
              Single Int |
              Pair Int Int |
              Lots [Int]

现在我希望从[NumCol] 中过滤掉与给定构造函数匹配的元素。我可以为 Pair 写它:

get_pairs :: [NumCol] -> [NumCol]
get_pairs = filter is_pair
    where is_pair (Pair _ _) = True
          is_pair _ = False

这可行,但它不是通用的。我必须为is_singleis_lots等写一个单独的函数。

我希望我可以写:

get_pairs = filter (== Pair)

但这仅适用于不带参数的类型构造函数(即Empty)。

那么问题来了,如何写一个函数,接受一个值和一个构造函数,并返回值是否与构造函数匹配?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    至少get_pairs 本身可以通过使用列表推导进行过滤来相对简单地定义:

    get_pairs xs = [x | x@Pair {} <- xs]
    

    对于匹配构造函数的更通用解决方案,您可以使用lens 包中的棱镜:

    {-# LANGUAGE TemplateHaskell #-}
    
    import Control.Lens
    import Control.Lens.Extras (is)
    
    data NumCol = Empty |
                  Single Int |
                  Pair Int Int |
                  Lots [Int]
    
    -- Uses Template Haskell to create the Prisms _Empty, _Single, _Pair and _Lots
    -- corresponding to your constructors
    makePrisms ''NumCol
    
    get_pairs :: [NumCol] -> [NumCol]
    get_pairs = filter (is _Pair)
    

    【讨论】:

    • 啊,list-filter-via-pattern-matching。这是保留fail 的少数优点之一。
    • @DanBurton 好吧,从技术上讲,它不使用fail,因为列表推导式有一个单独描述的与do 符号的脱糖。而且我敢肯定,他们宁愿将do 更改为使用mzero,也不愿直接删除它。
    • 第一个函数get_pairs到底发生了什么{}的语法是什么意思?我只在“记录语法”中看到过大括号
    • @lsund is 记录语法,只是没有列出任何要匹配的字段,这意味着它只检查给定的构造函数。作为一种特殊情况,即使数据类型没有使用记录语法定义,也可以使用它。 x@ 部分也将整个“记录”绑定到 x
    • 聪明,我不知道你可以这样匹配数据构造函数!
    【解决方案2】:

    标记工会的标记应该是一流的值,并且稍加努力,它们就是。

    跳棋警报:

    {-# LANGUAGE GADTs, DataKinds, KindSignatures,
        TypeFamilies, PolyKinds, FlexibleInstances,
        PatternSynonyms
    #-}
    

    第一步:定义标签的类型级版本。

    data TagType = EmptyTag | SingleTag | PairTag | LotsTag
    

    第二步:为类型级标签的可表示性定义值级见证。 Richard Eisenberg 的 Singletons 库将为您执行此操作。我的意思是这样的:

    data Tag :: TagType -> * where
      EmptyT   :: Tag EmptyTag
      SingleT  :: Tag SingleTag
      PairT    :: Tag PairTag
      LotsT    :: Tag LotsTag
    

    现在我们可以说出我们期望找到与给定标签相关联的内容。

    type family Stuff (t :: TagType) :: * where
      Stuff EmptyTag   = ()
      Stuff SingleTag  = Int
      Stuff PairTag    = (Int, Int)
      Stuff LotsTag    = [Int]
    

    所以我们可以重构你首先想到的类型

    data NumCol :: * where
      (:&) :: Tag t -> Stuff t -> NumCol
    

    并使用PatternSynonyms 来恢复您想到的行为:

    pattern Empty        = EmptyT   :&  ()
    pattern Single  i    = SingleT  :&  i
    pattern Pair    i j  = PairT    :&  (i, j)
    pattern Lots    is   = LotsT    :&  is
    

    所以发生的事情是NumCol 的每个构造函数都变成了一个标签,该标签由它所针对的标签类型索引。也就是说,构造函数标签现在与其他数据分开存在,由一个公共索引同步,确保与标签关联的内容与标签本身匹配。

    但我们可以单独讨论标签。

    data Ex :: (k -> *) -> * where  -- wish I could say newtype here
      Witness :: p x -> Ex p
    

    现在,Ex Tag 是“具有类型级别对应物的运行时标签”的类型。它有一个Eq 实例

    instance Eq (Ex Tag) where
      Witness EmptyT   ==  Witness EmptyT   = True
      Witness SingleT  ==  Witness SingleT  = True
      Witness PairT    ==  Witness PairT    = True
      Witness LotsT    ==  Witness LotsT    = True
      _                ==  _                = False
    

    此外,我们可以轻松提取NumCol的标签。

    numColTag :: NumCol -> Ex Tag
    numColTag (n :& _) = Witness n
    

    这使我们能够匹配您的规格。

    filter ((Witness PairT ==) . numColTag) :: [NumCol] -> [NumCol]
    

    这引发了一个问题,即您的规范是否真的是您所需要的。关键是检测到一个标签会让你对这个标签的东西有一个期望。输出类型 [NumCol] 并不能说明您知道自己只有对这一事实。

    您如何收紧您的功能类型并仍然交付它?

    【讨论】:

    • 你的大脑可以下载吗?我非常希望自己能够想到这种事情。
    • 我的大脑非常不可用,有时甚至对我来说也是如此。但我在这里所做的一切(也许我应该这么说)是标准问题“Sigma 类型”的 Haskell-fake。也就是说,一种依赖对的类型,其中第二个组件的类型取决于第一个组件的值。标记联合的 Sigma 类型编码作为标记枚举和任何其他标记说你需要的对是非常标准的(但仍然,令人讨厌的是,不是数据类型在 Agda/Idris 中的工作方式)。移植到 Haskell 需要单例结构,这也是标准的,也很烦人。
    【解决方案3】:

    一种方法是使用DataTypeableData.Data 模块。这种方法依赖于两个自动生成的类型类实例,它们为您携带有关类型的元数据:TypeableData。您可以使用{-# LANGUAGE DeriveDataTypeable #-} 导出它们:

    data NumCol = Empty |
              Single Int |
              Pair Int Int |
              Lots [Int] deriving (Typeable, Data)
    

    现在我们有了一个toConstr 函数,给定一个值,它为我们提供了它的构造函数的表示:

    toConstr :: Data a => a -> Constr
    

    这使得compare two terms 仅由其构造函数变得容易。剩下的唯一问题是我们需要一个值来比较我们定义谓词时的值!我们总是可以用undefined 创建一个虚拟值,但这有点难看:

    is_pair x = toConstr x == toConstr (Pair undefined undefined)
    

    所以我们要做的最后一件事是定义一个方便的小类来自动执行此操作。基本思想是在非函数值上调用toConstr,并通过首先传入undefined 对任何函数进行递归。

    class Constrable a where
      constr :: a -> Constr
    
    instance Data a => Constrable a where
      constr = toConstr
    
    instance Constrable a => Constrable (b -> a) where
      constr f = constr (f undefined)
    

    这依赖于FlexibleInstanceOverlappingInstancesUndecidableInstances,所以它可能有点邪恶,但是,使用(in)著名的眼球定理,它应该没问题。除非您添加更多实例或尝试将其与非构造函数一起使用。那么它可能会爆炸。猛烈地。没有承诺。

    最后,巧妙地包含邪恶,我们可以编写一个“构造函数等于”运算符:

    (=|=) :: (Data a, Constrable b) => a -> b -> Bool
    e =|= c = toConstr e == constr c
    

    =|= 运算符有点助记符,因为构造函数在语法上是用| 定义的。)

    现在你几乎可以写出你想要的东西了!

    filter (=|= Pair)
    

    另外,也许您想关闭单态限制。事实上,这是我启用的扩展列表,您可以直接使用:

    {-# LANGUAGE DeriveDataTypeable, FlexibleInstances, NoMonomorphismRestriction, OverlappingInstances, UndecidableInstances #-}
    

    是的,很多。但这就是我愿意为这项事业做出的牺牲。不用写额外的undefineds。

    老实说,如果您不介意依赖 lens(但男孩是那种依赖很糟糕),您应该采用 prism 方法。唯一推荐我的是你可以使用有趣的命名Data.Data.Data 类。

    【讨论】:

    • 两件事:(1)您可以使用我在答案中使用的相同 Pair {} 语法来构造一个所有参数未定义的值。 (2) 如果构造函数有任何严格的字段,任何一种方法放入undefineds 都不起作用。
    • 也许这只是我最近一直在写的那种things,但是不,这对我来说看起来并不像很多扩展。现在,即使是 UndecidableInstances 也失去了它的可怕性。 :)
    • @Jules:我对那个评论有点开玩笑:)。
    • 您可能希望将(=|=) 的类型限制为forall a. a -&gt; a -&gt; Bool。不同类型的两个构造函数应该从不相等,但正如 Data.Data 文档所说,“请注意,具有不同类型的构造函数的相等性可能不起作用——即 False 和 Nothing 的构造函数可能比较相等。”见hackage.haskell.org/package/base-4.8.0.0/docs/…
    • 您可能还想查看 GHC 指南,以使用更精确版本的 OverlappingInstances 和 Safe Haskell:ghc.haskell.org/trac/ghc/wiki/SafeHaskell/…
    猜你喜欢
    • 2012-06-02
    • 2012-10-05
    • 2016-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-18
    • 2018-05-04
    • 2019-07-22
    相关资源
    最近更新 更多