在F#中使用Option idiomatic吗?

Zai*_*jaj 9 f# idiomatic

我有以下函数检查customer数据源中a 的存在并返回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
Run Code Online (Sandbox Code Playgroud)

Mar*_*ann 9

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

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

let tryFindNext pred = function
    | Some x -> Some x
    | None -> tryFind pred
Run Code Online (Sandbox Code Playgroud)

您可以在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
Run Code Online (Sandbox Code Playgroud)

这与此处概述的方法非常相似.

  • 比我的方法更不通用,但总体上更好.+1那说,`| 一些x - >某些x`可能是浪费的 - 如果编译器没有正确地优化它,它可能会导致额外的分配.`| 一些_作为x - > x`将是清晰的并且可能更有效. (2认同)

Lee*_*Lee 9

选项形成一个monad,它们也是monoidal,因为它们支持表单的两个函数

zero: Option<T>
combine: Option<T> -> Option<T> -> Option<T>
Run Code Online (Sandbox Code Playgroud)

计算表达式用于提供一种更好的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()
Run Code Online (Sandbox Code Playgroud)

其中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
Run Code Online (Sandbox Code Playgroud)


ild*_*arn 5

在可读性方面,一点点抽象可以走很长的路......

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
Run Code Online (Sandbox Code Playgroud)

更容易遵循,唯一的开销是一些额外的null检查.

另外,如果我是你,因为大多数这些功能都是如此的小/琐碎,我会inline明智地撒上.


pia*_*ste 5

首先,这可能与您的问题没有直接关系,但您可能想要重置此功能中的逻辑.

代替:

"我寻找一个匹配fname,lastname和emai的客户;如果没有,我只找fname +电子邮件,然后只是发送电子邮件,然后创建一个客人"

这样做可能会更好:

"我寻找匹配的电子邮件.如果我得到多个匹配,我会寻找匹配的fname,如果再次出现倍数,我会寻找匹配的lname".

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

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

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

(我不敢提及它,但它也可能会加速你的代码,因为你不必为同一个字段不必要地多次检查记录,当你只需要电子邮件就足够检查其他字段.)

现在回答你的问题 - 问题不在于使用Option,问题是你执行了三次基本相同的操作!("找到匹配,然后如果找不到则寻找后备").以递归方式重构函数将消除丑陋的对角线结构,允许您在将来平凡地扩展函数以检查其他字段.

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

  • 由于您只调用validFoo具有相同参数的辅助函数,因此Foo可以将它们烘焙到函数定义中以缩小代码.
  • 使用.toLower()/ .toUpper()进行不区分大小写的字符串比较很常见,但稍微不理想,因为它实际上会为每个字符串创建新的小写副本.正确的方法是使用String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase).99%的时间这是一个无关紧要的微观优化,但如果你拥有庞大的客户数据库并进行大量的客户查询,那么这就是它真正重要的功能!
  • 如果有可能,我会修改createGuest功能,使其返回整个customer对象,只有把.id这个函数的最后一行-或者更好的是,返回customer从该函数的欢迎,并提供独立的单行findCustomerId = findCustomer >> (fun c -> c.id)的便于使用.

有了这些,我们有以下几点.为了示例,我将假设在多个同等有效匹配的情况下,您将需要最后一个或最新的匹配.但是你也可以抛出一个异常,按日期字段排序,或者其他什么.

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())
Run Code Online (Sandbox Code Playgroud)

最后一件事:那些丑陋的(通常是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())
Run Code Online (Sandbox Code Playgroud)