类型推断会干扰参考透明度

时间:2014-11-19 14:56:03

标签: haskell referential-transparency adhoc-polymorphism

Haskell语言在参考透明度方面提供的确切承诺/保证是什么?至少Haskell报告没有提到这个概念。

考虑表达式

(7^7^7`mod`5`mod`2)

我想知道这个表达式是否为1.为了我的安全,我会做两次表演:

( (7^7^7`mod`5`mod`2)==1, [False,True]!!(7^7^7`mod`5`mod`2) )

现在为(True,False)提供了GHCi 7.4.1。

显然,这个表达现在是参考不透明的。如何判断程序是否受此类行为影响?我可以用::来淹没程序,但这并不能让它变得非常易读。我之间是否还有其他类的Haskell程序?那是在完全注释和未注释的之间吗?

(除了我在SO上发现的唯一related question之外,还必须有其他内容)

7 个答案:

答案 0 :(得分:20)

对于“兼容”的任何合理定义,我认为无法保证在不同类型中评估多态类型表达式(如5)会产生“兼容”结果。

GHCi会议:

> class C a where num :: a
> instance C Int    where num = 0
> instance C Double where num = 1
> num + length []  -- length returns an Int
0
> num + 0          -- GHCi defaults to Double for some reason
1.0

由于length []0应该相同,因此它的参照透明度正在下降,但是在不同类型下使用num

此外,

> "" == []
True
> [] == [1]
False
> "" == [1]
*** Type error

可以在最后一行预期False

因此,我认为仅当指定确切类型来解析多态时,引用透明性才会成立。系统F的显式类型参数应用程序可以始终用变量替换变量而不改变语义:据我所知,GHC在优化期间内部完成此操作以确保语义不受影响。实际上,GHC Core有明确的类型参数可以传递。

答案 1 :(得分:17)

问题是重载,这确实违反了引用透明度。你不知道像(+)这样的东西在Haskell中做了什么;这取决于类型。

当数值类型在Haskell程序中不受约束时,编译器使用类型默认来选择一些合适的类型。这是为了方便,通常不会带来任何意外。但在这种情况下,它确实导致了一个惊喜。在ghc中,您可以使用-fwarn-type-defaults来查看编译器何时使用默认值为您选择类型。您还可以将行default ()添加到模块中以停止所有默认操作。

答案 2 :(得分:15)

我想到了一些可能有助于澄清事情的事情......

表达式mod (7^7^7) 5的类型为Integral a,因此有两种常用方法可将其转换为Int

  1. 使用Integer操作和类型执行所有算术,然后将结果转换为Int
  2. 使用Int操作执行所有算术。
  3. 如果在Int上下文中使用表达式,Haskell将执行方法#2。如果你想强迫Haskell使用#1,你必须写:

    fromInteger (mod (7^7^7) 5)
    

    这将确保使用Integer操作和类型执行所有算术运算。

    当您在ghci REPL中输入表达式时,默认规则将表达式键入Integer,因此使用方法#1。当您将表达式与!!运算符一起使用时,它被输入为Int,因此它是通过方法#2计算的。

    我原来的回答:

    在Haskell中评估表达式,如

    (7^7^7`mod`5`mod`2)
    

    完全取决于使用哪个Integral实例,这是每个Haskell程序员都学会接受的。

    每个程序员(使用任何语言)必须注意的第二件事是数字操作会受到溢出,下溢,精度损失等因素的影响,因此算术规则可能并不总是成立。例如,x+1 > x并非总是如此;加法和多个实数并不总是相关的;分配法并不总是成立;当您创建溢出表达式时,您将进入未定义行为领域。

    此外,在这种特殊情况下,有更好的方法来评估这个表达式,这样可以保留我们对结果应该是什么的更多期望。特别是,如果你想高效准确地计算一个^ b mod c,你应该使用“power mod”算法。

    更新:运行以下程序以查看Integral实例的选择如何影响表达式的计算结果:

    import Data.Int
    import Data.Word
    import Data.LargeWord -- cabal install largeword
    
    expr :: Integral a => a
    expr = (7^e `mod` 5)
      where e = 823543 :: Int
    
    main :: IO ()
    main = do
      putStrLn $ "as an Integer: " ++ show (expr :: Integer)
      putStrLn $ "as an Int64:   " ++ show (expr :: Int64)
      putStrLn $ "as an Int:     " ++ show (expr :: Int)
      putStrLn $ "as an Int32:   " ++ show (expr :: Int32)
      putStrLn $ "as an Int16:   " ++ show (expr :: Int16)
      putStrLn $ "as a Word8:    " ++ show (expr :: Word8)
      putStrLn $ "as a Word16:   " ++ show (expr :: Word16)
      putStrLn $ "as a Word32:   " ++ show (expr :: Word32)
      putStrLn $ "as a Word128:  " ++ show (expr :: Word128)
      putStrLn $ "as a Word192:  " ++ show (expr :: Word192)
      putStrLn $ "as a Word224:  " ++ show (expr :: Word224)
      putStrLn $ "as a Word256:  " ++ show (expr :: Word256)
    

    和输出(用GHC 7.8.3(64位)编译:

    as an Integer: 3
    as an Int64:   2
    as an Int:     2
    as an Int32:   3
    as an Int16:   3
    as a Word8:    4
    as a Word16:   3
    as a Word32:   3
    as a Word128:  4
    as a Word192:  0
    as a Word224:  2
    as a Word256:  1
    

答案 3 :(得分:7)

  

Haskell语言在参考透明度方面提供的确切承诺/保证是什么?至少Haskell报告没有提到这个概念。

Haskell没有提供准确的承诺或保证。存在许多函数,例如unsafePerformIOtraceShow,它们不是引用透明的。然而,名为 Safe Haskell 的扩展提供了以下承诺:

  

参考透明度 - 安全语言中的函数是确定性的,评估它们不会产生任何副作用。 IO monad中的函数仍然允许并且像往常一样运行。但是,根据其类型,任何纯粹的功能都保证确实是纯粹的。此属性允许安全语言的用户信任这些类型。这意味着,例如,unsafePerformIO :: IO a - >安全语言不允许使用某项功能。

Haskell提供了一个非正式的承诺:Prelude和基础库往往没有副作用,Haskell程序员倾向于用副作用来标记事物。

  

显然,这个表达现在是参考不透明的。如何判断程序是否受此类行为影响?我可以用:: all来淹没程序,但这并不能让它变得非常易读。我之间是否还有其他类的Haskell程序?那是在完全注释和未注释的之间吗?

正如其他人所说,问题出现在这种行为上:

Prelude> ( (7^7^7`mod`5`mod`2)==1, [False,True]!!(7^7^7`mod`5`mod`2) )
(True,False)
Prelude> 7^7^7`mod`5`mod`2 :: Integer
1
Prelude> 7^7^7`mod`5`mod`2 :: Int
0

这是因为7^7^7是一个巨大的数字(大约700,000个十进制数字),很容易溢出64位Int类型,但问题在32位系统上无法重现:

Prelude> :m + Data.Int
Prelude Data.Int> 7^7^7 :: Int64
-3568518334133427593
Prelude Data.Int> 7^7^7 :: Int32
1602364023
Prelude Data.Int> 7^7^7 :: Int16
8823

如果使用rem (7^7^7) 5,则Int64的余数将报告为-3,但由于-3相当于+2模5,mod报告为+2。

由于Integer类的默认规则,左侧使用Integral答案;由于Int的类型,右侧使用特定于平台的(!!) :: [a] -> Int -> a类型。如果对Integral a使用适当的索引运算符,则可以获得一致的结果:

Prelude> :m + Data.List
Prelude Data.List> ((7^7^7`mod`5`mod`2) == 1, genericIndex [False,True] (7^7^7`mod`5`mod`2))
(True,True)

这里的问题是不是引用透明度,因为我们调用^的函数实际上是两个不同的函数(因为它们有不同的类型) )。让你绊倒的是类型类,它是Haskell中约束歧义的实现;你已经发现这种模糊性(与不受约束的模糊性 - 即参数类型不同)可以提供违反直觉的结果。这不应该太令人惊讶,但有时候它确实有点奇怪。

答案 4 :(得分:5)

已选择其他类型,因为!!需要Int。完整计算现在使用Int而不是Integer

λ> ( (7^7^7`mod`5`mod`2 :: Int)==1, [False,True]!!(7^7^7`mod`5`mod`2) )
(False,False)

答案 5 :(得分:4)

您认为这与参考透明度有什么关系?您对7^mod52==的使用是应用变量到字典,是的,但我不明白为什么你认为这个事实使Haskell引用不透明。毕竟,经常将相同的函数应用于不同的参数会产生不同的结果!

参考透明度与此表达式有关:

let x :: Int = 7^7^7`mod`5`mod`2 in (x == 1, [False, True] !! x)

x此处是单个值,并且应始终具有相同的单个值。

相比之下,如果你说:

let x :: forall a. Num a => a; x = 7^7^7`mod`5`mod`2 in (x == 1, [False, True] !! x)

(或使用内联的表达式,这是等效的),x现在是一个函数,并且可以根据您提供给它的Num参数返回不同的值。您也可以抱怨let f = (+1) in map f [1, 2, 3][2, 3, 4],但let f = (+3) in map f [1, 2, 3][4, 5, 6],然后说“Haskell根据上下文为map f [1, 2, 3]提供不同的值,因此它是参考不透明“!

答案 6 :(得分:2)

可能另一种类型推理和参考 - 透明度相关的事情是“可怕的”Monomorphism restriction(确切地说,它的缺席)。直接引用:

  

一个例子,来自“哈斯克尔的历史”:
  考虑来自Data.List

的genericLength函数      

genericLength :: Num a => [b] -> a

     

考虑功能:

     

f xs = (len, len) where len = genericLength xs

     

len的类型为Num a => a,如果没有单态限制,则可以计算两次

请注意,在这种情况下,两个表达式的类型都相同。结果也是如此,但替代并非总是可行。