【问题标题】:Better Way to Define an Enum in Haskell在 Haskell 中定义枚举的更好方法
【发布时间】:2018-12-30 14:18:05
【问题描述】:

我想要一个数据类型来表示一组有限的整数,这些整数可以通过特定的名称来寻址。我认为最好的方法是使用枚举。

但是,有一个小问题。我知道定义枚举的唯一方法是这样的:

data MyDataType = Foo | Bar | Baz

instance Enum MyDataType 
 toEnum 0 = Foo
 toEnum 1 = Bar
 toEnum 2 = Baz

 fromEnum Foo = 0
 fromEnum Bar = 1
 fromEnum Baz = 2 

请注意,我必须重复同一对两次 - 一次是定义整数到枚举的映射,另一次是定义枚举到整数的映射。

有没有办法避免这种重复?

【问题讨论】:

  • 你知道deriving Enum 吗?太神奇了!
  • 好的,但是如果分配给名称的值不是连续的,则它可能不起作用,例如Foo 应该是 2,Bar 应该是 4,Baz 应该是 8,等等。
  • 除了 Augustss 的建议之外,我使用的一种方法是派生 Enum 并在间隙中放置填充类型(当间隙很小时):data SomeEnum = ValueA | Reserved1 | Reserved2 | ValueB | Reserved3 | ValueC
  • 不过需要注意的是 - 根据问题中的代码,长手是有效且清晰的。有时抓取样板文件并不是最好的方法。
  • 如果您想要的值实际上是 2、4、8、16 等。在我看来,可能还有另一种方法。比如,使用deriving Enum,然后编写自己的toEnum'fromEnum' 版本,调用toEnumfromEnum,然后进行2^x 转换。这是否更好,我不知道。

标签: haskell enums


【解决方案1】:
data MyDataType = Foo | Bar | Baz deriving (Enum)

【讨论】:

  • 适用于顺序枚举值(如在我的示例中),但如果值不是顺序的(例如 Foo 应该是 2,Bar 应该是 4,等等?)
  • 不是直接的——但是如果你可以写一个双射函数f,它将连续的整数从0..n变成你想要的整数,你可以定义一个newtype MyRealDataType = MyRealDataType MyDataType并给它一个@ 987654324@ 实例,它“更正”由MyDataTypeEnum 实例产生的值。缺点是当你想直接在名称上进行模式匹配时,你必须打开它,等等。
  • 问题是,值的变化没有特定的规律(例如,它们不是 2 的幂)。
【解决方案2】:
instance Enum MyDataType where
    fromEnum = fromJust . flip lookup table
    toEnum = fromJust . flip lookup (map swap table)
table = [(Foo, 0), (Bar, 1), (Baz, 2)]

【讨论】:

  • ...swap 是显而易见的 swap (x,y) = (y,x)(在最新的 Haskell 平台中定义为 Data.Tuple
  • 我知道我不应该关心,但是这会被 ghc 优化成有效的东西吗?
  • @Florian,可能不是。可能会写出类似的东西。
  • GHC 至少会实现只生成一次交换表(map swap table)吗?
【解决方案3】:

已接受的解决方案的问题在于,当您的表中缺少枚举时,编译器不会告诉您。 deriving Enum 解决方案很棒,但如果您想对数字进行任意映射,它就行不通了。另一个答案建议使用泛型或模板 Haskell。这通过使用Data 来跟进。

{-# Language DeriveDataTypeable #-}
import Data.Data
data MyDataType = Foo | Bar | Baz deriving (Eq, Show, Data, Typeable)

toNumber enum = case enum of
   Foo -> 1
   Bar -> 2
   Baz -> 4

当添加新的构造函数时,我们将在 toNumber 大小写映射中收到编译器警告。

现在我们只需要能够将该代码转换为数据,以便可以自动反转映射。在这里,我们生成了与接受的解决方案中提到的相同的table

table = map (\cData -> let c = (fromConstr cData :: MyDataType) in (c, toNumber c) )
      $ dataTypeConstrs $ dataTypeOf Foo

您可以填写与接受的答案相同的Enum 课程。没有提到的是,你也可以填写Bounded类。

【讨论】:

    【解决方案4】:

    由于您说这些数字不是由任何常规法律生成的,因此您可以使用通用编程(例如使用 Scrap Your Boilerplate)或 Template Haskell 来实现此问题的通用解决方案。我更喜欢 Template Haskell,因为它实际上会生成代码并编译它,因此您可以获得 GHC 的所有类型检查和优化优势。

    如果有人已经实现了这一点,我不会感到惊讶。它应该是微不足道的。

    【讨论】:

    • 好的,我应该想到augustss的解决方案。这个唯一的好处是你不需要列出所有的构造函数名称两次。
    • 你的得到优化。并正确检查。
    【解决方案5】:

    我在这里的示例是使用带有提示符 "λ: " 的 GHCI 8.4.4。

    我认为从 Enum 派生在这里最有意义,因为 Haskell 中最基本的类型也派生自 Enum(元组、字符、整数等......),并且它具有 builtin methods值进出枚举。

    首先,创建一个派生Enum 的数据类型(和Show,以便您可以查看REPL 和Eq 中的值以启用.. 范围补全):

    λ: data MyDataType = Foo | Bar | Baz deriving (Enum, Show, Eq)
    λ: [Foo ..]
    [Foo,Bar,Baz]
    

    枚举定义了一个方法fromEnum,您可以使用它来获取问题中请求的值(012)。

    用法:

    λ: map fromEnum [Foo ..]
    [0,1,2]
    

    定义一个给出任意值的函数是一件简单的事情(例如使用整数幂运算符,^ 的二次幂):

    λ: value e = 2 ^ (fromEnum e)
    

    用法:

    λ: map value [Foo ..]
    [1,2,4]
    

    另一个答案说:

    deriving Enum 解决方案很棒,但如果您想对数字进行任意映射,它就行不通了。

    好吧,让我们看看(如果您还没有使用:set +m 在 GHCI 中启用多行输入):

    arbitrary e = case e of
      Foo -> 10
      Bar -> 200
      Baz -> 3000
    

    用法:

    λ: map arbitrary [Foo ..]
    [10,200,3000]
    

    我们刚刚证明它确实有效,但如果我们不希望值从 0 增加 1,我更愿意从 fromEnum 计算它,就像我们对 value 所做的那样。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-28
      • 1970-01-01
      • 2022-01-05
      • 1970-01-01
      • 2013-01-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多