我可以在顶层实例化包含副作用值的类吗?

时间:2018-05-26 17:41:02

标签: class f# side-effects

这个问题与Should one wrap type providers containing values that have side effects inside a class?中的问题有关并且重叠,由Aaron M. Eshbach友好回答。

我正在尝试在我的代码中实现F# coding conventions页面

中的优秀建议

https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions

Use classes to contain values that have side effects部分特别有趣。它说

There are many times when initializing a value can have side effects, such as instantiating a context to a database or other remote resource. It is tempting to initialize such things in a module and use it in subsequent functions.

并提供了一个例子。然后它指出了这种做法的三个问题(我省略了那些缺乏空间,但可以在链接的文章中看到它们)并建议使用一个简单的类来保存依赖。

愚弄这个建议我实现了一个简单的类来包含一个有副作用的值:

type Roots() =
    let msg = "Roots: Computer must be one of THREADRIPPER, LAPTOP or HPW8"

    member this.dropboxRoot =
        let computerName = Environment.MachineName 
        match computerName with
        | "THREADRIPPER" -> @"C:\"
        | "HP-LAPTOP" -> @"C:\"
        | "HPW8" -> @"H:\"
        | _ -> failwith msg

然后我可以在函数

中使用它
let foo (name: string) =
    let roots = Roots()
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Temp\" + name + ".csv")
    printfn "%s" path

foo "SomeName"

到目前为止一切顺利。在上面的例子中,类很“轻”,我可以在任何函数中实例化它。

然而,包含具有副作用的值的类也可能是计算密集型的。在这种情况下,我想只实例化一次并从不同的函数调用它:

let roots = Roots()

let csvPrinter (name: string) =
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
    printfn "%s" path

let xlsxPrinter (name: string) =
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder2\" + name + ".xlsx")
    printfn "%s" path

csvPrinter "SomeName"
xlsxPrinter "AnotherName"

所以我的问题是:如果我在模块的顶层实例化类Roots,我就打败了创建类的目的,这是为了避免F# coding conventions页面中描述的问题?如果是这种情况,我该如何处理计算密集型定义?

1 个答案:

答案 0 :(得分:5)

简短的回答是 - 是的,这首先打败了拥有这种包装的目的。

然而,该指南有点错过森林的树木 - 真正的问题是在一个提倡功能纯度和参考透明度的环境中管理有状态依赖关系和外部数据这个更基本的问题,特别是当你正在寻找一个需要增长和变化的大型代码库(如果我们正在查看一次性的一次性脚本,只需完成工作即可完成)。它更像是填充和使用roots字段的方式(作为硬编码的静态依赖项),然后是否包含在类中的值。

我建议的方法是将业务逻辑编写为纯函数的模块(或多个模块),并将依赖关系明确地作为参数传递。这样,您就可以将依赖关系的决定推迟给调用者。这可能会一直向上,到程序的入口点(控制台应用程序中的主要功能,API中的Startup类等)。在可怕的OOP用语中,你所看到的就是组合根 - 相当于程序中你组装依赖项的地方。

这可能涉及在其他纯功能模块周围使用类包装器,正如您链接的约定所暗示的那样,但这不是已成定局。您可能有一个(副作用)函数为您生成值,您可能只是将这一个值传递给它。

let getDropboxRoot () : string option = 
    let computerName = Environment.MachineName 
    match computerName with
    | "THREADRIPPER" -> Some @"C:\"
    | "HP-LAPTOP" -> Some @"C:\"
    | "HPW8" -> Some @"H:\"
    | _ -> None        

let csvPrinter (dropboxRoot: string) (name: string) =
    let path = Path.Combine(dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
    printfn "%s" path

通过这种方式,您可以完全控制有效的操作 - 您可以随时调用该函数,如果环境发生变化,您可以再次调用它来获取新值。其余的代码既不知道也不关心你输入的值来自有效的操作 - 它使得它的作用和测试的推理变得简单。

拥有一个类包装器本身不会为这些属性添加任何内容。它可能为更多的样板提供更好的API,但正在讨论的真正问题是其他地方。