【问题标题】:Is this usage of Option idiomatic in F#?这是 F# 中 Option 惯用的用法吗?
【发布时间】:2015-12-31 13:28:56
【问题描述】:

我有以下函数检查数据源中是否存在customer 并返回 id。这是使用Option 类型的正确/惯用方式吗?

let findCustomerId fname lname email = 
    let (==) (a:string) (b:string) = a.ToLower() = b.ToLower()
    let validFName name (cus:customer) =  name == cus.firstname
    let validLName name (cus:customer) =  name == cus.lastname
    let validEmail email (cus:customer) = email == cus.email
    let allCustomers = Data.Customers()
    let tryFind pred = allCustomers |> Seq.tryFind pred
    tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus)
    |> function 
        | Some cus -> cus.id
        | None -> tryFind (fun cus -> validFName fname cus && validEmail email cus)
                  |> function
                    | Some cus -> cus.id
                    | None -> tryFind (fun cus -> validEmail email cus)
                              |> function
                                | Some cus -> cus.id 
                                | None -> createGuest() |> fun cus -> cus.id

【问题讨论】:

    标签: f# idioms


    【解决方案1】:

    当你不断缩进时,它永远不会好,所以看看你能做些什么是值得的。

    这是解决问题的一种方法,通过引入一个小辅助函数:

    let tryFindNext pred = function
        | Some x -> Some x
        | None -> tryFind pred
    

    您可以在 findCustomerId 函数中使用它来展平后备选项:

    let findCustomerId' fname lname email = 
        let (==) (a:string) (b:string) = a.ToLower() = b.ToLower()
        let validFName name (cus:customer) =  name == cus.firstname
        let validLName name (cus:customer) =  name == cus.lastname
        let validEmail email (cus:customer) = email == cus.email
        let allCustomers = Data.Customers()
        let tryFind pred = allCustomers |> Seq.tryFind pred
        let tryFindNext pred = function
            | Some x -> Some x
            | None -> tryFind pred
        tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus)
        |> tryFindNext (fun cus -> validFName fname cus && validEmail email cus)
        |> tryFindNext (fun cus -> validEmail email cus)
        |> function | Some cus -> cus.id | None -> createGuest().id
    

    这与the approach outlined here非常相似。

    【讨论】:

    • 比我的方法不太通用,但总体上更好。 +1 也就是说,| Some x -> Some x 可能是浪费 - 如果编译器没有正确优化它,它可能会导致额外的分配。 | Some _ as x -> x 将同样清晰且可能更高效。
    • Zaid,有关其背后原理的图形图像,请查看面向铁路的编程,fsharpforfunandprofit.com/posts/recipe-part2
    【解决方案2】:

    选项形成一个单子,它们也是单形的,因为它们支持表单的两个功能

    zero: Option<T>
    combine: Option<T> -> Option<T> -> Option<T>
    

    计算表达式用于提供更好的处理 monad 的方法,并且它们还支持 monoid 操作。因此,您可以为Option 实现一个计算构建器:

    type OptionBuilder() =
        member this.Return(x) = Some(x)
        member this.ReturnFrom(o: Option<_>) = o
        member this.Bind(o, f) = 
            match o with
            | None -> None
            | Some(x) -> f x
    
        member this.Delay(f) = f()
        member this.Yield(x) = Some(x)
        member this.YieldFrom(o: Option<_>) = o
        member this.Zero() = None
        member this.Combine(x, y) = 
            match x with
            | None -> y
            | _ -> x
    
    let maybe = OptionBuilder()
    

    其中Combine 返回第一个非空Option 值。然后您可以使用它来实现您的功能:

    let existing = maybe {
        yield! tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus)
        yield! tryFind (fun cus -> validFName fname cus && validEmail email cus)
        yield! tryFind (fun cus -> validEmail email cus)
    }
    match existing with
    | Some(c) -> c.id
    | None -> (createGuest()).id
    

    【讨论】:

    • 这是一个非常好的解决方案,但对我的情况来说有点矫枉过正。
    【解决方案3】:

    在可读性方面,一点抽象可以大有帮助...

    let bindNone binder opt = if Option.isSome opt then opt else binder ()
    
    let findCustomerId fname lname email = 
        let allCustomers = Data.Customers ()
        let (==) (a:string) (b:string) = a.ToLower () = b.ToLower ()
        let validFName name  (cus:customer) = name  == cus.firstname
        let validLName name  (cus:customer) = name  == cus.lastname
        let validEmail email (cus:customer) = email == cus.email
        let tryFind pred = allCustomers |> Seq.tryFind pred
        tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus)
        |> bindNone (fun () -> tryFind (fun cus -> validFName fname cus && validEmail email cus))
        |> bindNone (fun () -> tryFind (fun cus -> validEmail email cus))
        |> bindNone (fun () -> Some (createGuest ()))
        |> Option.get
        |> fun cus -> cus.id
    

    更容易理解,唯一的开销是一些额外的null 检查。

    另外,如果我是你,因为这些功能中的大多数都非常小/微不足道,我会明智地使用inline

    【讨论】:

      【解决方案4】:

      首先,这可能与您的问题没有直接关系,但您可能希望重新整理此函数中的逻辑。

      代替:

      “我寻找与 fname、lastname 和 emai 匹配的客户;如果失败,我只寻找 fname + email,然后只寻找电子邮件,然后创建一个客人”

      这样进行可能会更好:

      “我查找匹配的电子邮件。如果我得到多个匹配项,我会查找匹配的 fname,如果再次出现多个,我会查找匹配的 lname”

      这不仅可以让您更好地构建代码,还可以迫使您处理逻辑中可能存在的问题。

      例如,如果您有多个匹配的电子邮件,但没有一个具有正确的名称,该怎么办?目前,您只需选择序列中的第一个,这可能是您想要的,也可能不是您想要的,这取决于 Data.Customers() 的排序方式,如果它是排序的。

      现在,如果电子邮件必须是唯一的,那么这将不是问题 - 但如果是这种情况,那么您不妨跳过检查名字/姓氏!

      (我不愿提及它,但它也可能会在一定程度上加快您的代码速度,因为您不必多次检查相同字段的记录,也不会在电子邮件足够时检查其他字段。)

      现在开始回答您的问题 - 问题不在于使用 Option,问题在于您实际上执行了 3 次相同的操作! (“查找匹配项,如果未找到,则查找后备”)。以递归方式重构函数将消除丑陋的对角线结构,允许您在将来轻松扩展函数以检查其他字段。

      对您的代码的其他一些小建议:

      • 由于您只使用与 Foo 相同的参数调用 validFoo 辅助函数,因此您可以将它们烘焙到函数定义中以精简代码。
      • 使用.toLower()/.toUpper() 进行不区分大小写的字符串比较是很常见的,但由于它实际上会为每个字符串创建新的小写副本,因此有些不太理想。正确的方法是使用String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)。 99% 的情况下,这是一个无关紧要的微优化,但如果您拥有庞大的客户数据库并进行大量客户查找,那么这种功能实际上可能很重要!
      • 如果可能,我会修改createGuest 函数,使其返回整个customer 对象,并且只将.id 作为该函数的最后一行——或者更好的是,返回一个@987654330 @ 也来自此函数,并提供单独的单行 findCustomerId = findCustomer &gt;&gt; (fun c -&gt; c.id) 以方便使用。

      综上所述,我们有以下内容。为了这个例子,我假设在多个同样有效的匹配的情况下,你会想要 last,或者最近的一个。但你也可以抛出异常,按日期字段排序,或其他。

      let findCustomerId fname lname email = 
          let (==) (a:string) (b:string) = String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)
          let validFName = fun (cus:customer) ->  fname == cus.firstname
          let validLName = fun (cus:customer) ->  lname == cus.lastname
          let validEmail = fun (cus:customer) ->  email == cus.email
          let allCustomers = Data.Customers ()
          let pickBetweenEquallyValid = Seq.last
          let rec check customers predicates fallback = 
              match predicates with
              | [] -> fallback
              | pred :: otherPreds -> 
                  let matchingCustomers = customers |> Seq.filter pred
                  match Seq.length matchingCustomers with
                  | 0 -> fallback
                  | 1 -> (Seq.head matchingCustomers).id
                  | _ -> check matchingCustomers otherPreds (pickBetweenEquallyValid matchingCustomers).id            
          check allCustomers [validEmail; validFName; validLName] (createGuest())
      

      最后一件事:到处都是那些丑陋的(通常是 O(n))Seq.foo 表达式是必要的,因为我不知道 Data.Customers 返回什么样的序列,而一般的 Seq 类不是很对模式匹配友好。

      例如,如果Data.Customers返回一个数组,那么可读性会显着提高:

          let pickBetweenEquallyValid results = results.[results.Length - 1]
          let rec check customers predicates fallback = 
              match predicates with
              | [] -> fallback
              | pred :: otherPreds -> 
                  let matchingCustomers = customers |> Array.filter pred
                  match matchingCustomers with
                  | [||] -> fallback
                  | [| uniqueMatch |] -> uniqueMatch.id
                  | _ -> check matchingCustomers otherPreds (pickBetweenEquallyValid matchingCustomers).id
          check allCustomers [validEmail; validFName; validLName] (createGuest())
      

      【讨论】:

      • 感谢您富有洞察力和完整的回答,我使用的逻辑确实不是最好的,因为我很快注意到我违反了 DRY 原则,但此功能仅用于原型设计;我的想法是“编写一个坚持返回 an id 的函数”,我会考虑为 production 版本提供优雅的解决方案
      【解决方案5】:

      谈到语言的惯用用法,首先是 F# 提倡编写清晰反映意图的简洁代码。从这个角度看你的 sn-p 时,大多数代码都过多,只是隐藏了返回值不依赖于 firstnamelastname 的观察。

      您的 sn-p 可能会被重构为更短、更清晰的等效函数:

      • 给出三个参数会忽略所有参数,但email
      • 然后在所有客户的序列中尝试找到具有(忽略大小写)相同email 的客户,
      • 如果找到,则返回其id,否则返回createGuest().id

      几乎可以直译为

       let findCustomerId _ _ email =
           Data.Customers()
          |> Seq.tryFind (fun c -> System.String.Compare(email,c.email,true) = 0)
          |> function Some(c) -> c.id | None -> createGuest().id  
      

      【讨论】:

        【解决方案6】:

        让我重新表述和修改问题陈述:

        我正在寻找 1) 匹配的名字、姓氏和电子邮件,在这种情况下我想终止迭代。 如果做不到这一点,我会暂时存储一个客户,其中包含 2) 匹配的名字和电子邮件,或者不太理想的是 3) 仅匹配的电子邮件,然后继续查找 1)。 序列的元素最多只能计算一次。

        这种问题不太适合流水线Seq 函数,因为它涉及升级层次结构中的状态,并在达到最高状态时终止。 所以让我们以一种命令式的方式来做,使状态可变,但使用可区分的联合对其进行编码,并使用模式匹配来实现状态转换。

        type MatchType<'a> =
        | AllFields of 'a
        | FNameEmail of 'a
        | Email of 'a
        | NoMatch
        
        let findCustomerId fname lname email =
            let allCustomers = Data.Customers ()
            let (==) a b =          // Needs tweaking to pass the Turkey Test
                 System.String.Equals(a, b, System.StringComparison.CurrentCultureIgnoreCase)
            let notAllFields = function AllFields _ -> false | _ -> true
            let state = ref NoMatch
        
            use en = allCustomers.GetEnumerator()
            while notAllFields !state && en.MoveNext() do
                let cus = en.Current
                let fn = fname == cus.firstname
                let ln = lname == cus.lastname
                let em = email == cus.email
                match !state with
                | _                 when fn && ln && em -> state := AllFields cus
                | Email _ | NoMatch when fn && em       -> state := FNameEmail cus
                | NoMatch           when em             -> state := Email cus
                | _                                     -> ()
        
            match !state with
            | AllFields cus
            | FNameEmail cus
            | Email cus -> cus.id
            | NoMatch -> createGuest().id
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-11-03
          • 1970-01-01
          • 1970-01-01
          • 2023-02-23
          • 2016-11-05
          • 1970-01-01
          • 1970-01-01
          • 2016-03-01
          相关资源
          最近更新 更多