3

I implemented a Discriminated Union type that would be used to select a function:

type BooleanCombinator =
    | All
    | Some
    | None
    | AtLeast of int
    | MoreThan of int
    | NotMoreThan of int
    | LessThan of int
    | ExactlyOne
    | ExactlyTwo
    | AllButOne
    | AllButTwo

let boolToInt (b: bool) : int = if b then 1 else 0

let combineBooleans (combinator : BooleanCombinator)
                    (bools      : bool list)
                                : bool =

        let n = List.sumBy boolToInt bools

        match combinator with
        | BooleanCombinator.All -> List.forall id bools
        | BooleanCombinator.Some -> bools |> List.exists id
        | BooleanCombinator.None -> bools |> List.exists id |> not
        | BooleanCombinator.AtLeast i -> n >= i
        | BooleanCombinator.MoreThan i -> n > i
        | BooleanCombinator.NotMoreThan i -> n <= i
        | BooleanCombinator.LessThan i -> n < i
        | BooleanCombinator.ExactlyOne -> n = 1
        | BooleanCombinator.ExactlyTwo -> n = 2
        | BooleanCombinator.AllButOne -> n = bools.Length - 1
        | BooleanCombinator.AllButTwo -> n = bools.Length - 2

This looked Ok to me but the compiler started to look at all instances of Some and None as belonging to this DU, instead of the Option DU.

I do not want to go through all of my code replacing Some with Option.Some and None with Option.None.

Is there a way to tell the compiler that unqualified Some and None are actually Option.Some and Option.None?

Or should I just give different names to these DU cases, like AtLeastOne and ExactlyZero

Soldalma
  • 4,636
  • 3
  • 25
  • 38

2 Answers2

6

The general rule for resolving name collisions in F# is "last declaration wins". Because your custom DU is declared after Option, its constructors Some and None win over those of Option.

But this rule offers a way to fix the problem: you just need to "reassert" the declarations after your custom DU:

type Bogus = Some of int | None

let g = function Some _ -> 42 | None -> 5
let x = Some 42

let inline Some a = Option.Some a
let inline None<'a> = Option.None : 'a option
let (|Some|None|) = function | Option.Some a -> Some a | Option.None -> None

let f = function Some _ -> 42 | None -> 5
let y = Some 42

If you inspect the types of g, x, f, and y in the above code:

> g
g : Bogus -> int

> f
f : 'a option -> int

> x
Bogus

> y
int option

The function g and value x were inferred to have type Bogus -> int and Bogus respectively, because Some and None in their bodies refer to Bogus.Some and Bogus.None.

The function f and value y were inferred to have Option-related types, because Some and None in their bodies refer to the Some function and the (|Some|None|) active pattern that I defined just above.

Of course, this is a rather hacky way to restore status quo. This will convince the compiler, but humans will still have a hard time reading your code. I suggest you rename the cases of your DU instead.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
3

You can mark your DU with [<RequireQualifiedAccess>] attribute.

This means that you will be required to qualify the case name with the type whenever you use it in the code - which is something you do now anyway in your match expression.

That way an unqualified Some would still be resolved to mean Option.Some, despite the fact that you reuse the name.

It's a useful technique to know when you want to use a snappy name for a DU case - like None, Yes, Failure etc. - that by itself would be ambiguous or confusing to the reader (or the compiler, for that matter).

scrwtp
  • 13,437
  • 2
  • 26
  • 30