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

Sol*_*lma 3 f# class side-effects

这个问题与问题有关并且是否重叠在一个包装类型提供者是否包含在类中具有副作用的值?,亲切的回答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.
Run Code Online (Sandbox Code Playgroud)

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

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

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
Run Code Online (Sandbox Code Playgroud)

然后我可以在函数内部使用它

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

foo "SomeName"
Run Code Online (Sandbox Code Playgroud)

到现在为止还挺好.在上面的例子中,类很"轻",我可以在任何函数中实例化它.

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

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"
Run Code Online (Sandbox Code Playgroud)

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

scr*_*wtp 5

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

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

我在这里建议的方法是将业务逻辑编写为纯函数的模块(或多个模块),并将依赖关系明确地作为参数传递.这样,您就可以将依赖关系的决定推迟给调用者.这可能会一直向上,到程序的入口点(控制台应用程序中的主要功能,StartupAPI中的类等).在可怕的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
Run Code Online (Sandbox Code Playgroud)

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

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