Since some time, I have been struggling with one problem regarding pipe operator. Sometimes, I’d like to use intermediate values
arising when pipeline is processed. So, previously I was just splitting entire pipeline and create values from those intermediate values using let
binding.
Let’s take a look at this simple example. We want to split list at place where the highest number appears. So, my previous approach for that was something like this:
let inputList = [5;6;7;8;9;10;4;3;2;1]
let index =
inputList
|> List.indexed
|> List.maxBy (fun value -> snd value)
|> fst
inputList
|> List.splitAt index
However, recently I had some sort of breakthrough. I’ve realized that consecutive steps in pipe are just ordinary functions. Definition of pipe operator follows :
val inline ( |> ) : arg:'T1 -> func:('T1 -> 'U) -> 'U
It takes left argument and applies it to right function which takes argument with the same type as left argument of pipeline operator. So, what we are actually passing to pipe operator on the right is function. And actually what we have in our F#
toolset are lambda functions, and they are just functions without binding name to it.
For instance, we can create function using lambda functions, but assign name for it using let
binding:
let identity = fun x -> x
The same, we can when we are composing our pipeline. However, there are some nuances. Because we don’t have to use parentheses to declare lambda function I thought that arguments, which lambda function takes, are immediately available for the rest of pipeline. Unfortunately that’s not true. So, that’s what I tried first:
[5;6;7;8;9;10;4;3;2;1]
|> fun inputList -> List.indexed inputList
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList
But I got an error
The value or constructor inputList is not defined.
That actually makes sense if you know that F#
uses indentation to mark scope boundaries.
It’s better to spot when we add parentheses to lambda function
[5;6;7;8;9;10;4;3;2;1]
|> (fun inputList -> List.indexed inputList)
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList
Now, we can see that inputList variable is only visible inside lambda function. So, we can add parentheses to subsequent pipeline steps to make them part of this lambda function and allow inputList variable to be visible.
[5;6;7;8;9;10;4;3;2;1]
|> (fun inputList -> List.indexed inputList
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList)
Hmm, but it’s not looking that sexy, because now List.index inputList
expression puts new identation context at which we must ident subsequent expressions of this function.
However, there’s an exception regarding infix operators, you can offset them from the actual offside column by the size of token plus one, so in that case previous code snippet looks like this:
[5;6;7;8;9;10;4;3;2;1]
|> (fun inputList -> List.indexed inputList
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList)
Back to the topic, what we can do to improve our code’s appearance and still be able to access intermediate values from pipeline? We can move first expression of lambda function to newline and that would mean next expressions in that function must be indented the same as that line. Take a look:
[5;6;7;8;9;10;4;3;2;1]
|> fun inputList ->
List.indexed inputList
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList
But now, we break how pipeline looks like, and I feel this is not intuitive. We can take advantage of previously mentioned exception for infix operators. Anyway, I feel it is improved a litte bit but still doesn’t solve the issue.
[5;6;7;8;9;10;4;3;2;1]
|> fun inputList ->
List.indexed inputList
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList
I feel the most comfortable if we ident lambda function expressions a bit to explicitly denote that they belong to that function
[5;6;7;8;9;10;4;3;2;1]
|> fun inputList ->
inputList
|> List.indexed
|> List.maxBy (fun value -> snd value)
|> fst
|> fun max -> List.splitAt max inputList
We can do one more thing to get rid of last lambda function, we can define helper function flip
which does one simple thing. It allows to flip arguments which will be passed to given function. Finally, pipeline looks like this
let inline flip f x y = f y x
[5;6;7;8;9;10;4;3;2;1]
|> fun inputList ->
inputList
|> List.indexed
|> List.maxBy (fun value -> snd value)
|> fst
|> flip List.splitAt inputList
We’ve seen how we can reuse intermediate values from pipeline using lambda functions. Of course these are not all possibilities of how you can write that pipeline. I hope you will find something useful here, but just use a way that you feel more comfortable with.