F# tips weekly #9: Shadowing

F# tips weekly #9: Shadowing

Shadowing is one of the core design choices of the F# language. Although it may seem like a simple feature, allowing us to define a new binding with the same name as an existing one, there are multiple, sometimes surprising, implications and use cases.

Recursive functions

Shadowing is actually the reason why the rec keyword is required for recursive functions. Without the rec keyword, using the name of a function inside its body would be considered as shadowing.

let factorial n = [1..n] |> List.fold (fun x y -> x * y) 1

let rec factorial n =
    if n = 0 then 1
    else n * factorial (n - 1)

In the above example, with two versions of the factorial function, if the second one is without the rec keyword, it would call the first one instead of itself.

Order of open directives

Shadowing works globally, which means if we have two modules with functions of the same name, the one that is defined later will shadow the first one. This means that the order of open directives is important, and a change in order can introduce subtle bugs.

Replace library function

Shadowing can be used to replace a library function with a custom implementation. For example, we can log every call to the List.map function.

module List =
    let map f l = 
        printfn "Call List.map with %A" l
        l |> List.map f

Note that this new definition will only work in the scope where it is defined. We can use the [<AutoOpen>] attribute to make it available everywhere the project with the definition is referenced.

[<AutoOpen>]
module List =
    let map f l = 
        printfn "Call List.map with %A" l
        l |> List.map f

💡 Note that modules are not shadowed, but the content of two modules is merged together.

Obsolete attribute

We can use shadowing to emit a warning or error when some function is used. By using the [<Obsolete>] attribute, we can generate a generic warning message:

module Option =
    [<System.Obsolete("Option.get can throw an exception. Consider using Option.tryGet")>]
    let inline get x = Option.get x

We can also use the [<CompilerMessage>] attribute to generate a warning with a custom error code:

module Option =
    [<CompilerMessage("Option.get can throw an exception. Consider using Option.tryGet", 10001)>]
    let inline get x = Option.get x

We can even convert it into a compiler error:

module Option =
    [<CompilerMessage("Option.get can throw an exception. Consider using Option.tryGet", 10001, IsError = true)>]
    let inline get x = Option.get x

Replace generated function

In tip #5, we saw that we can use shadowing to replace a generated discriminated union case constructor.

We can even shadow generated patterns for pattern matching with Active Patterns. For example, we can extend cases for pattern matching with Square.

type Shape =
    | Rectangle of width: float * length: float
    | Circle of radius: float

let (|Square|Rectangle|Circle|) = function
    | Shape.Rectangle (w, l) when w = l -> Square w
    | Shape.Rectangle (w, l) -> Rectangle (w, l)
    | Shape.Circle r -> Circle r

💡 Note that we need to define all cases in one active pattern to keep the incomplete pattern match warning feature.

Replace types

It is possible to also shadow types, but I cannot recommend it because we actually create a new type with the same name, and all functions that used the original type will not work with the new one. So, this technique will need to also shadow all relevant functions on the original type.

Conclusion

Shadowing is a powerful feature that can be used not only to rebind local bindings but also to extend external functions or create a sort of custom linter.