F#常量模式匹配和检查消除

5

我是F#世界里的新手,目前正在尝试使用函数进行实验。我试图寻找新技术,使它们更加紧凑和灵活,并从类似于C#的编码风格中移动。

所以我有一段相当丑陋的代码,需要不断检查和模式匹配:

type StringChecking =
    | Success of string
    | Fail

    let StringProcessor(str : string) =
        let newstr = modifyFn str
        let result = checkFn newstr
        match result with
        | true -> Success result
        | false ->
            let newstr' = modifyFn' str
            let result' = checkFn newstr'
            match result' with
            | true -> Success newstr'
            | false -> 
                let newstr'' = modifyFn'' str
                let result'' = checkFn newstr''
                match result'' with
                | true -> Success newstr''
                | false -> 
                    Fail

有没有一些有用的技巧可以帮助我消除所有这些步骤,使我的代码更加优雅。如果问题看起来很奇怪或愚蠢,请原谅,但我真的想做些事情,因为我想在我的某个项目中使用它。我已经对C#中的不断检查感到非常厌倦了。谢谢你的时间和建议。

10
这类问题的好读物是“面向铁路编程”http://fsharpforfunandprofit.com/posts/recipe-part1/和http://fsharpforfunandprofit.com/posts/recipe-part2/。 - Functional_S
1
这个问题似乎与这个问题非常相似:https://dev59.com/Y4_ea4cB1Zd3GeqPSLu5 - Mark Seemann
@Functional_S 谢谢你提供这个链接。那是一篇非常好的文章。我的 F# 之旅就是从那个网站开始的。这篇文章和下面关于单子方法的建议可能就是我一直在寻找的东西。 - Zinodust
2个回答

5

成功的路径

enter image description here

假设目标是在一系列步骤中重复转换对象,只有在前一个步骤成功完成后才会执行下一个步骤。这将由 F# 的 Option 类型封装在此处。

如果你想要简洁性,你可以使用某种单子 绑定

let (>>=) arg f =   // bind operator
    match arg with
    | Some x -> f x
    | None -> None

let checkWith checker x =
    if checker x then Some x else None
"sample String"
|> (modifyFn >> checkWith checkFn)
>>= (modifyFn' >> checkWith checkFn')
>>= (modifyFn'' >> checkWith checkFn'')

但是你会注意到流水线的尴尬第一步,这是由于计算开始时未解包的值导致的。 因此:

Success "sample String"
>>= (modifyFn >> checkWith checkFn)
>>= (modifyFn' >> checkWith checkFn')
>>= (modifyFn'' >> checkWith checkFn'')

如果您想修改和验证相同的原始值,而不是上一步骤的结果,请简单地忽略它。
let str ="sample String"
modifyFn str |> checkWith checkFn
>>= (fun _ -> modifyFn' str |> checkWith checkFn')
>>= (fun _ -> modifyFn'' str |> checkWith checkFn'')

备选路径

在此输入图片描述

如果计算以成功完成一步而终止,否则继续进行备选步骤,我们将拥有不同的标识。类似于F#的defaultArg函数(arg:'T option -> defaultvalue:'T > 'T),我提议一个类似于bind的defaultBy

let defaultBy c a f =
    match a with
    | None -> f c
    | x -> x
let (>?=) f = defaultBy "sample string" f

"sample string"
|> (modifyFn >> checkWith checkFn)
>?= (modifyFn' >> checkWith checkFn')
>?= (modifyFn'' >> checkWith checkFn'')

非常感谢您的回答。是的,第三种情况更适合我。我已经了解了单子并想尝试它们。所以我是否正确地假设,在第三个例子中,如果在验证的任何步骤中出现失败结果,它将继续修改和验证原始字符串,直到成功结果/直到整个表达式结束?因为我目前正在调试以了解这个结构是如何工作的。 - Zinodust
在这种情况下,似乎如果表达式的第一部分返回失败,则以下所有内容都不会被评估,整个复合表达式将返回失败。因此,在我的情况下,需要修改StringChecking类型。 - Zinodust

4

单子非常适合这种情况!

如果你使用 F# 的 option 而不是定义自己的类型,那么已经有一个函数可以满足这个需求了,它就是 Option.bind。以下是实现代码:

module Option =
    let bind m f =
        match m with
        | Some(x) -> f x
        | None -> None

您可以这样使用它:
let stringProcessor str =
    str
    |> (modifyFn >> checkFn)
    |> Option.bind (modifyFn' >> checkFn)
    |> Option.bind (modifyFn'' >> checkFn'')

这很不错,但信不信由你,有一种方法可以使用计算表达式(也称为Haskell中的do-notation)实际上消除剩余的重复性。首先,让我们快速查看最终代码:

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

let option = new OptionBuilder()

let stringProcessor x =
    option {
        let! x' = x |> modifyFn |> checkFn
        let! x'' = x' |> modifyFn' |> checkFn'
        return! x'' |> modifyFn'' |> checkFn'' }

为了理解这个问题,我们可以从不同的角度来看待它。在函数式编程语言中,命名绑定实际上只是函数应用的伪装。你不相信吗?好吧,看看这段代码:
let x = f a b
doSomething x
doSomethingElse (x + 1)

可以将其视为以下语法糖:

(fun x ->
    doSomething x
    doSomethingElse (x + 1))
(f a b)

还有这个:

let a = 1
let b = 2
a * b

可以与此互换:

(fun a ->
    (fun b ->
        a * b)
     2)
 1

或者更一般地说,let var = value in expr(其中expr是在简单的let绑定之后出现的所有代码)可以与(fun var -> expr) value互换使用。
这就是计算表达式中let!Bind的关系。我们可以采用刚才看到的let绑定规则,并研究它如何与计算表达式配合使用。这种形式:
comp { let! var = value in expr }

等同于

comp.Bind (value, (fun var -> comp { expr }))

我们将相同的规则应用于expr。此外,还定义了许多其他形式,例如return,您可以在此处找到它们。现在,作为示例,让我们尝试对此进行解析:

comp {
    let! x' = f x
    let! x'' = f (x' + 1)
    return (g x) }

作为第一步,我们需要去掉第一个let!并将其展开。这样就得到了以下内容:

comp.Bind (f x, (fun x' ->
    comp {
        let! x'' = f (x' + 1)
        return (g x) }

再次执行此操作会得到:

comp.Bind (f x, fun x' ->
    comp.Bind (f x', (fun x'' ->
        comp { return (g x'') }))

最终将变成:

comp.Bind (f x, fun x' ->
    comp.Bind (f x', (fun x'' ->
        comp.Return (g x''))

非常感谢你的答复。我也会尝试这个方法。计算表达式很有趣,但对我来说有些棘手。但我会继续努力! - Zinodust
我不知道为什么,但我得到了“此表达式期望具有类型'a option,但这里具有类型'b *'c”的编译器错误。据我所知,在这种情况下,我的checkFn函数必须返回选项,但是仍然... - Zinodust
@ZInodust 啊,这是我的错,我没有检查我写的内容。这是在构建器的Bind方法定义中犯了一个错误,但现在已经修复了。 - Jwosty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接