标签归档:eta-conversion

再谈eta-conversion与eta-expansion

之前写过与eta-conversion有关的两篇blog,参考:
scala中的eta-conversion
scala雾中风景(1): lambda表达式的缩写

前几天看了一下shapeless作者的blog,他提到func _ 即把func方法转成对象也被成为eta-expansion,对比eta-conversion正好是反过来的过程:

把 x => func(x) 简化为 func _ 或 func 的过程称为 eta-conversion

把 func 或 func _ 展开为 x => func(x) 的过程为 eta-expansion

让我想起之前blog里把eta-conversion说成只适用于只有一个参数的函数是不对的。

今天老高转了一篇blog也提到eta-expansion,看了一下他提到了场景更复杂一些,涉及在方法重载的情况下编译器在类型推导时怎么选择的。写这篇blog不讨论复杂的场景,只对以前的两篇blog做个纠正和补充。

之前我在了解eta-conversion的时候,google的第一个结果就是haskell里的http://www.haskell.org/haskellwiki/Eta_conversion

看到它的描述,函数:

\x -> abs x  //相当于scala里 x => abs(x)

经过eta转换后等价于 abs,因为这个例子里列举的函数参数只有一个,我误以为eta-conversion就只是针对只有一个参数的函数lambda表达式的缩写。

另外,Haskell里的函数默认就是柯里化的,即带有多个参数的函数最终都可转换为只有一个函数的高阶函数。参考http://learnyouahaskell.com/higher-order-functions,所以即使eta-conversion在haskell里只针对带有一个参数的函数也说得过去。

Every function in Haskell officially only takes one parameter…

eta-conversion并非Haskell发明的,而是在lambda演算里定义的: http://en.wikipedia.org/wiki/Lambda_calculus

大意上,当一个函数 newFunc定义为: x => oldFunc(x) 时,newFunc(x)oldFunc(x)行为上完全等价,所以newFunc也可以写为oldFunc

在scala里,eta-conversion对函数的参数个数并不限定:

scala> def hf(f: (Int,Int)=>Unit) { 
            print("ok") 
        }

scala> def foo(x:Int, y:Int) { 
            print("donothing") 
        }

scala> hf(foo _)  //把 (x,y) => foo(x, y) 简化为 foo _
ok

scala> hf(foo)  //把 (x,y) => foo(x, y) 简化为 foo 
ok

上面2种简化写法称为 eta-conversion,编译器会把它们还原成函数对象称为eta-expansion

早先我们通过一些书或资料了解部分应用函数(partial function)时,讲述例子里的”_”符合时都只是说是“占位符”,基本不会提到这背后与eta转换有什么关系。还原一下在hf方法里的 foo _foo:

foo _ 这里的占位符相当于所有的参数,而不是单个参数,即 (x,y) => foo(x,y) 里的x,y都被"_"替代

对于 foo _ 这种写法,编译器会严格按照部分应用函数来对待,把foo方法封装成一个函数对象。而对于后边不带下划线foo这种写法,编译器还要看上下文,判断究竟是对其调用求值,还是进行eta转换。比如这种情况:

scala> def foo() = 200

// f缺乏类型信息,推导时优先尝试对foo求值, evaluation
scala> val f = foo
f: Int = 200

// f缺乏类型信息,但明确了把foo当函数对象对待, eta-expansion
scala> val f = foo _
f: () => Int = <function0>

// f声明了类型,且f的类型与foo的返回类型不匹配,尝试eta-expansion
scala> val f: ()=>Int = foo
f: () => Int = <function0>

在把方法转换为部分应用函数对象的过程中可能触发 eta-expansion,但也不是所有的部分应用函数都会符合 eta-expansion

scala> def foo(x:Int, y:Int) = x+y

// 不符合 eta-expansion
scala> val f = foo(_:Int, 3)
f: Int => Int = <function1>

上面产生的部分应用函数与原 x => foo(x, 3)(x,y) => foo(x, y) 不等价,并不是eta转换。

// 符合 
scala> val f = foo(_:Int, _:Int)
f: (Int, Int) => Int = <function2>

上面产生的部分应用函数与原(x,y) => foo(x, y) 等价,符合eta-expansion

scala中的eta-conversion

微博上“甲骨文java社区”给了一段java8的lambda表达的例子,然后撒迦同学 指出:

其实 number -> String.valueOf(number) 可以写成 String::valueOf 。Java 8的lambda支持这种eta-转换。

看着这个所谓的eta-转换很眼熟,scala中也支持,但之前从没弄清楚过它还有这么个“学术名词”。搜索了一下:eta是希腊字母 η, 发音“A塔”,也叫 η-conversion

scala规范的文档中没有找到eta-conversion的介绍,倒是找到eta-expansion
看了一下这里对eta-expansion的介绍

eta-expansion 与 eta-conversion 不是一回事;eta扩展指是把一个普通方法转换为函数对象的过程:
比如下面三种情况

1) val f = foo _ 
2) val f: (Int, Int) => Int = foo 
3) val f = foo(_, _) 

但是 : foo(_, 1) 不是合格的Eta扩展,it just expands to (a) => foo(a, 1)

回到eta-conversion,在haskell的文档中定义的很清楚:http://www.haskell.org/haskellwiki/Eta_conversion

《programming in scala》提到过这种形式,却没有提这个学术名词,而是很含混的一笔带过,让人以为是编译器“耍聪明”,而不了解这背后的理论依据(不过好像scala的作者也似乎在PIS一书中刻意避免去提一些抽象的名词)。

用scala看一下eta-conversion

//典型的,foreach的例子
scala> Array(1,2,3).foreach(println)
1
2
3

//自定义一个
scala> def foo(a:Any)(f:Any=>Unit) =  f(a)
foo: (a: Any)(f: Any => Unit)Unit

//完整方式,第二个参数是一段完整的lambda表达式 
scala> foo(2){(a:Any)=>println(a)}
2
//eta-转换
scala> foo(3){println}
3
//通常的缩写方式 
scala> foo(4){println _}
4

scala里 a => foo(a) 经过eta-conversion后,直接就是右边的函数名foo
而看起来java8里eta-conversion后的这个函数名还要做些”明确的修饰”, String.foo必须写为String::foo