haskell - 一种易于进行算术运算且有边界保证的类型

标签 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

到目前为止一切顺利。但是我的模块的用户会发现 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,进行计算,最后再转换回 UnitInterval。

但是等等...也许有更好的方法。我可以使 UnitInterval 成为一个仿函数! 像 fmap (\x -> x * exp x) a 这样的表达式应该给出结果 单位间隔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

或者,您也可以使用最后一个约束来实现它。当然,这种操作模式允许 UnitInterval 值超出 [0, 1],只要它们在被查看之前返回到那里即可。

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

相关文章:

haskell - Monad 还可以测量副作用

haskell - 返回类型丢失 IO

haskell - 在 Haskell 中计算 md5 摘要最便宜的方法是什么?

scala - 从不重新计算相同的部分评估两次的意义上说,部分函数应用程序可以是惰性的吗?

haskell - 使用 getLine 和 putStr 时发生 IO 乱序

haskell - 是否可以使用 Template Haskell 获得任何类型的表达式?

Haskell - 如何在实例中简单地生成 "div"或 "/"?

haskell - 如何在 Haskell 中使用 let 声明函数(仅限单行操作)

haskell - 我可以完全禁用 GHC 上的类型检查,以便将其用作函数式语言的编译目标吗?

Haskell 记录,更干净的方法?