基于枚举类型概括数据字段的正确方法

Proper way to generalize data fields based on enumerated type

使用一个固定的结构我们可以写

data Stats = Stats { lines :: !Int, words :: !Int }

instance Num Stats where
    fromInteger x = Stats x x
    (Stats a b) + (Stats a' b') = Stats (a + a') (b + b')

我们可以创建一些动态结构来实现通用版本

newtype Stats a = Stats { unStats :: [Int] } -- or Map, Vector, ...

instance forall a . (Enum a, Bounded a) => Num (Stats a) where
    fromInteger = Stats . replicate sz
                  where sz = fromEnum (maxBound::a) - fromEnum (minBound::a) + 1
    (Stats a) + (Stats b) = Stats $ zipWith (+) a b

(¨) :: forall a . (Eq a, Enum a, Bounded a) => Int -> a -> Stats a
x ¨ u = Stats $ map (\k -> if k == u then x else 0) [minBound .. maxBound :: a]

并用作

data TextStats = Lines | Words deriving (Eq, Ord, Enum, Bounded)

someTextStats :: Stats TextStats
someTextStats = 1 ¨Lines + 5 ¨Words

前一种方式是静态的(例如,单位测量函数是静态的)但后者不是在运行时遍历定义的结构的意义上。

与模板 Haskell 存在某种差异?谢谢!

如果您使用 RankNTypesScopedVariables 并且您不尝试使用双引号作为运算符名称,则您的方法有效:

{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}

newtype Stats a = Stats { unStats :: [Integer] } -- or Map, Vector, ...
  deriving (Show)

instance forall a . (Enum a, Bounded a) => Num (Stats a) where
    fromInteger = Stats . replicate sz
                  where sz = fromEnum (maxBound::a) - fromEnum (minBound::a) + 1
    (Stats a) + (Stats b) = Stats $ zipWith (+) a b

(€) :: forall a . (Eq a, Enum a, Bounded a) => Integer -> a -> Stats a
x € u = Stats $ map (\k -> if k == u then x else 0) [minBound .. maxBound :: a]

data TextStats = Lines | Words deriving (Eq, Ord, Enum, Bounded)

test = 3 € Lines + 5 € Words

我还更改了列表以包含整数而不是整数。这是您要找的吗?

正如我五年前所问的那样,我一直在寻找一种方法让聚合复合函数在编译时完全形成,而不是在 运行 时迭代。

一种方法可以是:

(一些序言)

{-# LANGUAGE MultiParamTypeClasses
           , FlexibleInstances
           , FunctionalDependencies
           , ExistentialQuantification #-}

import Text.Printf

累加器是为每个交付的新值更新的值

class Accumulator a v | a -> v where
  accumulate :: a -> v -> a

因为我们要组成多个累加器,所以定义一个终止符似乎很有用

data Nop v = Nop

instance Accumulator (Nop v) v where
  accumulate _ _ = Nop

任何累加器都将包含一个投影和一个聚合函数。那么为了方便我们可以定义

data Prj a v n = Accumulator a v =>
     Prj { prjValue :: n
         , prjAcc   :: n -> n -> n
         , prjPrj   :: v -> n
         , prjNext  :: a
         }

instance Accumulator a v => Accumulator (Prj a v n) v where
  accumulate (Prj n acc prj a) v = Prj (acc n (prj v)) acc prj $ accumulate a v

现在,作为示例,我们可以定义以下通用累加器

countAcc :: Accumulator a v => a -> Prj a v Int
countAcc = Prj 0 (+) (const 1)

minAcc, maxAcc :: (Bounded n, Ord n, Accumulator a v) => (v -> n) -> a -> Prj a v n
minAcc  = Prj maxBound min
maxAcc  = Prj minBound max

sumAcc, sum2Acc :: (Num n, Accumulator a v) => (v -> n) -> a -> Prj a v n
sumAcc  = Prj 0 (+)
sum2Acc = Prj 0 (\u u' -> u + u' * u')

例如,假设我们有一个仓库

data Item = Item { itemName   :: String
                 , itemStock  :: Int
                 , itemWeight :: Double
                 }

有以下数据

warehouse = [ Item "apple"  12 0.1
            , Item "orange" 21 0.12
            , Item "melon"   9 2.23
            ]

我们可以定义以下统计信息(这将是一个编译函数,即内部操作可以优化,因为它不会迭代

myStats = countAcc
        $ minAcc itemStock  $ maxAcc  itemStock
        $ sumAcc itemStock  $ sum2Acc itemStock
        $ sumAcc itemWeight $ Nop

然后提取我们数据的具体值(可以定义更方便的模式匹配投影)

Prj  items _ _ (
 Prj minItems   _ _ (Prj maxItems _ _ (
 Prj sumItems   _ _ (Prj sum2Items _ _ (
 Prj sumWeights _ _ _))))) =
      foldr (flip accumulate) myStats warehouse

和运行宁

main = do

  let avg, std :: Double
      avg  = fromIntegral sumItems / fromIntegral items
      std  = sqrt ( fromIntegral sum2Items / fromIntegral items + avg**2 )

  printf (unlines [ "%i item types"
                       , "min availability is %i"
                       , "max availability is %i"
                       , "there are a total of %i items"
                       , "average availability is %0.2f +/- %0.2f items"
                       , "the total weight of the goods is %0.2f" ])
              items minItems maxItems sumItems avg std sumWeights

我们得到

3 item types
min availability is 9
max availability is 21
there are a total of 42 items
average availability is 14.00 +/- 5.10 items
the total weight of the goods is 2.45

(撇开,更好的表示法)

添加 UndecidableInstancesTypeOperators 我们可以编写一个类型运算符来连接报告结果

data r :% r' = r :% r'
infixr 7 :%
class Values a r | a -> r where
  values :: a -> r
instance Values a n' => Values (Prj a v n) (n :% n') where
  values (Prj n _ _ a) = n :% values a
instance Values (Nop v) () where
  values _ = ()

现在,我们可以按照定义的方式编写预期的聚合值

items :% minItems :% maxItems :% sumItems :% sum2Items :% sumWeights :% () =
  values $ foldr (flip accumulate) myStats warehouse