DEV Community

loading...

Beautiful folds in F# - Part 2

shimmer profile image Brian Berns Updated on ・4 min read

When we left off, we had created a mechanism for applying multiple arbitrary functions to a sequence while enumerating the sequence only once. The only problem is that the result is a nest of tuples that we'd like to flatten out.

Extractor functions

To make this work, we need is a final step in the process that transforms the completed accumulator into whatever form the caller would like. Each of our primitive computations will continue to return its completed accumulator unchanged, so let's make this explicit by specifying the id function (id x = x) in the tuple that defines the computation:

let composableSum =
    (fun acc item -> acc + item),
    0,
    id   // new

let composableLength =
    (fun acc item -> acc + 1),
    0,
    id   // new

let composableMin =
    (fun acc item -> min acc item),
    Int32.MaxValue,
    id   // new

let composableMax =
    (fun acc item -> max acc item),
    Int32.MinValue,
    id   // new

Now we need to modify the compose function to take these "extractor" functions into account:

/// Creates a new computation that's composed of the two given computations.
let compose (step1, init1, extract1) (step2, init2, extract2) =
    let step (acc1, acc2) item =
        step1 acc1 item, step2 acc2 item
    let init = init1, init2
    let extract (acc1, acc2) =   // new
        let result2 = extract2 acc2
        extract1 acc1 result2
    step, init, extract

Let's walk through the new extract function carefully. The input to this function contains the completed accumulators of the two computations we're combining, acc1 and acc2. First we extract the result of the right-hand computation by applying its extractor function, as normal:

let result2 = extract2 acc2

The next line is the key to the entire process, so make sure it makes sense to you. The "obvious" thing to do is to apply the left-hand computation's extractor to its completed accumulator in the same way:

// WRONG - doesn't help
let result1 = extract1 acc1

However, this would leave us right back where we started, with two separate values (result1 and result2) that haven't been combined meaningfully. What if we instead assume that the left-hand extractor actually takes two inputs and knows how to combine them in whatever way is desired by the caller:

// RIGHT!
extract1 acc1 result2

Note what we're passing to this extractor: the final result of the right-hand computation (obtained by extract2 acc2) with the completed accumulator of the left-hand computation (acc1).

Composable combiners

This seems promising, but it assumes that the left-hand computation contains, in its "extractor", a function that takes the results of two computations and knows how to combine them. Our toMean function, as an example, is such a function:

let toMean (sum : int) (length : int) =
    float sum / float length

But to create a computation, we also need to specify a "step" function and initial value:

let composableToMean =
    (* step function? *),
    (* initial value? *),
    (* something that supplies "toMean" *)

Strangely, it doesn't matter much what we do here, as long as the extract function supplies toMean to compose, so let's create the simplest computation we can think of that the compiler will accept: a step function that always answers the unit value () regardless of its input. This means that we can also use () as the initial value. The extract function will then ignore the final accumulator, which will always be (), and simply return toMean:

let composableToMean =
    (fun () item -> ()),
    (),
    (fun () -> toMean)

You can think of this value as a setup that has to be in this form in order to prepare for the rest of the computation. Note that the type of the extractor is unit -> int -> int -> float, so it's actually a function that takes three arguments. It ignores the first one and sends the other two to toMean. Now we can compose and run our computation as follows:

let comp = composableToMean <*> composableSum <*> composableLength
run comp <| unrepeatable [ 1 .. 10 ]

The result is 5.5, as desired, but let's rewind a bit and think about what's going on when compose has to combine the completed accumulator of composableToMean with the completed accumulator of composableSum. First it applies composableSum's extractor (which is just id) to composableSum's completed accumulator (which is 55), producing 55. Next it invokes composableToMean's extractor with two parameters. The first is composableToMean's completed accumulator, which is just () and the second is the result of composableSum's computation, which we now know is 55. So we have something like this:

(fun () -> toMean) () 55

The first argument is consumed and ignored, and the second becomes a partial function application, toMean 55, which is itself a function that takes an argument (the length of the sequence) and returns the actual mean. This final function application occurs during the second invocation of compose.

This approach can be used to create arbitrary computations with a final step that combines the result of each component however desired. A single argument to the "combiner" function is supplied by each component, as we just saw.

For example, let's say that we wanted to again compute sum, length, min, and max in a single pass and combine them into a custom record type:

let createRecord sum length min max =
    {|
        Sum = sum
        Length = length
        Min = min
        Max = max
    |}

Let's create a helper function that turns any custom "combiner" function into a computation definition, just like we did for toMean:

/// Creates a composable version of the given function.
let composableCombiner combiner =
    (fun () item -> ()),
    (),
    (fun () -> combiner)

Our final computation is then defined as:

(composableCombiner createRecord)
    <*> composableSum
    <*> composableLength
    <*> composableMin
    <*> composableMax

I'm going to pause here again and follow up soon with a final installment that wraps up the idea of "beautiful folds". If you're still with me, thanks for reading!

Now available: Beautiful folds in F# - Part 3: Applicatives

Discussion

pic
Editor guide