这种 Option 在 F# 中的用法是惯用的吗?
Is this usage of Option idiomatic in F#?
我有以下函数检查数据源中是否存在 customer
和 returns id。这是使用 Option
类型的 right/idiomatic 方式吗?
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
一点抽象可以在可读性方面大有帮助...
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
检查。
此外,如果我是你,因为这些功能中的大多数都是如此 small/trivial,我会明智地在周围散布 inline
。
当你一个接一个地缩进时,它永远不会好,所以值得看看你能做些什么。
这是解决该问题的一种方法,方法是引入一个小辅助函数:
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
这与 非常相似。
谈到语言的惯用用法,F# 提倡编写清晰反映意图的简洁代码。当从这个角度看你的代码片段时,大部分代码都有过多的并且只隐藏了返回值无论如何都不依赖于 firstname
或 lastname
.
的观察结果
您的代码段可能会重构为更短、更清晰的等效函数:
- 给定三个参数将忽略所有参数,但
email
、
- 然后所有客户的顺序试图找到一个具有相同(忽略大小写)的
email
,
- 如果找到,则 returns 其
id
,否则 returns 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
选项形成一个 monad,它们也是幺半群的,因为它们支持以下形式的两个函数
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
returns 第一个非空 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
首先,这可能与您的问题没有直接关系,但您可能想重新安排此函数中的逻辑。
而不是:
"I look for a customer that matches fname, lastname, and emai; failing that, I look for just fname + email, then just email, then create a guest"
这样进行可能会更好:
"I look for a matching email. If I get multiple matches, I look for a matching fname, and if there's multiples again I look for a matching lname".
这不仅可以让您更好地构建代码,还可以迫使您处理逻辑中可能出现的问题。
例如,如果您有多个匹配的电子邮件地址,但其中 none 个名称正确怎么办?目前,您只需选择序列中的第一个,这可能是也可能不是您想要的,具体取决于 Data.Customers() 的排序方式,if 它的排序方式。
现在,如果电子邮件必须是唯一的,那么这将不是问题 - 但如果是这种情况,那么您最好跳过检查 first/last 名称!
(我不愿提及它,但它也可能会稍微加快您的代码速度,因为您不会不必要地多次检查相同字段的记录,也不会在仅电子邮件就足够时检查其他字段.)
现在开始回答您的问题 - 问题不在于 Option
的使用,问题在于您执行了 3 次本质上相同的操作! ("Find matches, then if not found look for a fallback")。以递归方式重构函数将消除丑陋的对角线结构,和允许您在未来简单地扩展函数以检查额外的字段。
对您的代码的一些其他小建议:
- 由于您只调用了
validFoo
辅助函数,并使用与 Foo
相同的参数,您可以将它们烘焙到函数定义中以简化代码。
- 使用
.toLower()
/.toUpper()
进行不区分大小写的字符串比较很常见,但略微次优,因为它实际上会为每个字符串创建新的小写副本。正确的方法是使用String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)
。 99% 的时间这是无关紧要的微优化,但如果您拥有庞大的客户数据库并进行大量客户查找,这就是真正重要的功能!
- 如果可能的话,我会修改
createGuest
函数,使其return是整个customer
对象,并且只将.id
作为最后一个此功能的行 - 或者更好的是,return 也来自此功能的 customer
,并提供单独的一行 findCustomerId = findCustomer >> (fun c -> 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
returns,而且一般的Seq
class对模式匹配不是很友好。
如果,例如,Data.Customers
return是一个数组,那么可读性会大大提高:
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())
让我改写并修改问题陈述:
我正在寻找 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
我有以下函数检查数据源中是否存在 customer
和 returns id。这是使用 Option
类型的 right/idiomatic 方式吗?
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
一点抽象可以在可读性方面大有帮助...
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
检查。
此外,如果我是你,因为这些功能中的大多数都是如此 small/trivial,我会明智地在周围散布 inline
。
当你一个接一个地缩进时,它永远不会好,所以值得看看你能做些什么。
这是解决该问题的一种方法,方法是引入一个小辅助函数:
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
这与
谈到语言的惯用用法,F# 提倡编写清晰反映意图的简洁代码。当从这个角度看你的代码片段时,大部分代码都有过多的并且只隐藏了返回值无论如何都不依赖于 firstname
或 lastname
.
您的代码段可能会重构为更短、更清晰的等效函数:
- 给定三个参数将忽略所有参数,但
email
、 - 然后所有客户的顺序试图找到一个具有相同(忽略大小写)的
email
, - 如果找到,则 returns 其
id
,否则 returnscreateGuest().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
选项形成一个 monad,它们也是幺半群的,因为它们支持以下形式的两个函数
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
returns 第一个非空 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
首先,这可能与您的问题没有直接关系,但您可能想重新安排此函数中的逻辑。
而不是:
"I look for a customer that matches fname, lastname, and emai; failing that, I look for just fname + email, then just email, then create a guest"
这样进行可能会更好:
"I look for a matching email. If I get multiple matches, I look for a matching fname, and if there's multiples again I look for a matching lname".
这不仅可以让您更好地构建代码,还可以迫使您处理逻辑中可能出现的问题。
例如,如果您有多个匹配的电子邮件地址,但其中 none 个名称正确怎么办?目前,您只需选择序列中的第一个,这可能是也可能不是您想要的,具体取决于 Data.Customers() 的排序方式,if 它的排序方式。
现在,如果电子邮件必须是唯一的,那么这将不是问题 - 但如果是这种情况,那么您最好跳过检查 first/last 名称!
(我不愿提及它,但它也可能会稍微加快您的代码速度,因为您不会不必要地多次检查相同字段的记录,也不会在仅电子邮件就足够时检查其他字段.)
现在开始回答您的问题 - 问题不在于 Option
的使用,问题在于您执行了 3 次本质上相同的操作! ("Find matches, then if not found look for a fallback")。以递归方式重构函数将消除丑陋的对角线结构,和允许您在未来简单地扩展函数以检查额外的字段。
对您的代码的一些其他小建议:
- 由于您只调用了
validFoo
辅助函数,并使用与Foo
相同的参数,您可以将它们烘焙到函数定义中以简化代码。 - 使用
.toLower()
/.toUpper()
进行不区分大小写的字符串比较很常见,但略微次优,因为它实际上会为每个字符串创建新的小写副本。正确的方法是使用String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)
。 99% 的时间这是无关紧要的微优化,但如果您拥有庞大的客户数据库并进行大量客户查找,这就是真正重要的功能! - 如果可能的话,我会修改
createGuest
函数,使其return是整个customer
对象,并且只将.id
作为最后一个此功能的行 - 或者更好的是,return 也来自此功能的customer
,并提供单独的一行findCustomerId = findCustomer >> (fun c -> 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
returns,而且一般的Seq
class对模式匹配不是很友好。
如果,例如,Data.Customers
return是一个数组,那么可读性会大大提高:
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())
让我改写并修改问题陈述:
我正在寻找 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