Fill Operator ->

/* 1 */  a->f()              // f is an ident
/* 2 */  a->f
/* 3 */  a->f1->f2
/* 4 */  a->f(b, c)
/* 5 */  a->[dyn.foo]()
/* 6 */  a->[dyn.foo]        // dyn.foo is an expr
/* 7 */  a->f(b, c, ... d)
/* 8 */  a?->f
/* 9 */  a->f.b

compile to

/* 1 */  f(a)
/* 2 */  f(a)
/* 3 */  f2(f1(a))
/* 4 */  f(a, b, c)
/* 5 */  dyn.foo(a)
/* 6 */  dyn.foo(a)
/* 7 */  f(a, b, c, ...d)
/* 8 */  a === null || a === undefined ? a : f(a)
/* 9 */  f(a).b

fill operator is like pipe operator
but its right side is an ident instead of an expression

it look like an extension function
but it doesn't bind this

it just fill the left-hand expression into the first parameter of the function

  • vs pipe operator
    Can be used for all existing functions without currying
  • vs extension function
    No this, can be used in any function, including arrow functions

This sounds like the elixir-style pipeline operator (ReScript implements their pipeline operator like this as well). There was a thread on the pipeline proposal discussing this style. Ultimately, they decided to not go that route. See here (which is too bad, I do like this style of pipeline).

Fill operator and pipe operator should be two things. Both are needed

Why's that? What's a major advantage to using the fill operator, that the current iteration of the pipeline operator does not provide? When you're needing to pipe, how would you personally choose between these two operators?

These are all of your initial examples, but using the pipeline operator. The end result isn't that different.

/* 1 */  a|>f(%)
/* 2 */  a|>f(%)
/* 3 */  a|>f1(%)|>f2(%)
/* 4 */  a|>f(%, b, c)
/* 5 */  a|>dyn.foo(%)
/* 6 */  a|>dyn.foo(%)
/* 7 */  a|>f(b, c, %, d)
/* 8 */  a|>% ?? f(%)
/* 9 */  a|>f(%).b

It's not the equivalence that matters, it's the syntax
pipe need %, it's not convenient to use
and it's easier to hint (autocompletion) using identifiers instead of expressions

Whether it's convenient to use is subjective.

That's not a reason to have two proposals - it's either a reason to change the pipe operator, or it's not.

1 Like
  • Pipe operator is fp style
  • Fill operator is other or maybe more oop

The fill operator is more like an extension function than a pipe

There are many languages with extension functions
  • c#
    string Foo(this string a, string b) => $"{a}{b}";
    
    "a".Foo("b"); // "ab"
    
  • Kotlin
    fun String.foo(b: String): String = "${this}$b";
    
    "a".foo("b"); // "ab"
    
  • Dart
    extension StringFoo on String {
      String foo(String b) => '${this}${b}';
    }
    
    "a".foo("b"); // "ab"
    
  • Swift
    extension String {
      func foo(b: String) -> String {
        "\(self)\(b)"
      }
    }
    
    "a".foo("b"); // "ab"
    
  • Rust
    trait Foo {
      fn foo(&self, b: &Self) -> Self;
    }
    impl Foo for String {
      fn foo(&self, b: &Self) -> Self {
        format!("{}{}", self, b)
      }
    }
    
    "a".foo("b"); // "ab"
    

I didn't know about ReScript before, ReScript does have this operator called pipe
but I don't think this operator is a pipe operator


how would you personally choose between these two operators?

  • Pipe
    When the right side is an expression, or not only function calls
  • Fill
    Extension function, Chain call

Of course I want to use dot . , but it's not possible
so I chose ->, which is a pointer in a c-like language, characterized by the ident on the right

The core of the fill operator is the use ident

For autocompletion, especially typescript
using ident can simply search for functions whose first argument is the type of the left expression
But for pipes, how to search expressions, it cannot autocomplete


a->f

Now that ident is used, we can also drop the parentheses directly
This is similar to extension properties in kotlin

val Int.Foo: Int 
  get() = this + 1

1.Foo // 2
1 Like

OOP already has method chaining.

oooh, I get what you're trying to go for now. The purpose is to be able to "add" functions to classes you do not control, without actually modifying them. So, if you want a to, say, add a "filter" function to the Map class, you can write this filter function, make sure it's in the local scope, then call yourMap->filter(...). This also explains the weird precedence rules you were going for.

So, conceptually, it's pretty different from ReScript's pipe function, but the way you presented it, it has very similar behaviors.

You might be interested in the bind-this proposal, which is means to resurrect some ideas from the extensions proposal. Its primary purpose isn't to help with this specific use case, but its current iteration can be used for this use-case, and I know at least one delegate is rooting for the bind-this proposal primarily for this purpose. It would let you write code such as this:

const filter = function(filterFn) {
  if (!(this instanceof Map)) throw new Error('Receiver must be a Map instance')
  return new Map(this.entries().filter(filterFn))
}

new Map(['myKey', 2])
  ::filter(([key, value]) => value > 1)
  .get('myKey')

The languages listed here are all statically typed languages, where extensions can be looked up and dispatched at compile time. This wouldn't be possible in JavaScript without sufficient syntax to make it unambiguous.

TypeScript's auto-complete for JavaScript could prioritise suggesting identifiers on the right-side of a pipe-operator, even if it is an expression position.