Scala與Haskell的嚴謹優雅性比較

banq發表於2015-03-30
函式語言主要優點是秉承數學的嚴謹性與可推導性,該文比較了純函式語言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])
<p class="indent">[3,4,5,6]
>>> (map (+ 1) [2, 3]) ++ (map (+ 1) [4, 5])
<p class="indent">[3,4,5,6]
<p class="indent">


上述代數效果並不是在每個號稱函式語言中都會有,其他語言使用這樣的方程式會產生副作用,無副作用是函式語言引以為傲的主要特點,因為其他語言不會像 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 }
<p class="indent">

使用串聯與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)
<p class="indent">


第一行中,兩個集合首先串聯,然後針對串聯結果使用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)
<p class="indent">

測試結果:

>>> 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))
!**?**>>>
<p class="indent">

現在函式的順序是冪等的。

對比一看,Haskell的優雅與簡單一目瞭然,很多人懷疑Haskell在消除副作用以後會破壞數學的優雅性,很顯然這不是真的。


相關文章