繁体   English   中英

一种易于算术并且在边界内保证的类型

[英]A type that's easy to do arithmetic with and is guaranteed in bounds

忍受我...这个问题需要一些解释,但我认为这是一个有趣的问题,我认为其他人已经面对它了。

我想要一个我知道的类型总是在0到1之间的值,包括0和1。 这很容易做到,我可以创建一个UnitInterval类型,只显示我的智能构造函数, toUnitIntervalfromUnitInterval解构fromUnitInterval

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | A number on the unit interval 0 to 1, inclusive.
newtype UnitInterval = UnitInterval Double
  deriving (Show, Eq, Ord, Fractional, Floating)

-- | Convert a value to a @UnitInterval@. The value will be capped to
--   the unit interval.
toUnitInterval :: Double -> UnitInterval
toUnitInterval = UnitInterval . max 0 . min 1

fromUnitInterval :: UnitInterval -> Double
fromUnitInterval (UnitInterval x) = x

到现在为止还挺好。 但是我的模块的用户会发现使用UnitIntervalDouble的算法很麻烦。 例如,

λ> let a = toUnitInterval 0.5
λ> let b = 0.25 :: Double
λ> toUnitInterval $ (fromUnitInterval a) * b
UnitInterval 0.125

当然我可以使UnitInterval成为Num的派生实例,所以只要我坚持使用UnitInterval ,我就可以很容易地进行算术运算。

λ> a*a
UnitInterval 0.25
λ> a+a+a
UnitInterval 1.5 -- Oops! out of range

但我可以为UnitInterval编写一个Num的自定义实现,其中像+边界操作进行边界检查。 但是我的模块的用户需要进行复杂的计算,其中部分结果不在范围内。 因此,他们必须将所有内容转换为Double s,进行计算,最后转换回UnitInterval

但是等等......也许有更好的方法。 我可以使UnitInterval成为一个函子! fmap (\\x -> x * exp x) a这样的UnitInterval 0.8243606353500641应该给出结果UnitInterval 0.8243606353500641 很好,干净的代码。 现在, Functor具有类型(* → *) ,而UnitInterval具有类型* 但我可以改变这一点,就像这样......

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | A number on the unit interval 0 to 1, inclusive.
newtype UnitInterval a = UnitInterval a
  deriving (Show, Eq, Ord, Num, Fractional, Floating)

-- | Convert a value to a @UnitInterval@. The value will be capped to
--   the unit interval.
toUnitInterval :: (Num a, Ord a) => a -> UnitInterval a
toUnitInterval = UnitInterval . max 0 . min 1

fromUnitInterval :: UnitInterval a -> a
fromUnitInterval (UnitInterval x) = x

instance Functor UnitInterval  where
  fmap f (UnitInterval x) = toUnitInterval (f x) -- line 16

但那不编译。 事后看来,我认为这是因为我需要约束fmap的结果,这会给它一个与Functor不同的类型签名。

amy.hs:16:29:
    No instance for (Num b) arising from a use of ‘toUnitInterval’
    Possible fix:
      add (Num b) to the context of
        the type signature for
          fmap ∷ (a → b) → UnitInterval a → UnitInterval b
    In the expression: toUnitInterval (f x)
    In an equation for ‘fmap’:
        fmap f (UnitInterval x) = toUnitInterval (f x)
    In the instance declaration for ‘Functor UnitInterval’

叹了闻......回到第一个版本,带着难看的算术。 有没有人有更好的解决方案?

你将要面对一些困难,因为[0,1]上的数字在(+)操作下没有关闭 换句话说,“[0,1]”内的保证不会通过添加来保留。

所以有几种方法可以解释你想要的东西。 一个是你可能正在寻找每个你重新约束值的“操作阶段”[0,1]

mapConstrain :: (Num a, Ord a) => (a -> a) -> (UnitInterval a -> UnitInterval a)
mapConstrain f (UnitInterval val) = UnitInterval (max 0 (min 1 (f val)))

像这样的操作将限制你会发现,因为很难写出像这样的东西

a :: UnitInterval Double
b :: UnitInterval Double
a + b

使用mapConstrain 然而, Applicative类视图提出了解决此问题的机制。

另一种方法是在每次操作后进行约束。 然后我们可以实例化Num

newtype UnitInterval a = UI a

constrain :: (Num a, Ord a) => a -> a
constrain = max 0 . min 1

instance Num a => Num (UnitInterval a) where
  UI a + UI b = UI (constrain $ a + b)
  UI a * UI b = UI (constrain $ a * b) -- not technically needed!
  abs (UI a)  = UI a
  signum (UI a) = UI (signum a)
  ...

最后的方法是允许无限制的操作,但只允许用户“查看”有效的UnitInterval值。 这可能是最简单的实现,因为您可以自动派生Num

newtype UnitInterval a = UI a deriving Num

getUI :: (Num a, Ord a) => UnitInterval a -> Maybe a
getUI (UI a) = if (a <= 1 && a >= 0) then Just a else Nothing

或者,你可以用一个最后的constrain命中它。 当然,这种操作模式允许UnitInterval值超出[0,1],只要它们在被查看之前返回那里。

当我考虑0和1之间的数字时,我倾向于考虑两件事之一:

有理数

data SM = SM {diff :: Natural, den :: Natural}

toRatio :: SM -> Ratio Natural
toRatio (SM diff den) = (1 + den + diff) / (1 + den)

instance Eq SM where
  SM diffx denx == SM diffy deny =
     diffx * (deny + 1) == diffy * (denx + 1)

这些数字确实保证在正确的范围内。

可计算的实数

这些更可怕,但有一个包给他们。 粗略地说,您可以将0到1之间的数字表示为无限的比特流。 实施乘法将是一个有趣的挑战; 如果你以某种方式设法找出除法,你将不必担心除零,因为它将永远运行。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM