本文章是由機器翻譯。

程式設計師雜談

C# 樂趣多

Ted Neward

學習一門新的語言可以説明開發人員帶來新的見解和新的方法在其他語言,如 C# 中編寫代碼。在這裡我個人的偏好是 F # 中,因為這些天我 F # MVP。雖然簡要地談到一個較早的列中的函數程式設計 (bit.ly/1lPaLNr),我想看看新的語言。

當這樣做,很可能該代碼最終將需要被 C# 編寫的 (我會在下一篇文章中做的)。但它仍然可以有所説明編寫它在 F # 中的代碼,原因有三:

  1. F # 有時可以解決的問題,像這更容易比 C#。
  2. 思考一個問題在不同的語言中通常可以説明澄清之前重寫它在 C# 中的解決方案。
  3. F # 是一種類似其表弟 C# 的.NET 語言。所以你可以想像可以解決它在 F # 中,然後把它編譯成一個.NET 程式集和簡單地打電話到它從 C#。(根據的演算法的複雜性,它實際上可以更理智解決。)

看看這個問題

考慮一個簡單的問題,對於這種類型的解決方案。想像一下你正在迅速,用於管理個人財務應用程式。作為應用程式的一部分,您需要"調和"交易你已經找到線上使用者已進入應用程式的交易記錄。這裡的目標是通過兩個清單的大多是完全相同的資料,工作和匹配相同的元素。你怎麼處理那些無與倫比的元素不是但未指定,但是您需要捕獲它們。

幾年前,我做一些承包的"直觀"的公司的使了當時最流行的 PC 應用程式來管理你的銀行。這是實際的問題,我曾在那裡工作。它是專門為後下載使用者的交易,由銀行被稱為支票帳戶註冊視圖。我不得不調和與那些使用者已經進入到應用程式,這些線上交易,然後問使用者關於不匹配任何交易。

每個事務包含金額、 交易日期和描述性的"注釋"。問題就在這裡:日期並不總是相匹配,也不做評論。

這意味著我可以比較的只有真實可信資料的交易金額。幸運的是,在兩個交易記錄將會到一分錢絕對相同的給定月份內是相當罕見的。所以這是一個"足夠好"的解決方案。我會回去和證實他們事實上是一個合法的匹配。只是把事情複雜化,這兩個傳入清單不需要匹配的長度。

F #ing 解決方案

有關于占主導地位的函數式語言的原則如何你"認為功能上。在這種情況下,第一是他們喜歡遞迴而不是反覆運算。換句話說,雖然受過正統訓練的開發人員將立即想要站起來的幾個嵌套的 for 迴圈,功能性的程式師將會想要遞迴。

在這裡,我要交易的本地清單和遠端交易記錄的清單。我會去通過每個清單的第一個元素。如果它們匹配,我會剝掉他們各自的清單這兩個,一起搗碎成結果清單中,並以遞迴方式重新調用本地和遠端清單的其餘部分。看看我正在用的類型定義:

type Transaction =  
  {
    amount : float32;
    date : System.DateTime;
    comment : string
  }
type Register =
  | RegEntry of Transaction * Transaction

簡單來說,我定義兩種類型。 其中一個是真的是沒有一些傳統的物件符號物件的記錄類型。 其他是受歧視的聯盟類型,這實在是一個物件/類圖中的偽裝。 我不會陷入 F # 語法在這裡的深處。 有很多其他資源在那裡的包括我的書,"專業 F # 2.0" (Wrox,2010年)。

一言以蔽之,這些都是輸入的類型和輸出類型,分別。 我之所以選擇可區分的聯合的結果很快就會明瞭。 鑒於這些兩個類型定義,它是很容易定義此函數看起來像想要什麼的外骨骼:

let reconcile (local : Transaction list) (remote : Transaction list) : Register list =
  []

請記住,在 F # 的說法,類型描述元來在名稱後。 所以這聲明的函數,則兩個交易記錄列出並返回註冊項的清單。 正如寫,它引出要返回一個空的清單 ("[]")。 這是好的因為我現在可以存根出少數幾個函數來測試 — — 測試驅動開發 (TDD) 的風格 — — 平原香草正常 F # 主控台應用程式中。

我可以和應該寫這些單元測試框架在現在,但我可以完成內主要使用 System.Diagnostics.Debug.Assert 和本地嵌套的函數本質上是同樣的事情。 其他人可能更願意與 F # REPL,在Visual Studio或者在命令列中,工作中所示圖 1

圖 1 創建主控台演算法與 F # REPL

[<EntryPoint>]
let main argv =
  let test1 =
    let local = [ { amount = 20.00f;
                    date = System.DateTime.Now;
                    comment = "ATM Withdrawal" } ]
    let remote = [ { amount = 20.00f;
                     date = System.DateTime.Now;
                     comment = "ATM Withdrawal" } ]
    let register = reconcile local remote
    Debug.Assert(register.Length = 1, 
      "Matches should have come back with one item")
  let test2 =
    let local = [ { amount = 20.00f;
                    date = System.DateTime.Now;
                    comment = "ATM Withdrawal" };
                  { amount = 40.00f;
                    date = System.DateTime.Now;
                    comment = "ATM Withdrawal" } ]
    let remote = [ { amount = 20.00f;
                     date = System.DateTime.Now;
                     comment = "ATM Withdrawal" } ]
    let register = reconcile local remote
    Debug.Assert(register.Length = 1, 
      "Register should have come back with one item")
  0 // Return an integer exit code

考慮到的地方有一個基本的測試腳手架我會攻擊的遞迴解決方案,正如你可以看到在圖 2

圖 2 使用 F # 模式匹配的遞推求解

let reconcile (local : Transaction list) 
  (remote : Transaction list) : Register list =
  let rec reconcileInternal outputSoFar local remote =
    match (local, remote) with
    | [], _ -> outputSoFar
    | _, [] -> outputSoFar
    | loc :: locTail, rem :: remTail ->
      match (loc.amount, rem.amount) with
      | (locAmt, remAmt) when locAmt = remAmt ->
         reconcileInternal (RegEntry(loc, rem) :: outputSoFar) locTail remTail
      | (locAmt, remAmt) when locAmt < remAmt ->
         reconcileInternal outputSoFar locTail remTail
      | (locAmt, remAmt) when remAmt > locAmt ->
         reconcileInternal outputSoFar locTail remTail
      | (_, _) ->
         failwith("How is this possible?")
  reconcileInternal [] local remote

你會注意到,這使得相當沉重的 F # 模式匹配使用。 這是在概念上類似于 C# 開關塊 (在同樣的一隻小貓是在概念上類似于劍齒虎)。 首先,定義一個本地遞迴 (rec) 函數基本上就是在外部函數相同的簽名。 還有一個附加參數來執行相匹配的結果為止。

內,第一次匹配塊檢查本地和遠端兩個清單。 第一個匹配子句 ([]、 _) 說,是否本地清單是空的我不在乎什麼是遠端清單 (底線是一個萬用字元) 因為做完了。 所以只是返回到目前為止取得的成果。 第二個匹配子句 (_、 []) 也是一樣的。

整件事的肉來在最後一場比賽子句中。 這提取本地清單的頭和將其綁定到的值 loc、 將其餘的清單放入 locTail、 做了同樣的遠端進入 rem 和 remTail,然後再匹配。 這一次,我從每個從清單中,剝去了這兩個專案中提取金額欄位,將它們綁定到的本地變數 locAmt 和 remAmt。

對於每個匹配的條款,我會以遞迴方式調用調和­內部。 關鍵的區別是用來做什麼的 outputSoFar 清單之前我遞迴。 如果 locAmt 和 remAmt 是一樣的它是一場比賽,所以在遞迴之前到 outputSoFar 清單中追加新的 RegEntry。 在任何其它情況下,我只是不理他們,遞迴。 結果將是 RegEntry 項的清單,這就是什麼返回到調用方。

擴展的想法

假設我只是不能忽視那些不匹配的專案。 我需要一個專案投入說,它是無與倫比的本地事務或無與倫比的遠端交易結果清單。 核心演算法仍然抱,只是加入登記冊的新專案歧視聯盟在每一個這些可能性,保持和遞迴之前, 將它們附加到清單中,如中所示圖 3

圖 3 將新專案添加到該登記冊

type Register =
  | RegEntry of Transaction * Transaction
  | MissingRemote of Transaction
  | MissingLocal of Transaction
let reconcile (local : Transaction list)
 (remote : Transaction list) : Register list =
  let rec reconcileInternal outputSoFar local remote =
    match (local, remote) with
    | [], _
    | _, [] -> outputSoFar
    | loc :: locTail, rem :: remTail ->
      match (loc.amount, rem.amount) with
      | (locAmt, remAmt) when locAmt = remAmt ->
         reconcileInternal (RegEntry(loc, rem) :: outputSoFar) locTail remTail
      | (locAmt, remAmt) when locAmt < remAmt ->
         reconcileInternal (MissingRemote(loc) :: outputSoFar) locTail remote
      | (locAmt, remAmt) when locAmt > remAmt ->
         reconcileInternal (MissingLocal(rem) :: outputSoFar) local remTail
      | _ ->
         failwith "How is this possible?"
  reconcileInternal [] local remote

現在的結果,將一個完整的清單,與 MissingLocal 或 MissingRemote 專案的每一筆交易,沒有相應的一對。 其實,這並不是十分的真實。 如果這兩個清單不匹配的長度,像我的 test2 情況較早前,剩下的專案不會給出"丟失"的條目。

以 F # 為的"概念化"而不是 C# 語言和使用功能的程式設計原則,這成為了一個非常快速的解決方案。 F # 使用廣泛類型推斷,所以在許多情況下同時充實了代碼,沒有確定的實際參數類型和返回時間提前。 F # 中的遞迴函數通常需要類型批註來定義的返回類型。 我走了沒有它在這裡因為它可以推斷返回類型給定上的外側,封閉功能。

在某些情況下,可以只將這編譯成一個程式集,把它交給 C# 開發人員。 對於很多的商店,不過,這不會飛。 因此下, 一次,我就會將其轉換為 C#。 老闆將永遠不會知道這段代碼其實開始生活作為 F # 代碼。

編碼愉快 !

Ted Neward 是在 iTrellis,一家諮詢服務公司的首席技術官。他已寫超過 100 篇文章和撰寫或合作撰寫了十幾本書,其中包括"專業 F # 2.0" (Wrox,2010年)。他是 C# MVP,在世界各地的會議上講話。他提供諮詢和指導定期 — — 他在聯繫 ted@tedneward.comted@itrellis.com 如果你感興趣讓他來與你的團隊工作和閱讀他的博客 blogs.tedneward.com

感謝以下的微軟技術專家對本文的審閱:林肯阿特金森