标签归档:eta-expansion

scala类型系统:类型推导

类型推导是一门博大的学问,背后有繁冗的理论,好在非编译器开发者不用太多了解,我看到这方面的文章时立刻就知难而退了。这里蜻蜓点水的带过。

Scala采用的局部的(local)、基于流的(flow-based)类型推断;而非像ML,Haskell等语言采用的更加全局化的Hindley-Milner类型推断方式。

在《Programming in Scala》一书中提到基于流的类型推断有它的局限性,但是对于面向对象的分支类型处理比Hindley-Mlner更加优雅。

基于流的类型推导在偏应用函数场景下,不能对参数类型省略,正体现了它的局限性。下面的偏应用函数声明,在Hindley-Milner类型推导(基于全局的)可以正常的推导的,会在scala里报错:

scala> def foo(a:Int, b:String) = a+b

scala> val f = foo(200, _)
<console>:8: error: missing parameter type for expanded function ((x$1) => foo(200, x$1))
   val f = foo(200, _)
                    ^

上面第二个参数占位符缺乏类型。需要显式的声明变量f的类型

scala> val f: String=>String = foo(200, _)
f: String => String = <function1>

或者显式的声明占位符参数类型

scala> val f = foo(200, _:String)
f: String => String = <function1>

不过这里有一个细节是eta-expansion的情况下可以省略参数类型,比如:

scala> def foo(a:Int, b:String) = a+b

scala> val f = foo _
f: (Int, String) => String = <function2>

scala> val f = foo(_,_)
f: (Int, String) => String = <function2>

上面两种写法占位符都是对全部参数,foo _foo(_,_) 在编译过程会编译器会进行eta-expansioneta扩展会把这个表达式转换为与foo方法类型一致的函数对象。这是在规范里定义的。

而最开始的写法 foo(200, _)并不是eta-expansion(原因见之前的这篇)。这种情况下类型推导并不会从方法体的上下文去推断参数的类型。所以需要显式的声明参数类型。

另外,对于泛型方法的情况,类型推导也需要注意泛型参数,参考这篇

再谈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