0

Can the F# compiler separate out code paths by currying a function in which different types imply different paths through subsequent called functions?

Consider the following discriminated union. There are 2 possibilities, which are theoretically different types:

type Choice =
    | Halve
    | Double

Suppose we have some specific functions for some of these cases:

let halve value = value / 2.0
let divide value = value * 2.0

And 2 functions which provide 2 separate code paths depending on the type of some param (remainder to complete the fs file):

let inner param value = 
    match param with
    | Choice.Halve -> halve value
    | Choice.Double -> divide value

let outer param value =
    let internalVariable = 
        match param with
        | Choice.Halve -> "halving"
        | Choice.Double -> "doubling"
    inner param value


[<EntryPoint>]
let main argv = 
    printf "%g\n" (outer Choice.Halve 4.0)
    let doubler = outer Choice.Double
    printf "%g\n" (doubler 6.0)

The Halve and Double are thus separate code paths, and we could have written them as two separate functions.

Theoretically, currying would say that there are two different functions; if you curry the first parameter to either of the Choice.Halve or Choice.Double types (like in doubler), then you have a function specific to that type and the compiler should be able to optimise later branches out.

Is that the case?

If I look in the IL, I can see no such optimisation but I suppose it's possible that this is JITted. A colleague suggests that branch prediction makes such optimisations unnecessary.

Is the only way to avoid the unnecessary branches to invert the structure and pass the divide/halve function in?

-- Edit --

John Palmer suggested adding inline, so I tried it and got the following optimised IL for outer:

IL_0001: ldarg.0
IL_0002: call instance int32 Program/Choice::get_Tag()
IL_0007: ldc.i4.1
IL_0008: bne.un.s IL_0016

IL_000a: ldarg.1
IL_000b: ldc.r8 2
IL_0014: mul
IL_0015: ret

IL_0016: ldarg.1
IL_0017: ldc.r8 2
IL_0020: div
IL_0021: ret

However, there is no apparent optimisation of the doubler curried function in main - so the uncurried function is being optimised, but not the curried one.

Phil H
  • 18,593
  • 6
  • 62
  • 99

1 Answers1

2

I don't think the compiler will do this automatically, but you can write your code so that it implements this explicitly:

let inner param = 
    match param with
    | Choice.Halve -> (fun value -> halve value)
    | Choice.Double -> (fun value -> divide value)

This function can be used in the same way as your original function, e.g. inner 1.0 2.0, but when you call it with just a single argument, it actually runs the first part of the body and returns a function created in the body of the match case.

Tomas Petricek
  • 225,798
  • 19
  • 345
  • 516
  • And note that `(fun value -> halve value)` is just an eta-expanded way of writing `halve`. – kvb Jun 06 '14 at 13:41
  • So if I replaced `halve value` with `halve`, `divide value` with `divide` and removed value from the argument list of `inner`, that would provide a separate path and get optimised? Why is there such a difference? – Phil H Jun 06 '14 at 13:52