Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F#: Best practices for organizing types and modules

If I take the functional approach of defining a type, and then describing functions (as opposed to instance methods) that operate on that type, how should I organize my code?

I generally go one of three ways:

(1):

module MyType =
    type MyType (x : int) =
        member val X = x with get

    let myFunction myType y = myType.X + y

(2):

type MyType (x : int) =
    member val X = x with get

[<RequireQualifiedAccess>]
module MyTypeOps =
    let myFunction myType y = myType.X + y

(3):

type MyType =
    {
        x : int
    }

    static member MyFunction myType y = myType.x + y

Pro of (1) is that functions are defined in a module. Cons of (1) are that the type is defined in the module, too, resulting in ugly MyType.MyType redundancy on instantiation and the inability to [<RequireQualifiedAccess>] if you want to allow open MyType as a work-around for that redundancy.

Pros of (2) are functions defined in a module, and no module.type redundancy. Con is that the module cannot be the same name as the type.

Pro of (3) is that the methods are static, and no module.type redundancy. Cons are that you cannot define some types (e.g. non-method values and active patterns) under this method.

Normally, I prefer (2), even though I hate having to name the module something that's less descriptive and intuitive than it should be. (2) also allows me to create mutually dependent types using type ... and ... in the rare case that I need to do that, which is obviously impossible for types defined in separate modules like approach (1).

What approach do my fellow F# programmers take? I'm wondering if I'm overlooking something obvious (or not-so-obvious), or, if I'm not, whether there's a convention out there that addresses the inability to name a module the same name as its corresponding type inside the same namespace.

like image 858
MiloDC Avatar asked Apr 24 '17 06:04

MiloDC


1 Answers

There's a fourth way that you haven't listed, which is to have the type and the module named the same. You think it can't be done, but it's actually a very common practice; you simply have to (in F# versions prior to 4.1) use the [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] attribute on the module. It's a bit ugly to write that everywhere, which is why F# 4.1 made this the default behavior if you define a type with the same name as a module. To see how it's done, look at the FSharpx.Collections code (among many other projects). Here's one file that isn't too horrendously big, that makes for a good example:

https://github.com/fsprojects/FSharpx.Collections/blob/master/src/FSharpx.Collections/Queue.fs

namespace FSharpx.Collections

type Queue<'T> (front : list<'T>, rBack : list<'T>) = 
    // ...
    member this.Conj x = 
        match front, x::rBack with
        | [], r -> Queue((List.rev r), [])
        | f, r -> Queue(f, r)
    // ...

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Queue =
    // ...
    let inline conj (x : 'T) (q : Queue<'T>) = (q.Conj x) 
    // ...

When you organize your code this way, with a type and module of the same name, the F# compiler is perfectly able to keep things straight -- especially if you follow standard naming conventions, having member methods named in PascalCase style but having functions in the module named in camelCase style. The C# compiler would get confused, but the CompilationRepresentation attribute takes care of that, ensuring that other .Net languages see the module with the name QueueModule. So from F#:

let q = new Queue([1;2;3], [])
let moreItems = q |> Queue.conj 4
let stillMoreItems = moreItems.Conj 5

From C#:

Queue<int> q = new Queue<int>({1,2,3}, {});  // Not valid list syntax, but whatever
Queue<int> moreItems = QueueModule.Conj(4, q);
Queue<int> stillMoreItems = moreItems.Conj(5);

The Queue.conj function looks more natural to use from F#, and the member method moreItems.Conj looks more natural to use from C#, but both are available in both languages.

like image 158
rmunn Avatar answered Nov 09 '22 06:11

rmunn