【问题标题】:What is the optimal way of representing a floating point number in a range from 0 to 1?表示 0 到 1 范围内的浮点数的最佳方式是什么?
【发布时间】:2013-02-23 08:06:32
【问题描述】:

我正在寻找能够表示值 0.2131230.01.0 的数值类型,但拒绝超出范围的值,例如 -0.21231.2312。是否存在适合该目的的特定类型,将数字限制在特定范围内的最佳通用方法是什么?

当然,想到的第一个答案是:只使用Double,但是被 Haskell 的类型系统宠坏了,我已经习惯了在类型级别上最大限度地保护程序。

【问题讨论】:

  • @TTT 在 SO 上,相关标签隐含的语言参考是惯例。所以,是的。
  • 你看过这个吗(它没有给你静态类型检查,但在定义模块之外,不可能有无效值)? stackoverflow.com/questions/4557394/…
  • 这个数据类型需要支持哪些操作?
  • @NikitaVolkov 如果您不需要支持任何操作,那么data NumberBetweenZeroAndOne = NumberBetweenZeroAndOne 支持您需要的所有功能,并且无需实际存储 [0,1] 中的数字以进行引导。但大概这不足以满足您的目的。您需要此数据类型支持哪些操作?
  • “最佳”是指“惯用的”,还是您真的有兴趣在最大限度地提高精度的同时最大限度地减少开销?

标签: haskell types floating-point numbers


【解决方案1】:

严肃的建议

您可以在适当位大小的单词周围使用新类型包装器(和智能构造器):

newtype SmallFrac = SF Word64

-- Example conversion (You'd actually want to make
-- instances of common classes, I assume)
sfToDouble :: SmallFrac -> Double
sfToDouble (SF x) = fromIntegral x / fromIntegral (maxBound `asTypeOf` x)

instance Show SmallFrac where
    show = show . sfToDouble

实现乘法和除法的成本可能比您想的要高,但至少加法很容易(防止上溢/下溢的模数),而且您声称不需要任何运算甚至更好。

一个不太有用的建议

如果您只需要一个表示值介于 1 和 0 之间的符号,那么请采纳 dave4420 的建议,只需要一个单位类型:

newtype SmallFrac = SF ()

没有针对这种类型的操作,甚至没有转换到/从其他类型的兴趣,例如Double,但这符合所述的要求。

【讨论】:

  • 您不太有用的建议暗示了设计数据表示的一个非常重要的想法。我第一次明确接触到这个想法是在 Andrej Bauer 的an article 中。
  • 同意!我的意思是符号表示可能对提问者的具体问题没那么有用,并不是说它一般没用。
  • 我真的很喜欢你的“严肃”建议。在所有答案中,它是唯一接近类型级别的问题。然而,我完全不明白如何使用“不太有用的建议”。你能详细说明一下吗?
  • 请注意,“严重”的答案只是一个固定点类型,其中有零个非小数位。我从未测试过这样的极端情况,但您也许可以使用我的 fixedpoint-simple 包并使用模板 haskell 宏 mkFixedPoint 生成带有所有有用类的类型 FixedPoint_0_64
【解决方案2】:

不标准。你必须做一个——我建议smart constructor。请记住,尽管这种类型支持的数字操作很少——您不能添加它们并将它们保留在集合中,也不能否定它们,所以我建议不要使用 Num 实例。在乘法上使用Monoid 是合理的。

【讨论】:

  • 这也将是一个动态检查,不幸的是它没有达到目的。这也会导致每次实例化的开销都非常不合理。
  • @NikitaVolkov,那你就不走运了。 Haskell 缺乏了解有关编译类型值的详细信息的能力。参照。伊德里斯、阿格达、科克。
  • @NikitaVolkov 为什么智能构造函数是“不合理的开销”?你真的相信这会导致性能问题,还是只是学术上的,并且“感觉”应该有更有效的方法?
【解决方案3】:

基于Double的表示

newtype Rep1 = Rep1 Double

checkRange :: Double -> Maybe Double
checkRange x
  | 0 < x && x < 1 = Just x
  | otherwise = Nothing

toRep1 :: Double -> Maybe Rep1
toRep1 x = Rep1 . (\x -> tan $ (x-0.5) * pi) <$> checkRange x

fromRep1 :: Rep1 -> Double
fromRep1 (Rep1 x) = atan x / pi + 0.5

基于整数的表示

data Rep2 = Rep2 Integer Integer

fromRep2 :: Rep2 -> Double
fromRep2 (Rep2 a b) = fromIntegral (abs a) / fromIntegral (abs a + abs b + 1)

toRep2 :: Double -> Maybe Rep2
toRep2 = error "left to the reader"

【讨论】:

  • 这些只是例子,当然,还有更多的可能性。
【解决方案4】:

智能构造器模式的一种变体。

这可能有点过头了。

{-# LANGUAGE TemplateHaskell #-}
module Foo (Foo(), doubleFromFoo,
            maybeFooFromDouble, unsafeFooFromDouble, thFooFromDouble)
where
import Language.Haskell.TH

不管怎样,标准的newtype...

newtype Foo = Foo Double

获取Double 很容易...

doubleFromFoo :: Foo -> Double
doubleFromFoo (Foo x) = x

在运行时放入 Double 会导致运行时检查,无法绕过...

maybeFooFromDouble :: Double -> Maybe Foo
maybeFooFromDouble x
        | 0 <= x && x <= 1 = Just (Foo x)
        | otherwise        = Nothing

...除非您对不安全感到高兴(并且有一些社交手段来强制所有unsafeFooFromDouble 的使用实际上都是安全的)...

unsafeFooFromDouble :: Double -> Foo
unsafeFooFromDouble = Foo

但如果它是编译时常量,您可以在编译时进行检查,而无需运行时开销:

thFooFromDouble :: (Real a, Show a) => a -> Q Exp
thFooFromDouble x
        | 0 <= x && x <= 1 = return $ AppE (VarE 'unsafeFooFromDouble)
                                           (LitE (RationalL (toRational x)))
        | otherwise        = fail $ show x ++ " is not between 0 and 1"

这就是你使用最后一个函数的方式:

$(thFooFromDouble 0.3)

切记不要在$(! 之间放置任何空格。

【讨论】:

  • 哦,麻烦了。这行不通吧?客户端代码中没有数据构造函数,所以thFooFromDouble生成的代码无法使用。我想如果你导出了unsafeFooFromDouble,你可以在生成的代码中使用它,这比直接导出数据构造函数要好。然后你必须为unsafe grep 你的代码,并确保unsafeFooFromDouble 的任何使用都有注释说明为什么它们是安全的,但你也许应该为unsafePerformIO 等这样做。是的,我会同意的。
  • 这是错误的——如果您在 TH 宏中使用引号语法(即:'Foo)而不是构造字符串名称(即:mkName "Foo"),那么您将获得名称的早期绑定.卫生,无需出口。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-05
  • 2020-02-25
  • 2017-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多