F#“有状态”的计算表达式

3

我目前正在学习F#,遇到了一些难题;我认为其中很大一部分是学会函数式思维。

目前我正在学习计算表达式,我想定义一个处理跟踪状态的计算表达式,例如:

let myOptions = optionListBuilder {
    let! opt1 = {name="a";value=10}
    let! opt2 = {name="b";value=12}
}

我希望能够使myOptions成为一个Option<'T> list,这样每个let!绑定操作都会使生成器“跟踪”定义的选项。
我不想使用可变状态来实现 - 例如,通过由生成器维护并在每个bind调用中更新的列表。
有没有一种方法可以实现这一点?
更新:结果的 Option<'T> list 类型只是代表性的,在实际情况中,我可能会有一个OptionGroup<'T>类型来包含一个列表以及一些额外的信息 - 所以正如Daniel在下面提到的,我可以使用列表推导式来得到一个简单的列表。

两件事情:我建议不要创建一个名为Option的新类型,因为语言中已经存在一个。也许可以使用OptionWithItem或类似的名称。其次,如果你有答案,应该将它与其他答案一起发布。你仍然可以标记帮助你找到答案的那个答案为“the”答案,但这样可以清楚地区分问题和答案。 - N_A
3个回答

5

我在这里编写了一个字符串构建计算表达式 (链接)

open System.Text

type StringBuilderUnion =
| Builder of StringBuilder
| StringItem of string

let build sb =
    sb.ToString()

type StringBuilderCE () =
    member __.Yield (txt : string) = StringItem(txt)
    member __.Yield (c : char) = StringItem(c.ToString())
    member __.Combine(f,g) = Builder(match f,g with
                                     | Builder(F),   Builder(G)   ->F.Append(G.ToString())
                                     | Builder(F),   StringItem(G)->F.Append(G)
                                     | StringItem(F),Builder(G)   ->G.Append(F)
                                     | StringItem(F),StringItem(G)->StringBuilder(F).Append(G))
    member __.Delay f = f()
    member __.Zero () = StringItem("")
    member __.For (xs : 'a seq, f : 'a -> StringBuilderUnion) =
                    let sb = StringBuilder()
                    for item in xs do
                        match f item with
                        | StringItem(s)-> sb.Append(s)|>ignore
                        | Builder(b)-> sb.Append(b.ToString())|>ignore
                    Builder(sb)

let builder1 = new StringBuilderCE ()

注意到底层类型是不可变的(包含的 StringBuilder 是可变的,但不必如此)。每个yield不是更新现有数据,而是将当前状态和传入输入组合在一起,得到一个新的 StringBuilderUnion 实例。您可以使用F#列表来完成此操作,因为向列表头添加元素只是构造新值而不是突变现有值。
使用StringBuilderCE的方式如下:
//Create a function which builds a string from an list of bytes
let bytes2hex (bytes : byte []) =
    string {
        for byte in bytes -> sprintf "%02x" byte
    } |> build

//builds a string from four strings
string {
        yield "one"
        yield "two"
        yield "three"
        yield "four"
    } |> build

注意使用yield而不是let!因为我实际上不想在计算表达式中使用该值。


标记为答案,因为它让我想到了一个使用几乎相同系统的CE构建器。 - Clint

3

解决方案

使用mydogisbox提供的基准StringBuilder CE构建器,我成功地制作出了以下可行的解决方案:

type Option<'T> = {Name:string;Item:'T}

type OptionBuilderUnion<'T> =
    | OptionItems of Option<'T> list
    | OptionItem of Option<'T>

type OptionBuilder () =
    member this.Yield (opt: Option<'t>) = OptionItem(opt)
    member this.Yield (tup: string * 't) = OptionItem({Name=fst tup;Item=snd tup})
    member this.Combine (f,g) = 
        OptionItems(
            match f,g with
            | OptionItem(F), OptionItem(G) -> [F;G]
            | OptionItems(F), OptionItem(G) -> G :: F
            | OptionItem(F), OptionItems(G) -> F :: G
            | OptionItems(F), OptionItems(G) -> F @ G
        )
    member this.Delay f = f()
    member this.Run (f) = match f with |OptionItems items -> items |OptionItem item -> [item]

let options = OptionBuilder()

let opts = options {
        yield ("a",12)
        yield ("b",10)
        yield {Name = "k"; Item = 20}
    }

opts |> Dump

Combine中,结果列表的顺序重要吗?如果不重要,那么应该尽可能避免使用@,因为它是一项非常昂贵的操作。在三种情况下,您可以简单地将OptionItem与列表连接起来,如此:F::G(假设F是一个项目,G是一个列表)。 - N_A
@mydogisbox 我注意到当我在 Linqpad 中运行它时 - 对于我所给出的示例,顺序并不重要,但我确实会确保组合的单数部分成为列表的新头部以确保排序。 - Clint
@mydogisbox 我刚刚进行了更改,使用了更便宜的::,并使得该项最终位于头部位置。 - Clint

2

F#支持开箱即用的列表推导。

let myOptions =
    [
        yield computeOptionValue()
        yield computeOptionValue()
    ]

谢谢您,我已经知道了。也许我应该编辑我的问题以表明结果列表只是一个示例,实际上这个列表将由某些容器类型表示,并具有其他方法等。 - Clint
在这种情况下,一个更简单的选项可能是为您的类型定义一个 ofList 函数。 - Daniel
确实可能是这样,但如果我想要透明地进行任何特定的操作,那么我似乎需要计算表达式,不是吗?例如,如果我想要能够执行 let! x = ("n",12) 并自动将元组转换为 Option<int> 类型。 - Clint
2
既然你似乎在进行黑魔法,那么就只能靠你自己了。 :) - Daniel

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