eta转换以严格的语言更改语义

时间:2018-09-07 00:49:43

标签: haskell lambda ocaml lazy-evaluation ml

使用此OCaml代码:

let silly (g : (int -> int) -> int) (f : int -> int -> int) =
        g (f (print_endline "evaluated"; 0))

silly (fun _ -> 0) (fun x -> fun y -> x + y)

它打印evaluated并返回0。但是,如果我扩展f以获得g (fun x -> f (print_endline "evaluated"; 0) x)evaluated将不再打印。

此SML代码相同:

fun silly (g : (int -> int) -> int, f : int -> int -> int) : int =
        g (f (print "evaluated" ; 0));

silly ((fn _ => 0), fn x => fn y => x + y);

另一方面,即使使用严格的编译指示,此Haskell代码也不会打印evaluated

{-# LANGUAGE Strict #-}

import Debug.Trace

silly :: ((Int -> Int) -> Int) -> (Int -> Int -> Int) -> Int
silly g f = g (f (trace "evaluated" 0))

main = print $ silly (const 0) (+)

(不过,我可以使用seq做到这一点,这对我来说很有意义)

虽然我了解OCaml和SML在理论上做正确的事情,但是有没有实际的理由更喜欢这种行为而不是“懒惰”行为? Eta-contract是一种常见的重构工具,我完全不敢以严格的语言使用它。我觉得我应该对所有事物进行奇异的eta扩展,因为否则可以在不应该使用部分应用函数的参数的情况下对其进行评估。 “严格”行为什么时候有用?

Strict编译指示下,Haskell为什么以及如何表现不同?有什么我可以熟悉的参考文献,可以更好地理解现有方法的设计空间和利弊?

2 个答案:

答案 0 :(得分:3)

要解决您的问题的技术部分,eta转换还会更改惰性语言中表达式的含义,您只需要考虑其他类型构造函数的eta-rule,例如+而不是->。

这是二进制和的eta-规则:

(case e of Lft y -> f (Lft y) | Rgt y -> f (Rgt y))  =  f e    (eta-+)

这个方程式在急切的评估中成立,因为e总是在两边都减少。但是,在懒惰的评估下,r.h.s。仅在e也强制使用时才减少f。这可能会使l.h.s. r.h.s.不会。因此,方程式在懒惰的语言中不成立。

在Haskell中使其具体化:

f x = 0
lhs = case undefined of Left y -> f (Left y); Right y -> f (Right y)
rhs = f undefined

在这里,尝试打印lhs会发散,而rhs会产生0

关于这一点,还有很多可以说的,但实质是两种评估制度的方程式理论都是双重的。

潜在的问题是,在懒惰的制度下,每种类型都被_|_(非终止)居住,而在渴望的情况下则不是。这具有严重的语义后果。特别是,Haskell中没有归纳类型,并且您不能证明结构性递归函数的终止,例如列表遍历。

类型理论方面有一系列研究,区分数据类型(严格)和协数据类型(非严格),并以双重方式同时提供这两种类型,从而兼顾了两个方面。

编辑:关于为什么编译器不应该扩展函数的问题:这将彻底破坏每种语言。在具有最明显效果的严格语言中,因为通过多种功能抽象 stage 的功能是一项功能。最简单的例子可能是这样:

let make_counter () =
  let x = ref 0 in
  fun () -> x := !x + 1; !x

let tick = make_counter ()
let n1 = tick ()
let n2 = tick ()
let n3 = tick ()

但是影响并不是唯一的原因。扩展eta也可以大大改变程序的性能!同样,您有时想要上演特效,有时有时也想要上演 work

match :: String -> String -> Bool
match regex = \s -> run fsm s
  where fsm = ...expensive transformation of regex...

matchFloat = match "[0-9]+(\.[0-9]*)?((e|E)(+|-)?[0-9]+)?"

请注意,我在这里使用了Haskell,因为此示例表明,无论是急切的语言还是惰性的语言,隐式的eta扩展都是不可取的!

答案 1 :(得分:1)

关于您的最后一个问题(为什么Haskell会这样做),“严格Haskell”的行为与真正严格的语言有所不同的原因是Strict扩展名并没有真正将评估模型从惰性转换为惰性严格。默认情况下,它只是将绑定的一个子集变成“严格”绑定,并且仅在限制Haskell意义上将评估强制为弱头法线形式。同样,它只影响扩展打开的模块中的绑定。它不会追溯影响其他地方进行的绑定。 (此外,如下所述,严格性在部分函数应用中不会生效。必须在强制使用任何参数之前完全应用该函数。)

在您的特定Haskell示例中,我相信Strict扩展名的唯一作用就好像您在silly的定义中明确编写了以下bang模式:

silly !g !f = g (f (trace "evaluated" 0))

没有其他效果。特别是,它不会使const(+)的参数严格,也不会通常更改函数应用程序的语义以使其渴望。

因此,当术语silly (const 0) (+)print强制执行时,唯一的效果是将其对WHNF的参数求值作为silly函数应用的一部分。效果类似于写作(在非Strict Haskell中):

let { g = const 0; f = (+) } in g `seq` f `seq` silly g f

显然,将gf强制为其WHNF(它们是lambda)不会产生任何副作用,并且当应用silly时,const 0在剩余的参数中仍然很懒,因此产生的术语类似于:

(\x -> 0) ((\x y -> <defn of plus>) (trace "evaluated" 0))

(应该解释为不带Strict扩展名-这里都是懒惰的绑定),这里没有任何东西会引起副作用。

如上所述,这个示例掩盖了另一个微妙的问题。即使您已将所有视线都严格限制:

{-# LANGUAGE Strict #-}

import Debug.Trace

myConst :: a -> b -> a
myConst x y = x

myPlus :: Int -> Int -> Int
myPlus x y = x + y

silly :: ((Int -> Int) -> Int) -> (Int -> Int -> Int) -> Int
silly g f = g (f (trace "evaluated" 0))

main = print $ silly (myConst 0) myPlus

这仍然不会打印“评估”。这是因为,在对silly的严格版本强制使用第二个参数的myConst的评估中,该参数是myPlus和{{1}的严格版本的部分应用。 }在完全应用之前,不会强制使用任何参数。

这也意味着,如果将myPlus的定义更改为:

myPlus

然后,您将可以在很大程度上重现ML行为。因为myPlus x = \y -> x + y -- now it will print "evaluated" 现在已完全应用,它将强制其参数,并且将显示“已评估”。您可以在myPlus的定义中再次抑制eta展开f

silly

因为现在silly g f = g (\x -> f (trace "evaluated" 0) x) -- now it won't 强制使用第二个参数时,该参数已经存在于WHNF中(因为它是一个lambda),所以我们永远都不会使用myConst的应用,无论它是否完整。

最后,我想我不会将“ Haskell加上f扩展名和类似Strict这样的不安全副作用当作设计空间中的好点。它的语义可能(几乎)是连贯的,但是它们确实很奇怪。我认为唯一严重的用例是当您的某些代码的语义“显然”不依赖于懒惰或严格的评估,但是通过大量强制可以提高性能。然后,您只需打开trace即可提高性能,而不必考虑太多。