地道的F# - 函数式 vs. 命令式

我们的故事要从这个叫Stuart Bowers的家伙开始,下图中,他正拿着一个来自Café Press网站的杯子喝着咖啡,此杯子正是他对函数式编程做出出色贡献的象征。

 

 IMAGE_154

StuartAmalga(一个微软推出的医疗系统)项目的一个开发者,也是微软公司的一位全职员工。此人不写博客——此乃世界的一大损失也

 

不管怎样,他还是帮我审读过我写的书中的几个章节,并且最近我们还有一次促膝长谈,讨论并分享怎样才是一个地道的F#风格的话题。

 

问题

 

提问:在编写下面的代码时,怎样才是“正确的F#方式”:

创建一个共享的,可变得值并且生成聊个线程池用来累加一个ListF#中的链表)中的元素。(故意创建一个竞争的环境,此乃这个代码示例的关键所在。)

 

命令(?)式

让我们以如何使用命令式方式来回答此问题作为开始。

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/2len。然而,关键的因素被隐藏在代码中两个不同的位置。

 

函数式(?)方法

现在让我们来看看如何用更函数式的风格实现它。

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

posted @ 2012-01-12 15:19  tryfsharp  阅读(290)  评论(0编辑  收藏  举报