在F#中使用Option idiomatic吗?

时间:2015-10-03 12:09:22

标签: f# idiomatic

我有以下函数检查数据源中irb(main):005:0> require 'fileutils' => false irb(main):006:0> FileUtils::mkdir_p 'foo3' => ["foo3"] irb(main):007:0> Dir["/home/caveman/Desktop/*"] => ["/home/caveman/Desktop/foo3", "/home/caveman/Desktop/bar2", "/home/caveman/Desktop/foo2"] 的存在并返回id。这是使用customer类型的正确/惯用方法吗?

Option

6 个答案:

答案 0 :(得分:9)

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

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

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 :(得分:9)

选项形成一个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返回第一个非空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

答案 2 :(得分: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

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

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

答案 3 :(得分:5)

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

而不是:

&#34;我找的是匹配fname,lastname和emai的客户;失败了,我只找fname +电子邮件,然后只是发送电子邮件,然后创建一个客人&#34;

这样做可能会更好:

&#34;我寻找匹配的电子邮件。如果我得到多个匹配,我会寻找匹配的fname,如果再次出现倍数,我会寻找匹配的lname&#34;

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

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

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

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

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

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

  • 由于您只使用validFoo的相同参数调用Foo辅助函数,因此可以将它们烘焙到函数定义中以缩小代码。
  • 使用.toLower() / .toUpper()进行不区分大小写的字符串比较很常见,但稍微不理想,因为它实际上会为每个字符串创建新的小写副本。正确的方法是使用String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)。 99%的时间这是一个无关紧要的微观优化,但如果你有一个庞大的客户数据库并进行大量的客户查询,那么这就是它真正重要的功能!
  • 如果可能,我会修改createGuest函数,使其返回整个customer对象,并仅将.id作为最后一行函数 - 或者更好的是,还从此函数返回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返回,而Seq一般Data.Customers 1}}类对模式匹配非常友好。

例如,如果 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()) 返回一个数组,那么可读性将得到显着改善:

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);

io.on('connection', function (socket) {
    console.log('socket connected');

    socket.on('disconnect', function () {
        console.log('socket disconnected');
    });

    socket.emit('text', 'wow. such event. very real time.');
});

server.listen(3000, function() {
    console.log('Socket.io Running');
});

答案 4 :(得分:3)

谈论惯用语言,首先,F#促进编写清晰反映意图的简洁代码。从这个角度看你的代码片段时,大多数代码都过多,只隐藏了观察结果,即返回的值无论如何都不依赖于PageImplfirstname

您的代码段可能会重构为更短且更清晰的等效功能:

  • 被赋予三个参数忽略所有,但lastname
  • 然后所有客户的序列试图找到一个(忽略大小写)相同的email
  • 如果找到,则返回其email,其他返回id

几乎字面意思是

createGuest().id

答案 5 :(得分:2)

让我重新解释并修改问题陈述:

我正在寻找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