忍受我...这个问题需要一些解释,但我认为这是一个有趣的问题,我认为其他人已经面临过这个问题。
我希望我知道的类型的值始终为0到1(包括0和1)。
这很容易做到,我可以创建一个类型UnitInterval
,只展示我的智能构造函数toUnitInterval
和解构函数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
到目前为止一切顺利。但是我的模块的用户会发现UnitInterval
和Double
s的算术很混乱。例如,
λ> 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
但是我可以为Num
编写UnitInterval
的自定义实现,其中像+
这样的操作会进行检查。
但是我的模块的用户需要进行复杂的计算,其中部分结果不在范围内。
因此,他们必须将所有内容转换为Double
s,进行计算,最后转换回UnitInterval
。
但是等等......也许还有更好的方法。我可以让UnitInterval
成为一个仿函数!
像fmap (\x -> x * exp x) a
这样的表达式应该给出结果
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 :(得分:5)
由于(+)
操作下[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],只要它们在被查看之前返回那里。
答案 1 :(得分: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之间的数字表示为无限的比特流。实施乘法将是一个有趣的挑战;如果你以某种方式设法找出除法,你不必担心除零,因为它只会永远运行。