Scott Nimrod
Scott Nimrod

Reputation: 11595

Is there a step-by-step process for converting a series of pipelines into a functional composition?

Is there a step-by-step process for converting several pipelines into a functional composition?

Side note: Did I even use the term "functional composition" in the right context?

I have the following code:

let purchase qty inventory =
    buyFrom inventory qty |> fromInventory
                          |> reduceBy qty

I wanted to refactor this code to support functional composition.

So I thought I could do this:

let purchase qty inventory =
    buyFrom inventory >> fromInventory >> reduceBy qty

This code compiles. However, it appears that I am missing an argument when attempting to invoke the purchase function within the debugger.

Here's my test for the function:

[<Test>]
let ``buy last product``() =
    [SomeProduct] |> purchase 1
                  |> should equal []

NOTE:

I'm still struggling with FP fundamentals and still do not understand the process of converting a pipeline work flow into a functional composition.

I think I understand that functional composition relies on partial application which I think I have a handle on now. Thus, I have reason to believe that my refactored function is missing an argument.

Any guidance on a workflow I can use to get better at functional composition?

Appendix:

type Product =
    | SomeProduct

type TransactionResult = { Price:decimal; Inventory:Product list }

(* Functions *)

let priceOf qty product =
    match product with
    | SomeProduct -> match qty with
                     | 3 -> decimal (qty - 1) * 1.99m
                     | _ -> decimal (qty) * 1.99m

let rec reduceBy count list =
    match list with
    | first::remaining when count = 1         -> remaining
    | first::second::remaining when count > 1 -> second::remaining |> reduceBy (count - 1)
    | remaining when count = 0                -> remaining
    | _ -> []

let buyFrom inventory qty =
    { Price=priceOf qty (List.head inventory); Inventory=inventory }

let fromInventory transactionResult =
    transactionResult.Inventory

Upvotes: 7

Views: 137

Answers (2)

TheInnerLight
TheInnerLight

Reputation: 12184

The direct translation to a composed form would be either:

Using "forward" function composition:

let purchase qty inventory = (buyFrom inventory >> fromInventory >> reduceBy qty) qty

Using "backward" function composition (Personally, I'm not sure I like the term "backward" because this is function composition in the traditional mathemetical sense of the term, i.e. f << g = f ∘ g):

let purchase' qty inventory = reduceBy qty << fromInventory << buyFrom inventory <| qty

As you can perhaps see from the above examples, it's really most valuable when it lets you skip the last argument entirely. In this example, you can't do that because you depend on qty twice so I'd recommend you stick with piping here.


In general, if you have a function f(x) and a function g(x) you can write a composed function h(x) = (f ∘ g)(x) in the following ways:

let h x = f (g (x))
let h x = x |> g |> f
let h x = f << g <| x
let h x = (g >> f) x

In the second two cases, using function composition, you can omit the x entirely.

let h = f << g
let h = g >> f

Upvotes: 7

Mark Seemann
Mark Seemann

Reputation: 233150

Usually, if you can massage your pipeline expression to have the form

let f x y value = value |> f1 x |> f2 y |> f3

you can refactor it to

let f x y = f1 x >> f2 y >> f3

That's not quite the case here, as your expression begins with a function call (buyFrom inventory qty), the value of which is then piped to fromInventory.

In this case, I think you can refactor to

let purchase' inventory qty =
    qty |> buyFrom inventory |> fromInventory |> reduceBy qty

but then you can't really get any further because qty is also needed as an argument to reduceBy.

I think you might be able to eliminate both occurrences of qty by using the Reader or State monad, but it's hardly worth the effort.

Upvotes: 6

Related Questions