Naming the arguments of functions that are themselves arguments in F#

Viewed 87

Giving names to function arguments is a way of documenting our code; if there was no such need then we could give them names such as “arg1” and “arg2” and be no worse for it – but in reality this would of course make our code much harder to understand. In F# we often pass around functions as arguments to other functions, in which case their arguments are no longer named:

let foo<'T> (bar : 'T -> 'T -> float -> 'T) =
    // do something

This function foo takes a “bar” as an argument, which itself takes two 'T and a float and returns a 'T, but it is hard to tell what it is supposed to do. It would become much clearer if we could name the arguments of “bar”:

let foo<'T> (bar : (startVal : 'T) -> (endVal : 'T) -> (interpolationAmt : float) -> 'T) =
    // do something

Now it is obvious what “bar” is used for: It interpolates between two values of type 'T. Furthermore it also becomes clear that the order of the 'T arguments matter and in which order to apply them. But unfortunately this code doesn’t compile.

So my question is if there is a way to do this that I’ve missed, and if not how do people usually deal with this problem?

2 Answers

If bar interpolates, you could call it interpolate.

You could also use simple type aliases as markers to aid communication. However, it's not clear from looking at foo that StartValue<'T> is just a 'T:

type StartValue<'T> = 'T
type EndValue<'T> = 'T
type InterpolationAmount<'T> = 'T

let foo<'T> (interpolate : StartValue<'T> -> EndValue<'T> -> InterpolationAmount<float> -> 'T) =
    ()

Another approach using a more object-oriented style would be to use an interface.

type IInterpolate<'T> =
    abstract member Interpolate : startVal:'T -> endVal:'T -> interpolationAmount:float -> 'T

let foo<'T> (interpolate : IInterpolate<'T>) =
    ()

EDIT: Another option is to use an anonymous record type:

let foo<'T> (interpolate : {| StartVal:'T; EndVal:'T; InterpolationAmount:float |} -> 'T) =
    ()

Unfortunately, it's not possible to add parameter names to the function signature. The best you can do to make the function signature easy to understand is custom types. But I suggest creating custom type only for real domain types which have some meaning on their own. Usually, such types have some restrictions - non-empty list, string with max length 42, negative integer etc. So for your function, I would create a type for interpolation amount:

type InterpolationAmount = private InterpolationAmount of float

module InterpolationAmount =
    let create (amount: float) : InterpolationAmount =
        if amount < 0. then failwith "blah-blah"
        else InterpolationAmount amount

At least it's now clear what float argument is and you can control the interpolation amount value.

Now about interpolation start and end. To ensure that the user will pass correctly arguments to this function you can make it a function with a single argument. You will of course lose the partial application option, but all arguments will have names and it's hard to mess up here:

type InterpolationArgs<'T> = {
    startValue: 'T
    endValue: 'T
    amount: InterpolationAmount
}

Of course, using a name other than bar will also give a hint to the user

let foo<'T> (interpolate : InterpolationArgs<'T> -> 'T) =
     interpolate {
        startValue = a
        endValue = b
        amount = InterpolationAmount.create 1.2
     }
Related