Scala与Haskell的严谨优雅性比较

函数语言主要优点是秉承数学的严谨性与可推导性,该文比较了纯函数语言Haskell在代数方程上与Scala语言的不同性,突出了Haskell纯函数语言的特点。

Haskell for all: Algebraic side effects

初中或小学数学中我们都学过方程式:
f * (xs + ys) = (f * xs) + (f * ys)
左边的方程等同于右边的方程,而在函数语言中也是秉承这种交换性的,假设我们做个符号替换:
1. 使用 Haskell的map函数替换数学的乘号。
2. 使用Haskell的 ++ 操作符替换数学的加号。

这样上面的方程式就变成了Haskell的等式:
map f (xs ++ ys) = (map f xs) ++ (map f ys)

也就是说,将集合xs和ys串联起来后,然后基于串联后的集合使用名为f的map函数,其结果等同于:对xs和ys每个集合单独使用名为f的map函数,然后串联这两个结果。

使用Haskell REPL运行效果如下:


>>> map (+ 1) ([2, 3] ++ [4, 5])
[3,4,5,6]
>>> (map (+ 1) [2, 3]) ++ (map (+ 1) [4, 5])
[3,4,5,6]

上述代数效果并不是在每个号称函数语言中都会有,其他语言使用这样的方程式会产生副作用,无副作用是函数语言引以为傲的主要特点,因为其他语言不会像 Haskell那样等式两边产生相同的顺序效果,如[3,4,5,6],有的可能是[3,5,4,6],这样就没有代数方程的严谨性了。

让我们使用混合式语言Scala实现看看:


>>> def xs() = { print("!"); Seq(1, 2) }
>>> def ys() = { print(
"?"); Seq(3, 4) }
>>> def f(x : Int) = { print(
"*"); x + 1 }

使用串联与map函数先后不同就会导致结果顺序不同:


>>> (xs() ++ ys()).map(f)
!?****res0: Seq[Int] = List(2, 3, 4, 5)
>>> (xs().map(f)) ++ (ys().map(f))
!**?**res1: Seq[Int] = List(2, 3, 4, 5)

第一行中,两个集合首先串联,然后针对串联结果使用map函数,结果是,首先打印出"!"和"?",然后是f函数对每个元素的结果,打印出4次"*";而在第二行,对xs集合每个元素使用f函数后,在同样对ys采取同样操作,打印的结果和前面顺序就不同了,不是"!?",而是"!*","?"符号夹在4个"*"中间了。

这表明,语句的前后顺序在Scala会导致不同的程序结果,这种语句很显然没有代数方程的特点和严谨性,这样的语句是无法可推导的,经不起推敲的,有副作用的,不是可交换性的associative。

那么Scala是不是没有办法解决呢?原文给出了Scala的复杂解决方案:


-- f * (xs + ys) = (f * xs) + (f * ys)
f =<< (xs <|> ys) = (f =<< xs) <|> (f
=<< ys)

测试结果:

>>> import Control.Applicative
>>> import Pipes
>>> let xs = do { lift (putChar '!'); return 1
<|> return 2 }
>>> let ys = do { lift (putChar '?'); return 3
<|> return 4 }
>>> let f x = do { lift (putChar '*'); return (x + 1) }
>>> runListT (f =<< (xs <|> ys)) -- Note:
`runListT` discards the result
!**?**>>> runListT ((f =<< xs) <|> (f =<< ys))
!**?**>>>

现在函数的顺序是幂等的。

对比一看,Haskell的优雅与简单一目了然,很多人怀疑Haskell在消除副作用以后会破坏数学的优雅性,很显然这不是真的。


通过上面案例我们发现Scala并不是纯的函数语言,Scala号称是面向对象和函数语言的混合,那么Scala是纯的面向对象吗?

IS SCALA PURE OBJECT-ORIENTED PROGRAMMING LANGUAGE??一文回答了这个问题。

Scala是纯的面向对象,其函数和初始类型在Scala中也都是对象。

函数是对象
内部函数只是一个带有apply方法的trait,如下是一个名为Function1的 trait接口,其有一个apply函数(方法) :

trait Function1[A,B]{
def apply(x:A):B
}

上面这个trait接口表示它将类型A作为参数,返回类型 B,所以它是一种关于类型 Int => Int = <function1>的函数。

如果按照下面编写一个匿名函数:

val f=(x:Int)=>x*x

在Scala内部,它实际是:

val f=new Function1[Int,Int]{
def apply(x:Int)=x*x
}

当我们调用这个函数时内部其实是调用trait的apply函数方法,比如我们如此调用:

f(4)

内部它调用trait的apply函数方法,实际是如下对象方式的调用:

f.apply(4)

初始类型也是对象
内部所有原始类型都是带有操作方法的类,看看下面代码:Boolean 类型

abstract class Boolean{
def ifThenElse[T](t: =>T,e: =>T):T
def &&(x: =>Boolean):Boolean=ifThenElse(x,false)
}

内部的true 或 false其实是一个对象,覆盖了原来的方法行为。

object true extends Boolean
{
def ifThenElse[T](t: =>T,e: =>T)=t
}

object false extends Boolean
{
def ifThenElse[T](t: =>T,e: =>T)=e
}

在Scala中,每个初始类型都是一个对象,操作符是函数方法,这样,当我们编写数学公式如 3+3 时,其内部面向对象翻译为:3.+(3)
这里的“3”代表一个对象,而“+”是对象类内部的函数方法而已。

Int类的定义如下:


class Int{
def +(that:Int):Int
def -(that:Int):Int
def /(that:Int:Int
def *(that:Int):Int
def
}