Is it acceptable to use an underscore instead of a generic type?

When specifying a generic type I would normally use something like:

| head::tail -> tail |> List.forall(fun (item: 'a list) -> item.Length = head.Length)

However, I’ve seen code which uses an underscore instead, like this:

| head::tail -> tail |> List.forall(fun (item: _ list) -> item.Length = head.Length)

Is this a ‘normal’ thing to do?
What are the pros/cons of doing this?
Is this sort of thing accepted/expected or is it ‘frowned upon’?
Are there good reasons for doing it or should I not normally use it?

I found this in the documentation: Generics - F# | Microsoft Learn

“To specify that a type argument should be inferred by the compiler, you can use the underscore, or wildcard symbol (_), instead of a named type argument.”

…but it doesn’t give any more information about it.

So, an underscore and a generic type can sometimes overlap or be used in the same spot, but they mean pretty different things. Underscore pretty much just means I’m too lazy to write out the type or perhaps figure out what the type is supposed to be, and would rather the compiler did that for me. A generic type means that when invoking the function, any type should suffice (or perhaps any type that satisfies some constraints). You already pointed out an example where you can use an underscore or a generic type interchangeably. An example where you can use an underscore but not a generic type would be

let matches : Customer -> CustomerContactInfo = ...

let lookupContact (contacts:IDictionary<_,_>) (employee:Employee) customer =
  match contacts.TryGetValue employee.ID with
  | (true, contacts) -> contacts |> Array.tryFind (matches customer)
  | (false, _) -> None

So there I could annotate contacts as IDictionary<EmployeeID, CustomerContactInfo[]>, but that’s a lot of typing. I really just need to tell the compiler that it’s an IDictionary, and it can figure out the rest. But it wouldn’t be correct if I said IDictionary<'a, 'b>, because the key and value types have to match specific concrete types, not just anything.

The compiler always tries to infer the most generic type possible, so it just so happens that an underscore can often end up being the same as just a generic type, but they do technically say different things.

If you are writing an annotation and intend the input to truly be generic, I recommend sticking with 'a instead of _, because the compiler will tell you if you accidentally do something to make it no longer generic, e.g.,

> let f (x:'a) =
-   if x = 5 then true else false;;

    if x = 5 then true else false;;

stdin(11,10): warning FS0064: This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'int'.

val f: x: int -> bool

Use the underscore when you really intend to say, “I don’t care to bother with what type this is, and want the compiler to just figure it out.” Of course you don’t need the annotation at all if the whole thing is just an underscore, so you often see it when you need to tell the compiler part of an annotation but don’t want to type out the whole thing.

1 Like

Ah, thank you; that explains it nicely.

Underscore = Tells the compiler to work it out for itself (it it can);

Generic Type = Tells the compiler that anything which ‘fits’ will work (and to warn otherwise).

I’d like to add that in most cases, esp if the types are records or DUs, that the type will be inferred. In other words, you don’t have to specify a type, or an underscore, at all. This greatly simplifies writing functions and also helps with refactoring (change the type name won’t require a change to every function that uses that type).

Note that sometimes when you need to cast a type, you can force the compiler to infer, as opposed to typing the whole type, like x :> _ or y :?> _.

1 Like

Thanks for the extra information.