将枚举列表按位表示为整数

5
为了将用户账户权限保存在外部(例如在数据库中),我想要用一个枚举的元素列表来表示具有派生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

我该如何解决这个问题?

2
首先,你看过 the Data.Bits module 了吗? - C. A. McCann
@C. A. McCann 没有,你认为它会有用吗? - Jakub
我认为它没有完全符合你想要的功能(虽然它似乎应该有),但它有一堆比特操作,这将使事情变得更加容易。 - C. A. McCann
3个回答

10

以下是完整的解决方案。它应该表现更好,因为它的实现基于位运算而不是算术运算,这是一种更有效的方法。该解决方案还尽力将事物泛化。

{-# 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

非常感谢!是否可以向类添加默认定义,以便我可以简单地编写 instance ToBitMask Permission???当然,假设 Permission 权限已经是 EnumBounded 的实例。 - Jakub
@Jakub 进行了更新,更加通用化,如果您感兴趣的话。 - Nikita Volkov
太好了!现在我可以真正地与任何类型一起使用它,甚至不需要花费一秒钟时间思考如何实现这些函数。 - Jakub
@NikitaVolkov 这可能是一个独立的库!太完美了! - Elliot Cameron

4

我确定在Hackage上已经有做这件事的东西了,但使用 Data.Bits 模块手动编写自己的代码也很简单。

你可以将 enumsToInt 简化为类似于 foldl' (.|.) . map (bit . fromEnum) 的形式,即先转换为整数索引,再转换为单一位,最后通过按位或来折叠。至少这样可以避免担心重复值的问题。

对于 intToEnums ,没有非常方便的方法,但是为了快速解决,你可以像这样做:filter (testBit foo . fromEnum) [minBound .. maxBound]。 当然,这仅适用于Bounded类型,并假设枚举值不超过外部类型具有的位数,并且fromEnum从0开始使用连续的整数,但看起来你已经将这些作为前提条件了。


2

EnumSet 可能正是你想要的。它甚至有一个 intToEnums 函数(尽管它似乎只与我尝试过的类型中的 T Integer a 一起有效——特别是,T Int Char 给出了意外结果),并且不会在序列化/反序列化后重新生成重复条目(因为它是集合),而列表可能会有这种期望。


是的,你需要一个更大的 Int。除非你的机器使用128位字,否则你甚至无法将基本ASCII字符放入该 EnumSet 中。如果想要使用 Int,请尝试 Ordering 或类似 (Bool, Bool) 的东西。 - C. A. McCann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接