地道的F# - 函数式 vs. 命令式
我们的故事要从这个叫Stuart Bowers的家伙开始,下图中,他正拿着一个来自Café Press网站的杯子喝着咖啡,此杯子正是他对函数式编程做出出色贡献的象征。
Stuart是Amalga(一个微软推出的医疗系统)项目的一个开发者,也是微软公司的一位全职员工。此人不写博客——此乃世界的一大损失也…
不管怎样,他还是帮我审读过我写的书中的几个章节,并且最近我们还有一次促膝长谈,讨论并分享怎样才是一个地道的F#风格的话题。
问题
提问:在编写下面的代码时,怎样才是“正确的F#方式”:
创建一个共享的,可变得值并且生成聊个线程池用来累加一个List(F#中的链表)中的元素。(故意创建一个竞争的环境,此乃这个代码示例的关键所在。)
命令(?)式
让我们以如何使用命令式方式来回答此问题作为开始。
open System.Threading let sumArray (arr : int[]) = let total = ref 0 // Add the first half let thread1Finished = ref false ThreadPool.QueueUserWorkItem( fun _ -> for i = 0 to arr.Length / 2 - 1 do total := arr.[i] + !total thread1Finished := true ) |> ignore // Add the second half let thread2Finished = ref false ThreadPool.QueueUserWorkItem( fun _ -> for i = arr.Length / 2 to arr.Length - 1 do total := arr.[i] + !total thread2Finished := true ) |> ignore // Wait while the two threads finish their work while !thread1Finished = false || !thread2Finished = false do Thread.Sleep(0) !total
命令式的代码非常的直观且提供如下一些好处:
- 简单。只需要两个调用QueueUserWorkItem的线程和一些布尔标量来跟踪是否线程完成了,而不是整个正在运行的线程。
然而,这也有一些弊端:
- 冗余。两个传入QueueUserWorkItem的表达式本质上是一样的,因此,此代码是重复的。如果你想调整他们,例如添加日志,那么你或者会忘记在两个地方都更新代码,或者会仅仅添加代码两次。
- 数据隐藏。这两个线程最大的区别在于他们累加的数组元素的下标范围。第一个数组从0计算到len/2 – 1,第二个线程从len/2到len。然而,关键的因素被隐藏在代码中两个不同的位置。
函数式(?)方法
现在让我们来看看如何用更函数式的风格实现它。
open System.Threading let sumArray (arr : int[]) = // Define a location in shared memory for counting let total = ref 0 // Define two flags in shared memory let thread1Finished, thread2Finished = ref false, ref false // Generate a lambda to sum a section of the array. let sumElements (startIndex,endIndex) flag = fun (_:obj) -> for i = startIndex to endIndex do total := arr.[i] + !total flag := true // Divy up the array let firstHalf = 0,(arr.Length / 2 - 1) let secondHalf = (snd firstHalf)+1,(arr.Length - 1) // Generate Delegates to sum portions of the array let thread1Delegate = sumElements firstHalf thread1Finished let thread2Delegate = sumElements secondHalf thread2Finished [ thread1Delegate; thread2Delegate ] // Queue up the delegates |> List.map ThreadPool.QueueUserWorkItem // Queue the use work items |> ignore // Ignore the results // Wait while the two threads finish their work while !thread1Finished = false || !thread2Finished = false do Thread.Sleep(0) !total
让我们先来看看这个函数式风格的弊端:
- 太多的变量值。在这个函数里有7个变量值绑定——想必它们都是必要的。
- 概念数。这个更函数式的实现当然要求你要理解一些F#编码原则。(虽然把线程代码匹配到一个QueueUserWorkItem是相当流行的)
虽然可以说是混淆,但函数式风格提供了一些明显的好处:
- 重要的数据可以被抽出,而不是硬编码"0 to len / 2"和"len / 2"这些值,计算数组元素的总数的边界被明确地抽出放在连续的代码行上。这使得声明离一误差(OBOE详见维基百科)更容易地被找到和修复。
- 代码重用,而不是硬编码这两个线程池函数,一个需要明确边界的sumFlements函数被引入来求和。
就我而言,我部分地支持第一个例子的简单,但是第二个例子在调用范围值的价值不应该被忽视。我最喜欢F#的一个地方是它是多范式的语言,即通常情况下有一个以上的方法来解决问题,这意味着你有多个选择。
原文链接:http://blogs.msdn.com/b/chrsmith/archive/2009/04/23/idiomatic-f-functional-vs-imperative.aspx