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

mhw*_*bat 6 haskell

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

我想要一个我知道的类型总是在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
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.但是我的模块的用户会发现UnitIntervals和Doubles的算术很混乱.例如,

?> let a = toUnitInterval 0.5
?> let b = 0.25 :: Double
?> toUnitInterval $ (fromUnitInterval a) * b
UnitInterval 0.125
Run Code Online (Sandbox Code Playgroud)

当然我可以做UnitInterval一个派生的实例Num,所以只要我坚持UnitIntervals ,我就可以很容易地做算术.

?> a*a
UnitInterval 0.25
?> a+a+a
UnitInterval 1.5 -- Oops! out of range
Run Code Online (Sandbox Code Playgroud)

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

但是等等......也许有更好的方法.我可以做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
Run Code Online (Sandbox Code Playgroud)

但那不编译.事后来看,我看到了这是因为我需要约束结果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’
Run Code Online (Sandbox Code Playgroud)

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

J. *_*son 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)))
Run Code Online (Sandbox Code Playgroud)

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

a :: UnitInterval Double
b :: UnitInterval Double
a + b
Run Code Online (Sandbox Code Playgroud)

使用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)
  ...
Run Code Online (Sandbox Code Playgroud)

最后的方法是允许无限制的操作,但只允许用户"查看" 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
Run Code Online (Sandbox Code Playgroud)

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