如何使我的概率密度类型成为 Monoid 的实例?

How to make my probability density type an instance of Monoid?

我有一个类型可以描述post-calibration radiocarbon date probability distributions。问题的细节和背景无关紧要:它归结为 _calPDFCals 中每年 _calPDFDens 中的一个概率值:

data CalPDF = CalPDF {
    -- | Sample identifier, e.g. a lab number
      _calPDFid :: String
    -- | Years calBCAD
    , _calPDFCals :: VU.Vector YearBCAD
    -- | Probability densities for each year in '_calPDFCals'
    , _calPDFDens :: VU.Vector Float
    }

(VUData.Vector.Unboxed)

现在:通常的做法是对多个此类分布求和以得出和概率分布。这意味着对 _calPDFCals 中的年份进行完全外部联接,然后对 _calPDFDens 中的各个值求和。我是这样实现的:

sumPDFs :: CalPDF -> CalPDF -> CalPDF
sumPDFs = combinePDFs (+)

combinePDFs :: (Float -> Float -> Float) -> CalPDF -> CalPDF -> CalPDF
combinePDFs f (CalPDF name1 cals1 dens1) (CalPDF name2 cals2 dens2) = 
    let startRange = minimum [VU.head cals1, VU.head cals2]
        stopRange = maximum [VU.last cals1, VU.last cals2]
        emptyBackdrop = zip [startRange..stopRange] (repeat (0.0 :: Float))
        pdf1 = VU.toList $ VU.zip cals1 dens1
        pdf2 = VU.toList $ VU.zip cals2 dens2
        pdfCombined = fullOuter f pdf2 (fullOuter f pdf1 emptyBackdrop)
        pdfNew = CalPDF (name1 ++ "+" ++ name2) (VU.fromList $ map fst pdfCombined) (VU.fromList $ map snd pdfCombined)
    in normalizeCalPDF pdfNew
    where
        -- 
        fullOuter :: (Float -> Float -> Float) -> [(YearBCAD, Float)] -> [(YearBCAD, Float)] -> [(YearBCAD, Float)]
        fullOuter _ xs [] = xs
        fullOuter _ [] ys = ys
        fullOuter f xss@(x:xs) yss@(y:ys)
            | fst x == fst y = (fst x, f (snd x) (snd y)) : fullOuter f xs ys
            | fst x < fst y  = x                          : fullOuter f xs yss
            | otherwise      = y                          : fullOuter f xss ys

我想知道是否可以重写这段代码,使 CalPDF 成为 Monoid 的实例,而 sumPDFs 成为 <>.

我无法克服并导致我 post 的问题是 mempty 应该是什么样子。我在 combinePDFs: emptyBackdrop 中已经有了这样的东西。这在我的实施中是必需的,如果两个输入 PDF 之间不重叠 fillcomplete 年。

emptyBackdrop 满足 mempty 的一些要求,但它取决于输入的 PDF。从理论上讲,真正的 mempty 应该是一个 CalPDF,它从时间的开始开始,到时间的结束结束,并且将这些无限年中的每一年都归为零概率。但这不能用未装箱的向量来实现。

有没有一种优雅的方法来制作 CalPDFMonoid 的实例?将它作为我已有的 Semigroup 的实例是否有用?


编辑:正如@leftaroundabout 所建议的,这里是上述设置的可重现的最小实现。

main :: IO ()
main = do
    let myPDF1 = [(1,1), (2,1), (3,1)]
        myPDF2 = [(2,1), (3,1), (4,1)]
    putStrLn $ show $ sumPDFs myPDF1 myPDF2

type CalPDF = [(Int, Float)]

sumPDFs :: CalPDF -> CalPDF -> CalPDF
sumPDFs pdf1 pdf2 = 
    let startRange = minimum [fst $ head pdf1, fst $ head pdf2]
        stopRange = maximum [fst $ last pdf1, fst $ last pdf2]
        emptyBackdrop = zip [startRange..stopRange] (repeat (0.0 :: Float))
        pdfCombined = fullOuter pdf2 (fullOuter pdf1 emptyBackdrop)
    in pdfCombined
    where
        fullOuter :: [(Int, Float)] -> [(Int, Float)] -> [(Int, Float)]
        fullOuter xs [] = xs
        fullOuter [] ys = ys
        fullOuter xss@(x@(year1,dens1):xs) yss@(y@(year2,dens2):ys)
            | year1 == year2 = (year1, dens1 + dens2) : fullOuter xs ys
            | year1 < year2  = x                      : fullOuter xs yss
            | otherwise      = y                      : fullOuter xss ys

任何 Semigroup 都可以用 Maybe 提升到 Monoid

考虑稍微修改一下您的类型。

import Data.Map (Map)
import qualified Data.Map as M

data CalPDF = CalPDF
    { _calPDFid :: [String]
    , _calPDFdens :: Map YearBCAD Float
    }

实例现在确实可以很短:

instance Semigroup CalPDF where
    CalPDF id dens <> CalPDF id' dens' = CalPDF
        (id <> id')
        (M.unionWith (+) dens dens')

instance Monoid CalPDF where
    mempty = CalPDF mempty mempty

我使用 [String] 代替单个 + 分隔的 String 有两个原因:它允许你在你的名字中使用 + 而不会产生歧义,并且它使 <> 更简单一些,因为当一个或另一个参数为空 String 时您不需要避免添加 + 以保持 law-abiding。 Pretty-printers 仍然可以用 +s 显示这个,例如intercalate "+".

您可以使用 HashMapIntMap 代替 Map,如果其中之一更符合您的需求,则基本相同。