Friday, January 16, 2009

A forward pipe (“ |> ”) operator in Scala

One of the addictive things from F# is the |> operator defined by

but I've not yet found an equivalent in Scala, even though there are a ton of useful things given by the built-in APIs -- for example, I've not had to implement the usual abstraction to manage an (array, offset, length) combination, when there's Array.slice() there already.

So, just for fun...

with unit test

Refrigerator logic — I later realise that without actually adding any variance annotations the piped-to function can be one that takes any super-class of the value type. A little more thought suggests to me that type-inference does that automagically because it is able to reconcile all inputs into the types in that single expression, and will force the value to the supertype in order to make everything match up.

8 comments :

Daniel said...

I really like this. Scala's semicolon inference makes it impossible to lay it out vertically, as we usually see in F#, within a block, but all you have to do is place the whole computation inside parenthesis, making it an expression:

(input
|> ("""\s\s+""".r replaceAllIn(_ : String, " "))
|> doStuff
)

Of course, this is not all that much useful in Scala, as the functions are methods defined on the class o the data. So you really don't need any operator to use map, filter, etc.

Still, I think it would be a nice addition to Scala's library.

Unknown said...

Why not just

implicit def toPiped[T] (value:T) = new {
def |>[R] (f : T => R) = f(this.value)
}

Daniel said...

@Nodir

Use of implicits in the way suggested results in reflection calls, which are slow. It may not matter, but, then again, it may.

Unknown said...

@Daniel

What a pity! Anyway it is inconsistent: other Scala code is allowed to access anonymous class members, but the implicit method return type is Object. The return type could be the anonymous type, which is public btw. At least they could use type casting instead of reflection.

James Moore said...

What about composing the functions with andThen?

Instead of:

42 |> makeFloating |> sample

it's

val chainOfEvents = (makeFloating _) andThen sample

val result = chainOfEvents(42)

And of course you can move chainOfEvents somewhere where it's only evaluated once if you need to.

Steve Gilham said...

Yeah, the pipe operator is probably best suited to a style somewhat more on the functional side than Scala's sweet spot; one where data and functions are separated, the individual steps are written as short functions on one object, then you push the plain ol' data records of different types through them e.g.

val (v,r, lonsun) = now |> decimalDay |> Sun |> SunLongitude
val (xg, yg, zg) = now |> decimalDay |> Moon |> primaryCentric

to take an example from the little ephemeris applet I wrote a while back.

Most times, the particular stack of transformations on a piece of data are a one-off -- in this case the value of

(decimalDay _) andThen Sun andThen SunLongitude

would only be used in the one place.

On an aesthetic note -- as discussed in the Staircase book, the parser combinators are defined in symbolic operator style, so that they don't drown the grammar. For the same reason that the ~ combinator is not named andThen, Function1.andThen could do with a >> synonym too. There's precedent, after all, in the form of the fold operations having /: and :\ synonyms.

D. Gates said...

It's part of my standard repertoire, since it lets the order of my code consistently match the order of my operations. Scala code naturally does this when chaining methods or chaining higher-order function calls on collections, and the forward-pipe lets me keep the chain going:

((1 to 300) map square
map (_ % 85)
|> (_.max)
|> println

Jamie Pullar said...

I found it useful to extend the functionality to tuples:

class PipeExtension[A](a: A) {
def |>[B](func:A => B) = func(a)
}

class TuplePipeExtension[A,B](a:(A,B)) {
def |>[C](func:(A,B) => C) = func(a._1, a._2)
}

class TriplePipeExtension[A,B,C](a:(A,B,C)) {
def |>[D](func:(A,B,C) => D) = func(a._1, a._2, a._3)
}

class QuadruplePipeExtension[A,B,C,D](a:(A,B,C,D)) {
def |>[E](func:(A,B,C,D) => E) = func(a._1, a._2, a._3, a._4)
}

class QuintuplePipeExtension[A,B,C,D,E](a:(A,B,C,D,E)) {
def |>[F](func:(A,B,C,D,E) => F) = func(a._1, a._2, a._3, a._4, a._5)
}

object ImplicitPipe {
implicit def pipe[A](a: A) = new PipeExtension[A](a)
implicit def tuplePipe[A,B](a:(A,B)) = new TuplePipeExtension[A,B](a)
implicit def triplePipe[A,B,C](a:(A,B,C)) = new TriplePipeExtension[A,B,C](a)
implicit def quadruplePipe[A,B,C,D](a:(A,B,C,D)) = new QuadruplePipeExtension[A,B,C,D](a)
implicit def quintuplePipe[A,B,C,D,E](a:(A,B,C,D,E)) = new QuintuplePipeExtension[A,B,C,D,E](a)
}