Hiding stuff can be very useful. When you create a library, or a DSL, or perhaps a framework, you want to provide a good user-experience by hiding implementation details. At the same time, you don’t want your code to add a magnitude of complexity, to fulfil this requirement. Fsharp is perfect for that, and the following may help.
Note that I will only handle cases which are harder to find on the internet, for basic syntax, please follow provided links.
Signature files
Signature files may be used to show or hide things at a file level. So if you have code file “mycode.fs”, you can have signature file “mycode.fsi”, which needs to come before the .fs file, in the file order of the project. So “mycode.fsi” specifies what can be seen in “mycode.fs”, by other files in the same project, and outside the project. What’s included in the .fsi will be accessible, what’s excluded will be hidden. You can automatically generate a signature file for the whole project, and use this as a start-point . I’ve seen issues with a generated signature file, so you still might need a bit of editing, but in most cases this is unnecessary.
Discriminated union
The cases of a discriminated union can either be fully hidden or fully public.
Example code:
| Case1
| Case2 of string
with
static member Switch a = if a = Case1 then Case2("hi") else Case1
type ClosedDU =
| Op1
| Op2
| Case1
| Case2 of string
with
static member Switch : OpenDU -> OpenDU
type ClosedDU
Hiding in a record
A record may have record properties, but also static and instance members . A record may not inherit from another class (including an abstract class), but a record may implement an interface. So how do you show and hide all that?
First example is with all record properties public. Example code:
Id : int
Name : string
}
with
static member OpenStatic() = ()
static member ClosedStatic() = ()
member this.Open() = ()
member this.Close() = ()
Id : int
Name : string
}
with
static member OpenStatic : unit -> unit
member Open : unit -> unit
Id : int
Name : string
}
with
static member Create() = {Id = 1; Name = "test"}
static member OpenStatic() = ()
static member ClosedStatic() = ()
member this.Open() = ()
member this.Closed() = ()
type ClosedRecord with
static member Create : unit -> ClosedRecord
static member OpenStatic : unit -> unit
member Open : unit -> unit
If a record implements an interface, you can hide it by excluding it from the signature file. You will get a warning that via reflection, one can still derive the interface. This is a design choice, if you hide to provide user experience, why care about someone digging with reflection?
Access Modifiers
Signature files are a nice way to hide and show large parts of your implementation. But sometimes you need it more fine grained.
If for example you have implemented an interface, or inherited from a baseclass (for classes), then you are required to include all interface or baseclass members in the signature file.
Sometimes that is more than you want to be public, because there can be technical reasons to choose for a baseclass/interface. For example, you want a set of types to implement a function member, while this function is only used internally. For user-experience, you don’t want to show that technical function to the outside world.
To hide such technical methods, you can use the “internal” access modifier on individual member functions, including “new “. You still need to mention all interface/inherited member functions in the signature.
Example code:
type Base() =
abstract member GetMyData : string with get
type StoreSomething (a : string ,b : int)=
inherit Base()
let str = a
let num = b
new (a: string) = StoreSomething(a,-1)
override this.GetMyData with get() = sprintf "%s:%d" a b
type Base =
abstract member internal GetMyData : string with get
[<Class>]
type StoreSomething =
inherit Base
new : string * int -> StoreSomething
new : string -> StoreSomething
override internal GetMyData : string with get
The outside world will see type “Base”, but that doesn’t have to be bad. You can also hide “Base”, but whether this is a good idea depends on your design.
You want to show a baseclass like “Base”, when you also implement types “StoreSomethingElse”, “StoreAnothingThing”, and you need to provide a combinator operator like this:
type Base() =
abstract member GetMyData : string with get
static member (>!<) (l:Base, r:Base) = [l, r]
static member (>!<) (l:Base, r:Base list) = [l] |> List.append r