Search code examples
classf#type-providersside-effects

Should one wrap type providers containing values that have side effects inside a class?


I am trying to implement in my code the excellent advice in the F# coding conventions page

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

The section Use classes to contain values that have side effects is particularly interesting. It says

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.

and provides an example. Then it points out three problems with this practice (I omit those for lack of space, but they can be seen at the linked article) and recommends using a simple class to hold dependencies.

I wonder how should type providers be treated? For example, if I have the following code,

[<Literal>]
let projDataPath = __SOURCE_DIRECTORY__ + @"\data\"

[<Literal>]
let configPath = projDataPath + "config.json"

type Cnfg = JsonProvider<Sample=configPath>
let config = Cnfg.Load(configPath)

using the type provider to initialize a value is subject to the problems associated to value initialization with side effects described in the article?

In other words, should I wrap the type provider inside a class?


Solution

  • You should generally not expose instances of type providers or their provided types to consumers at all. Aside from the potential interoperability issues that may arise from non-F# .NET consumers, instances of Type Providers often represent an interface to some private state or resource that your application is managing. As a general rule, it is best to abstract the underlying resources from your consumer and present a model that best fits the problem domain.

    The linked article is specifically warning against capturing instances of classes with a finite lifecycle as bindings in a module, because the bindings are immutable and the instance will become invalid when its lifecycle ends. This would be equivalent to having a DbContext or similar instance as a static readonly member in a C# class. It would be initialized by the static constructor, but could never change, even if the database connection becomes closed and the DbContext instance is no longer useful.