Syntax Proposal - "And-then-style" function-chain call

I really appreciate decreasing braces quantity in Scala, this way, I want to suggest some extension to Scala standard library.

When we have a list, it is easy to define function chain, in a way we speaking on human languages.

val res = list
  .map(_ + 1)
  .filter(_ > 0)

It is read literally: “get a list, and then add 1 to every element, and then take elements, which are more than 0”.

We can do the same with function construction too.

val f = 
  f1 andThen
  f2 andThen
  f3

But when we call a function chain, we cannot write it in “and-then” style. We have to write it like

val res = f3(f2(f1(x)))

The problems are:

  1. Too many braces in a long call-chain.
  2. It is hard to read (even if we split it on several lines).
  3. If we’ll split it on several lines, we’ll get many visual nesting levels.
  4. Moreover, it is in reversed order, comparing with order of our speaking and thinking.

We don’t say: “Do f3 with thing, that we’ll get after calling f2 with thing, that we get after…”. We say, instead: “Get x, and then do f1 with that, and then do f2 with result, and then…”.

Wolfram language has nice construction, like “andThen”, but for function chain calling instead of function construction.

It is “//”. So, someone, instead of

f3(f2(f1(x))))

can write

x // f1 // f2 // f3

It is in order of our natural speaking and thinking, and very easy to read and to edit. And have much less braces.

For adding the same to Scala 3, we need only one line.

extension [T](x: T) inline def \\[R](f: T => R) = f(x)

And it will work with all types without any overhead. And even, Tuple types will be automatically transformed to function’ multiple arguments. So, we can write not only

x \\ f1 \\ f2 \\ f3

but

list .
  map(_ * 2) .
  sum \\
  (_ + 1) \\
  println

or

def xyz(x: Int, y: Int, z: Int) = (x, y + z)

(1, 2, 3) \\
xyz \\
(_ + _) \\
factorial

We even can define logging function, like

def echo[T](x: T) = {println(x); x}

and easily add temporary log inside long chain-evaluation:

val res = 2 \\
  (_ * 10) \\
  echo \\
  (_ / 2)

So, I suggest to add this very useful one line function to Predef:

extension [T](x: T) inline def \\[R](f: T => R) = f(x)

And, for symmetry, add

extension [T, R](f: T => R) inline def @:(x: T) = f(x)

…so someone can write function call chain without braces, like

f3 @: f2 @: f1 @: x

It already exists. There are 2 functions pipe and tap defined in scala.util.chaining.

2 Likes

Probably, it is hard to find them, when they are in “util.chaining” and not in Predef, so, I’ve never seen their usage in examples or books, and can’t imagine, where to search them.

And, probably, their letter-names make some visual noise, and need to have some symbolic pseudonyms, like “\\”.

(1, 2, 3)  pipe
 xyz       pipe
 (_ + _)   pipe
 factorial pipe
 println

is harder to read for me, than

(1, 2, 3)   \\
  xyz       \\
  (_ + _)   \\
  factorial \\
  println

Moreover, in today implementation “pipe” surely has overhead. “Extension-inline” implementation, on the other hand, has not.

But I’m glad, that one of them is already here.

Maybe, “@:” analog is here too, but I don’t see it in “ChainingOps” (“tap” is not analog, it is more universal version of “echo” in my example).

I prefer the plain old chaining of map, but pipe at least gives me a clue as to what’s going on. \, on the other hand, is utterly mysterious.

In my opinion, syntax that makes code completely unreadable for programmers used to other (mainstream) languages should be considered very carefully.

5 Likes

“map” is for containers, but I talk about how implements the same for all entities, because I love container’s “linear” syntax and want to use it everywhere.

list map (_ + 1) map (_ * 10)

10 pipe (_ + 1) pipe (_ * 10)

or

10 \\ (_ + 1) \\ (_ * 10)

“\\” is not the best symbolic name, I chose it for example, only because Wolfram’s one has name of “//”.

Probably, something like |> would be better. It looks Ok:

x |> f |> g

or

val res = 2
  |> (_ * 10)
  |> echo
  |> (_ / 2)

Intelij Idea shows “|>” as an arrow-like triangle.

x // f1 // f2 // f3 (from older post)

Can’t we use something like function composition in maths ? An operator like f ∘ g. Akka actually uses the ~> operator for its stream system. Maybe we can use it for functions.

Example:

val finalFunction = f ~> g ~> h //means f(g(h(_)))

Intelij Idea shows “|>” as an arrow-like triangle.

I think we shouldn’t consider IDE’s font as a plus. What if the user uses another IDE ? What if the changes his font ? etc…

1 Like

Since you mentioned |>, Elm has such operator with exactly the same meaning and the symmetric <| one: (quoting from docs/syntax)

In addition to the normal math operations for addition and subtraction, we have the (<|) and (|>) operators. They are aliases for function application, allowing you to write fewer parentheses.

viewNames1 names =
  String.join ", " (List.sort names)

viewNames2 names =
  names
    |> List.sort
    |> String.join ", "

-- (arg |> func) is the same as (func arg)
-- Just keep repeating that transformation!

Historical note: this is borrowed from F#, inspired by Unix pipes.

Relatedly, (<<) and (>>) are function composition operators.

1 Like

|> is the de-facto piping operator, since it still looks like a nice arrow without ligatures. Even then, I think most people do use fonts with them

2 Likes

I think, nowdays, if symbol is good looking as itself, IDEs support for it is a plus. Surely, IDE is not a rare thing, but rather mainstream, now.

Nice. Id didn’t know about it. Just tried some symbol combinations.

1 Like

That looks like Fira Code, not IntelliJ IDEA’s font Jetbrains Mono

1 Like

JetBrains Mono also supports this ligature

1 Like

Yes, I just wanted to say that it isn’t the font we were talking about just before that message

It actually doesn’t:
image

Edit: I’m in IDEA’s default font (Jetbrains Mono)

Ligatures are optional, but you can turn them on: https://www.jetbrains.com/lp/mono/#ligatures

2 Likes

Ok but we go back to the first problem. Altrough I find this symbol cool, what if the user uses another IDE ? Or another font ? Or even if he uses IDEA with JB Mono, what if he doesn’t uses ligature ?.

It still looks like an arrow/triangle

4 Likes

I think font ligatures can’t be a proper argument for this operator, it’s nice to have, but just that.

The argument here is that there are at least two other (FP) languages supporting this operator with the same semantics. (and this is why this ligature exists in the fonts)

4 Likes

Yes but I mean I don’t think a single (even the most popular) IDE’s default font has something to do with operator choice.

But this is a good argument for the |> operator.

Or just continue using . and extension methods. Even though I use F# daily, I think ship has already sailed with Scala. You also don’t get things like auto completion with the chaining style of things. Its bit of a foreign change to be introducing this style of code imho.

extension [A](a: A) def through(f: A => R) = f(a)

list
  .map(_ * 2)
  .through(sum)
  .through(_ + 1)
  .through(println)
1 Like