Is it possible to not allow duplicate discriminated union types in a list of discriminated unions?

Viewed 79

The goal is to have a list of discriminated union types where only one type of a particular case is allowed in the list no matter the underlying data for example:

type Car = 
| Honda 
| Tesla of string 

let carList = [Honda; Tesla("Tesla"); Tesla(""); Tesla("Tesla")]

//List should look like 
let correctCarList = [Honda; Tesla ("Tesla")]

In the case above thier should only be one element of type tesla allowed in the list, even if the underlying data associated with that case is different, it's still a duplicate tesla type.

I'm struggling to see if this is possible, if I make a set the set will only prohibit duplicate types where the type and underlying data is the same (so it would remove the extraneous Tesla("tesla") because there are two of them. Is it possible to filter out DS cases just by thier type only? Is there a way to only allow one descriminator to be created from a DS type?

3 Answers

You can do this by implementing your own custom comparison on the type, but I would recommend being very careful, because it may not always behave the way you expect:

[<CustomComparison; CustomEquality>]
type Car = 
    | Honda
    | Tesla of string

    member private carA.CompareTo(carB : obj) =
        match carA, carB :?> Car with
            | Honda, Tesla _ -> -1
            | Tesla _, Honda -> 1
            | _ -> 0   // any Tesla equals any other Tesla

    interface IComparable with
        member carA.CompareTo(carB) =
            carA.CompareTo(carB)

    override carA.Equals(carB) =
        carA.CompareTo(carB) = 0

    override car.GetHashCode() =
        match car with
            | Honda -> 0
            | Tesla _ -> 1

Test code:

set [Honda; Tesla("Tesla"); Tesla(""); Tesla("Tesla")]
    |> printfn "%A"   // set [Honda; Tesla "Tesla"]

(Tesla("Tesla") = Tesla(""))
    |> printfn "%A"   // true!
    type Tesla = Tesla of string
    type Honda = Honda
    // 0 or 1 Honda and 0 or 1 Tesla
    type Garage = Honda option * Tesla option
    // for when you want zero to many hondas, and zero to one tesla
    type BigGarage = Honda list * Tesla option 
    // the valet requires at least one honda to be registered
    type ValetBigGarage = Honda * Honda list * Tesla option

the options are endless, enjoy! While it's a less fancy solution it also should always work exactly as you expect.

It is worth adding that if you just wanted to turn the list with repeated kinds of cars into a list that has no duplicates, you can do this using the simple List.distinctBy function:

type Car = 
| Honda 
| Tesla of string 

let carList = 
  [Honda; Tesla("Tesla"); Tesla(""); Tesla("Tesla")]

let correctCarList = 
  carList
  |> List.distinctBy (function Honda -> 1 | Tesla _ -> 2)

This does not give you any type guarantees (you can still create incorrect lists), but it is the simplest thing that may work fine in many cases.

Related