Haskell 将枚举按位表示为Int

Haskell 将枚举按位表示为Int,haskell,enums,bit-manipulation,Haskell,Enums,Bit Manipulation,为了在外部保存用户帐户权限(例如在DB中),我希望将具有派生Enum实例的枚举元素列表表示为Int 数字的每一位都被视为一个标志(或布尔值),表示列表中是否存在第i个元素。 换言之,2的每一次幂代表一个元素,这些幂的总和代表一个唯一的元素列表 例如: data Permissions = IsAllowedToLogin -- 1 | IsModerator -- 2 | IsAdmin

为了在外部保存用户帐户权限(例如在DB中),我希望将具有派生
Enum
实例的枚举元素列表表示为
Int

数字的每一位都被视为一个标志(或布尔值),表示列表中是否存在第i个元素。
换言之,2的每一次幂代表一个元素,这些幂的总和代表一个唯一的元素列表

例如:

data Permissions = IsAllowedToLogin   -- 1
                 | IsModerator        -- 2
                 | IsAdmin            -- 4
                 deriving (Bounded, Enum, Eq, Show) 

enumsToInt [IsAllowedToLogin, IsAdmin] == 1 + 4 == 5

intToEnums 3 == intToEnums (1 + 2) == [IsAllowedToLogin, IsModerator]

将此类列表转换为
Int
的函数非常容易编写:

enumsToInt :: (Enum a, Eq a) => [a] -> Int
enumsToInt = foldr (\p acc -> acc + 2 ^ fromEnum p) 0 . nub
请注意,公认的答案包含了更有效的实现

真正困扰我的是倒车功能。我可以想象它应该有这种类型:

intToEnums :: (Bounded a, Enum a) => Int -> [a]
intToEnums = undefined               -- What I'm asking about

我应该如何处理这个问题?

可能正是您想要的。它甚至有一个
intToEnums
函数(尽管它似乎只与我尝试过的
T Integer a
类型一致工作-特别是,
T Int Char
给出了意外的结果),并且在序列化/反序列化后不会重新创建重复的条目(假设它是一个集合),虽然列表可能会带来这种期望。

我相信hackage上已经有这样的东西了,但它足够简单,可以让您自己使用

您可以将
enumsToInt
简化为类似于
foldl'(.|)。映射(bit.fromnum)
,即转换为整数索引,然后转换为单个位,然后按位或折叠。如果没有其他问题,这将使您不用担心删除重复项


对于
intToEnums
没有什么特别方便的,但是对于快速解决方案,您可以执行类似于
过滤器(testBit foo.fromnum)[minBound..maxBound]
的操作。当然,这只适用于
有界的
类型,并假定枚举的值并不比外部类型的位多,并且
fromEnum
使用从0开始的连续整数,但听起来好像您在这里以所有这些为前提。

以下是一个完整的解决方案。它应该表现得更好,因为它的实现是基于位运算而不是算术运算的,这是一种更有效的方法。这个解决方案也尽可能地概括事物

{-# LANGUAGE DefaultSignatures #-}
import Data.Bits
import Control.Monad

data Permission = IsAllowedToLogin   -- 1
                | IsModerator        -- 2
                | IsAdmin            -- 4
                deriving (Bounded, Enum, Eq, Show) 

class ToBitMask a where 
  toBitMask :: a -> Int
  -- | Using a DefaultSignatures extension to declare a default signature with
  -- an `Enum` constraint without affecting the constraints of the class itself.
  default toBitMask :: Enum a => a -> Int
  toBitMask = shiftL 1 . fromEnum

instance ToBitMask Permission

instance ( ToBitMask a ) => ToBitMask [a] where 
  toBitMask = foldr (.|.) 0 . map toBitMask

-- | Not making this a typeclass, since it already generalizes over all 
-- imaginable instances with help of `MonadPlus`.
fromBitMask :: 
  ( MonadPlus m, Enum a, Bounded a, ToBitMask a ) => 
    Int -> m a
fromBitMask bm = msum $ map asInBM $ enumFrom minBound where 
  asInBM a = if isInBitMask bm a then return a else mzero

isInBitMask :: ( ToBitMask a ) => Int -> a -> Bool
isInBitMask bm a = let aBM = toBitMask a in aBM == aBM .&. bm
使用以下命令运行它

main = do
  print (fromBitMask 0 :: [Permission])
  print (fromBitMask 1 :: [Permission])
  print (fromBitMask 2 :: [Permission])
  print (fromBitMask 3 :: [Permission])
  print (fromBitMask 4 :: [Permission])
  print (fromBitMask 5 :: [Permission])
  print (fromBitMask 6 :: [Permission])
  print (fromBitMask 7 :: [Permission])

  print (fromBitMask 0 :: Maybe Permission)
  print (fromBitMask 1 :: Maybe Permission)
  print (fromBitMask 2 :: Maybe Permission)
  print (fromBitMask 4 :: Maybe Permission)
输出

[]
[IsAllowedToLogin]
[IsModerator]
[IsAllowedToLogin,IsModerator]
[IsAdmin]
[IsAllowedToLogin,IsAdmin]
[IsModerator,IsAdmin]
[IsAllowedToLogin,IsModerator,IsAdmin]
Nothing
Just IsAllowedToLogin
Just IsModerator
Just IsAdmin

首先,你看过吗?@C.A.麦肯不,我没看过!你认为它有用吗?我不认为它有任何东西能完全满足你的需要(尽管它似乎应该存在),但它有一系列按位操作,这将使事情变得更容易。是的,你需要一个更大的
Int
。除非您的计算机使用128位字,否则您甚至无法将基本ASCII字符放入
EnumSet
。如果您想使用
Int
s,请尝试订购
或类似的
(Bool,Bool)
。非常感谢!是否可以将默认定义添加到类中,这样我就可以简单地将
实例写入bitmask Permission
???当然,假设
Permission
permissions已经是
Enum
Bounded
@Jakub的实例,如果您感兴趣的话,我们会对其进行更新,使其更加通用化。太好了!现在我真的可以将它用于任何类型,甚至不用花一秒钟的时间思考如何实现这些函数。@NikitaVolkov这可能是一个自己的库!太完美了!