Introduction

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.

Study case

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

Summary

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.



blog comments powered by Disqus

Published

27 April 2016

Tags