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

Can I instantiate classes containing values with side effects at the top level?

此问题与 中的问题相关并重叠,请 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.

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

根据这个建议,我实现了一个简单的 class 来包含一个有副作用的值:

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"

到目前为止一切顺利。在上面的示例中,class 相当于 "light",我可以在任何函数中实例化它。

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

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"

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

简短的回答是 - 是的,这首先违背了使用这种包装器的目的。

然而,该指南有点只见树木不见森林 - 真正的问题是在提倡函数纯度和引用透明性的环境中管理状态依赖关系和外部数据的更基本问题,尤其是当您正在寻找时在一个需要随时间增长和变化的大型代码库中(如果我们正在查看一次性脚本,只需做能完成工作的事情)。更多的是如何填充和使用 roots 字段(作为硬编码的静态依赖项),然后那里的值是否包含在 class 中。

我在这里推荐的方法是将您的业务逻辑编写为纯函数的一个模块(或多个模块),并将依赖项作为参数显式传递。这样,您就可以将有关依赖项的决定推迟到调用者。这可能一直向上,直到程序的入口点(控制台应用程序中的主要功能,API 中的 Startup class 等等)。在可怕的 OOP 术语中,您所看到的相当于组合根 - 程序中 assemble 您的依赖项所在的位置。

这可能涉及 class 围绕其他纯功能模块的包装器,正如您 link 所建议的惯例,但这不是一个已成定局的结论。你很可能有一个(副作用)函数来为你产生价值,你可以只传递这个单一的价值。

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

这样您就可以完全控制您的有效操作 - 您可以随时调用该函数,如果环境发生变化,您可以再次调用它以获得新值。代码的其余部分既不知道也不关心您输入的值是否来自有效的操作——它使推理它所做的事情以及测试变得简单。

用 class 包裹它本身不会给这些属性增加任何东西。它可能会提供更好的 API 以获得更多的样板文件,但正在讨论的真正问题在别处。