忍受我...这个问题需要一些解释,但是我认为这是一个有趣的问题,我认为其他人也遇到过。

我想拥有一个我知道它将始终具有介于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

到现在为止还挺好。但是我模块的用户会发现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

但是我可以为Num编写一个自定义的UnitInterval实现,其中+之类的操作会进行边界检查。
但是我模块的用户将需要进行复杂的计算,其中部分结果将不在范围内。
因此,他们将必须将所有内容都转换为Double,进行计算,最后将其转换回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,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]的范围,只要它们在被查看之前就回到了那里。

关于haskell - 一种易于进行算术并在范围内保证的类型,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/30531944/

10-11 17:41