2015-11-19 35 views
4

我喜歡使用管道運算符'|>'很多。然而,混合與函數返回「簡單」值的函數返回「選項類型的值」,事情就變得有點凌亂,例如當:是一個「可選」管道操作員慣用的F#

// foo: int -> int*int 
// bar: int*int -> bool 
let f (x: string) = x |> int |> foo |> bar 

的作品,但它可能會拋出一個「System.FormatException:。 ..」

現在假設我想解決的是通過使功能‘詮釋’給一個可選的結果:

let intOption x = 
    match System.Int32.TryParse x with 
    | (true, x) -> Some x 
    | (false,_) -> None 

只有現在的問題是當然的功能

let g x = x |> intOption |> foo |> bar 

由於輸入錯誤而無法編譯。好吧,簡單地定義一個 'optionalized' 管:

let (|=) x f = 
    match x with 
    | Some y -> Some (f y) 
    | None -> None 

現在我可以簡單地定義:

let f x = x |> intOption |= foo |= bar 

,一切都像一個風情萬種。

好的,問題:那是否是慣用的F#?是否可以接受?不好的風格?

注:當然,只要有正確的類型「| =」操作符允許分裂,並隨意選擇合併「管道」,而只關心他們重要的選擇:

x |> ...|> divisionOption |= (fun y -> y*y) |=...|>... 
+4

我沒有看到這方面的需要操作員可以使用'|> Option.map F' - 事實上,您可以定義操作就像那樣;) - 並且最好的方法是使用'|> Option.bind f''得到單調情況;) – Carsten

+1

使用管道運算符沒有什麼特別的「習慣用法」,除了它有時有助於類型推斷。濫用它(以及任何其他自定義操作符)可能會大大降低代碼的可讀性。 – Petr

+0

噢,沒想到Option.map。所以我想這回答所有問題;有一個核心庫函數,其中我的操作符或多或少是一個特例,因此使用inbuild函數肯定會更好...... thx –

回答

8

我想使用Option.map會更慣用:

設GX = X |> intOption |> Option.map FOO |> Option.map酒吧

+0

是的,正如我對Carsten評論中所提到的,我沒有想到'Option.map',它當然可以解決問題... thx! –

2

Option.map/Option.bind是一個非常好的簡單解決方案,我認爲如果你有一個或兩個鏈接函數,這是處理事情的最佳方式。

我認爲值得補充的是,偶爾你可能會遇到相當複雜的嵌套選項行爲,在這一點上,我認爲值得定義一個MaybeBuilder。一個非常簡單的例子是:

type MaybeBuilder() = 
    member this.Bind(m, f) = 
     Option.bind f m 
    member this.Return(x) = 
     Some x 
    member this.ReturnFrom(x) = 
     x 

let maybe = MaybeBuilder() 

然後,您可以在語法使用:

maybe { 
    let! a = intOption x 
    let! b = foo a 
    let! c = bar b 
    return c 
} 
2

還有其他答案尚未涉及兩個方面。

對F#的 Option
  • 明智地使用運營商定製,而不是流水線標準功能的
    • 單子操作可以提高可讀性

    而是像MaybeBuilder()一個完全成熟的計算表達式,我們可以定義讓對象函數爲Option類型提供了monadic操作。讓我們通過運營商>>=代表綁定操作:

    let (>>=) ma f = Option.bind f ma 
    // val (>>=) : ma:'a option -> f:('a -> 'b option) -> 'b option 
    let ``return`` = Some 
    // val return : arg0:'a -> 'a option 
    

    由此得出

    let (>=>) f g a = f a >>= g 
    // val (>=>) : f:('a -> 'b option) -> g:('b -> 'c option) -> a:'a -> 'c option 
    let fmap f ma = ma >>= (``return`` << f) 
    // val fmap : f:('a -> 'b) -> ma:'a option -> 'b option 
    let join mma = mma >>= id 
    // val join : mma:'a option option -> 'a option 
    

    fmap基本上是Opion.map; join將一個嵌套實例嵌套一層,並且由Kleisli運算符>=>構成是流水線的替代。

    在輕量級語法中,運算符免於使用嵌套作用域增加縮進。當將lambda函數串聯在一起時,這可能很有用,允許嵌套,同時仍然縮進至多一個級別。

    a_option 
    |> Option.bind (fun a -> 
        f a 
        |> Option.bind (fun b -> 
         g b 
         |> Option.bind ...)) 
    

    VS

    a_option 
    >>= fun a -> 
        f a 
    >>= fun b -> 
        g b 
    >>= ... 
    
  • 1

    使用(|>)似乎是一個很突出的概念,通過計算鏈穿線價值的實現。但是,由於F#操作符的語法限制(優先級和左/右關聯性),在實際項目中使用此概念可能有點困難。即:

    • 無論何時您使用Option.mapOption.bind,都很難使用代碼塊。代碼intOption |> Option.map foo |> Option.map bar只有在foobar被命名的函數時纔會很好用;
    • 很難保持lambda小而分開;
    • 在任何情況下,代碼將出現括號(我不喜歡,因爲我的Lisp次:)

    使用的幾個小功能,在「鏈接」的方式讓寫一個更簡潔的代碼。
    備註:對於現實生活中的項目,我強烈建議與您的團隊協商,因爲新的操作員或擴展方法可能對您團隊的其他成員不利。


    幾乎是一個真實的應用程序代碼。說,你的應用程序使用一個命令行解析器,其將這個命令行:

    MyApp.exe -source foo -destination bar -loglevel debug 
    

    ...到包含鍵/值對一個Map<string, string>

    現在,讓我們只專注於處理loglevel參數,看看它是如何被代碼處理:

    1. 濾波器MapKey="loglevel";請注意,可能有零個元素;
    2. 但也可能有幾個元素,所以我們需要得到第一個;
    3. 然後我們解析出您的應用程序特定enumLogLevel類型的刺值。請注意,解析可能會失敗;例如,如果附加了調試器,我們可以任意覆蓋該值;
    4. 但是,此時仍有可能存在None值。我們放置一些默認值;
    5. 現在我們確信價值是Some,所以請致電Option.get

    這是代碼。評論表明從上面的列表步驟:

    let logLevel = 
        "loglevel" 
        |> args.TryFind       // (1) 
        |> Option.bind  ^<| Seq.tryPick Some // (2) 
        |> Option.bind  ^<| fun strLogLevel -> // (3) 
         match System.Enum.TryParse(strLogLevel, true) with 
         | true, v -> Some v 
         | _ -> None 
        |> Option.Or  ^<| fun _ ->   // (4) 
         if System.Diagnostics.Debugger.IsAttached then Some LogLevel.Debug else None 
        |> Option.OrDefault ^<| fun _ ->   // (5) 
         LogLevel.Verbose 
        |> Option.get        // (6) 
    

    在這裏,我們看到一個鍵("loglevel")通過的「optionalized」計算的鏈被順序變換。每個lambda都會爲要轉換的值引入自己的別名(例如,strLogLevel)。


    而這裏的圖書館使用:

    // A high precedence, right associative backward pipe, more info here: 
    // http://tryfs.net/snippets/snippet-4o 
    let inline (^<|) f a = f a 
    
    /// <summary>Advanced operations on options.</summary> 
    type Microsoft.FSharp.Core.Option<'T> with 
        // Attempts to return Some either from the original value or by calling lambda. 
        // Lambda is required to return a monadic value (Option<'T>) 
        static member Or f (x:Option<'T>) = 
         match x with 
         | None  -> f() 
         | x   -> x 
    
        // Same as above, but for lambdas returning plain types (e.g., `T) 
        static member OrDefault f (x:Option<'T>) = 
         match x with 
         | None  -> f() |> Some 
         | x   -> x