Search code examples
f#discriminated-union

Can I call methods of "aliased" types in single-case discriminated unions?


I read in the excellent F# for Fun and Profit that I could use single-case discriminated unions for, among other things, get type safety. So I tried to do that. But I quickly discovered that I could not call the methods of the type that was (sort of) aliased or viewed by the DU.

Here is an example:

type MyList = MyList of List<int>
let list' = [1; 2; 3]
printfn "The list length is %i" list'.Length
let myList = MyList([1; 2; 3])
// The list length is 3
// type MyList = | MyList of List<int>
// val list' : int list = [1; 2; 3]
// val myList : MyList = MyList [1; 2; 3]

So far so good. Then I tried

myList.Length
// Program.fs(12,8): error FS0039: The field, constructor or member 'Length' is not defined.

Is there a simple way of accessing the methods of the "aliased" or "viewed" type when one is using a single-case DU?


Solution

  • This is not what you call an "alias". You've declared a whole new type, which wraps a list inside it. Your type is not just another name for list, it's a wrapper.

    If you want to declare a true alias, you should use syntax type A = B, for example:

    type MyList = List<int> 
    let myList : MyList = [1; 2; 3]
    printf "Length is %d" myList.Length
    

    With such alias, MyList will be 100% substitutable for List<int> and vice versa. This is what is usually meant by "alias". Different name for the same thing.

    A wrapper, on the other hand, will give you extra type safety: the compiler will catch if you try to use a naked List<int> where MyList is expected. This is how such wrappers are usually used. But this extra protection comes with a price: you can't just use MyList as if it was a list. Because that would kill the whole idea. Can't have it both ways.

    Q: wait, do you mean to say I have to reimplement all the list functionality just to get this wrapper?

    Well, no. If you move past methods and towards functions, you can make this wrapping/unwrapping generic by providing a map:

    type MyList = MyList of List<int>
       with static member map f (MyList l) = f l
    
    let myList = MyList [ 1; 2; 3 ]
    let len = MyList.map List.length myList
    

    If you find yourself using a specific function a lot, you can even give it its own name:

    let myLen = MyList.map List.length
    let len = myLen myList
    

    Here is another article from Mr. Wlaschin on the subject.

    Also note: this is yet another way in which methods are inferior to functions. One would generally do well to avoid methods when possible.