近年来,函数式编程发展突飞猛进。探讨该主题的书籍和会议数量激增、Scala 和 Clojure 等语言在快速普及,还有 John Carmack、Bob Martin 等名人的支持,都说明了这一事实。
如今,没有哪种新发布的编程语言不支持“函数式编程”,甚至保守温和、经过企业认证的 Java 也开始有了甚至。
是的,这是一个全新的世界。
为什么转向函数式编程?
我成为一名函数式编程软件工程师已经有四年多了。我喜欢 FP,每天都能学到更多东西。
最近我接受了一份短期合同,参与一个现有 Java 应用程序的开发工作。在开发这个应用程序(在我看来它基本可以算作是“企业级 Java“)时,我重新审视了自己喜欢上函数式编程的基本原因(时间一长,你会认为它们是理所当然的)。这些原因包括:
在我的经验中,这是很常见的收益。
这些好处是众所周知的。与 5 年前相比,今天的大多数程序员都听说过函数式编程,许多人都在使用 FP 中的一些技术(至少是高阶函数),而且越来越多的人加入进来,成为了 FP 的传教士。
FP 的“宗教信仰”
在函数式编程(FP)的光谱上,人们都落在了两个极端上。在一个极端,FP 是一种能够丰富指令式编程的方式(例如,将一个轻量级的回调传递给一个函数,或将一个块传递给一个循环)。而在另一个极端,FP 是一种编写所谓“纯”代码的方式——也就是没有副作用的代码,是纯粹的、参考透明的函数。
有些人已经深深地爱上了 FP(非常可以理解!),他们简直将 FP 当作了一种信仰。因此,我把它称为 FP 的“宗教信仰”。
但只要是“宗教”就会有一个问题,那就是可能存在盲目的教条主义。
对于 FP 来说,我认为它蒙蔽了许多人的眼睛,让他们看不清一些本该显而易见的东西。
软件工程的目标
作为一名软件工程师,我的工作一般来说是生产可运行、可理解,及可维护的软件。向我付费的人们大都希望开发结果包括以下几个方面:
其实,他们的希望也是我所希望的。我喜欢没有 bug 的代码,这让我对自己的工作有一种自豪感,而且我讨厌调试。我希望我写的所有代码都容易理解,因为我可能需要在几个月或几年后再回来看这些代码(另外它有助于减少错误)。而且我非常喜欢那些组织得很好的代码,我可以很容易和安全地改变它以适应新的需求。
因此,如果软件工程的目标是正常运作的、可理解及可维护的软件,那么顺着这个逻辑提出的问题是:函数式编程能帮助我们实现它吗?
我的答案是:不一定。
流氓 FP
为了说明我的观点,我决定在函数式编程语言 Haskell 中实现快速排序。按照其主页上的描述,Haskell 是一种高级的、纯粹的函数式编程语言,目前也是我最喜欢的编程语言之一。
你几乎不可能在其他语言中得到比 Haskell 更多的“FP”基因了。所有用 Haskell 编写的程序都是纯函数式的(虽然有一些方法可以作弊,但我们在这里可以忽略不计)。
说到这里,请打起精神,看看我对快排的实现。
module Main (main) where
import Control.Applicative
import>import>import>type Array a = IOArray Int a
whileM :: IO Bool -> IO () -> IO ()
whileM pred effect = do
whileM pred effect
else return ()
quick_sort :: Ord a => Array a -> IO (Array a)
quick_sort a = do
(m, n) <- getBounds a
let loop' = loop
loop' a m (n + 1)
loop ary m n = if (n < 2) then return ary else do
let readVal idx = readArray ary (idx + m)
let writeVal idx = writeArray ary (idx + m)
let readValRef ref = readIORef ref >>= readVal
let writeValRef ref v = readIORef ref >>= writeVal <*> pure v
pivotVal <- readVal $ n `div` 2
leftIdxRef<- newIORef 0
rightIdxRef <- newIORef $ n - 1
let incLeft= modifyIORef leftIdxRef (+1)
let decRight = modifyIORef rightIdxRef (subtract 1)
let readLeftIdx = readIORef leftIdxRef
let readRightIdx = readIORef rightIdxRef
whileM ((<=) <$> readLeftIdx <*> readRightIdx) $ do
leftVal<- readValRef leftIdxRef
rightVal <- readValRef rightIdxRef
if (leftVal < pivotVal) then incLeft
else if (rightVal > pivotVal) then decRight
writeValRef leftIdxRef rightVal
writeValRef rightIdxRef leftVal
leftIdx<- readLeftIdx
rightIdx <- readRightIdx
loop a m(rightIdx + 1)
loop a leftIdx (n - leftIdx)
main = newListArray (0, 7) [9, 2, 3, 45, 2, 9, 2, 1] >>= quick_sort >>= getElems >>= putStrLn.show
复制代码
尽管这个程序是“纯函数式的“,但它的代码是完全、彻底的垃圾:
上述就是一个纯粹的函数式程序,它与软件工程的目标完全无关。这是一个不那么典型的示范,但还有许多更能说明问题的现实范例,函数式程序员会很认同它们的。
这是 FP 的流氓行为,也证明了代码是“纯函数式“并不意味着就一定有什么价值。
可爱的 FP
现在我想给大家看一下 Haskell 中比较有名的快排例子。这并不完全是经典的快速排序,因为它并不是原地排序,但也足够接近了。
quicksort :: Ord a => [a] -> [a]
quicksort []= []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
lesser= filter (< p) xs
greater = filter (>= p) xs
复制代码
这才是优雅的实现!这也是为什么人们会这么喜欢 FP 的原因。
从定义上来说,这段代码的确是正确的。如果你了解 Haskell 的语法,它就很容易理解,而且没有什么排序代码比它更容易维护的了(好吧,filter 确实应该被 partition 取代,因为 filter 会破坏信息;使用 filter 需要手动否定布尔谓词< p,这代表了重复的信息内容)。
我们现在有两个纯粹的函数式程序,都是用同样的语言编写的,但两者之间却有天壤之别。
这是什么原因呢?
FP 不是目标
我的观点是,尽管 FP 让我们更容易编写好的代码,但仅仅因为某些东西是函数式的,甚至是“纯函数式的”,并不一定意味着它就有多好。
换句话说,作为试图改进自己技术的软件工程师,我们不应该仅仅因为某个东西是“函数式的”或“纯函数式的”就崇拜它或为它辩护。虽然使用函数式编程的技术有可能写出好代码,但也有可能写出坏代码。
FP 并不能保护我们。我们需要另一种标准来衡量“好代码“,而不是简单地认为“函数式“就是好代码。
我认为这个标准与可组合性、可理解性和正确性有很大关系。
毕竟,我们是被美化的、会说话的猿猴,填充我们头骨的脂肪从来就不是为了写软件而设计的。
认识到这一事实后,我们就可以通过好代码的定义来尝试提升自己编写和维护正常软件的能力(事实上,我们在这方面的能力是相当有限的)。
FP 不是答案
在给好代码下定义时,我没有提到任何与函数式编程、静态类型或其他很多东西相关的内容,因为这些“只是”达到目的的手段。有时这些手段可以帮助我们创建、理解和编排正确的代码。
但就其本身而言,它们并不是我们工作的目标。
每一种技术都必须根据其自身的特点来衡量优劣,而与它是否是“函数式“无关。
原文链接: