Parse pipe operator `!>` for side effect calls

I’m hoping the parser could parse !> as an operator similar to the pipe operator, but it will not be defined in Base by default instead it can be used by package developers if they want to show some of their callable objects has side effect explicitly while enabling users to use pip operator.

Although it is not possible to control and infer side effects for Julia compiler, it is always possible to show this explicitly by API design. Currently, we only have a pip operator that works for any callable object. I think this is also consistent with our current API design (by naming functions with side effects with ! in the end), so it is probably something missed for pipe operators.

In my use case, due to the quantum non-cloning theorem, all the operations on a quantum computer have to be in-place on quantum registers since we are not allowed to copy the register state, however, pipe operator is something perfect for representing a chain of quantum gates (some linear operators) to the register, but |> is not something very elegant since it cannot show the call chain are all in place functions, I’d like to have something like the following

!>(x, y) = apply!(x, y)

r !> H !> H !> H

Any comments?


update: I posted a PR since it’s quite simple, RFC: add !> pipe-like operator by Roger-luo · Pull Request #35770 · JuliaLang/julia · GitHub

1 Like

I’m not sure I understand the need for a special syntax: Given in-place functions that modify and return their argument, be them f! and g!, couldn’t one just do

x |> f! |> g!

?

3 Likes

Yeah, it’d be nice to have more pipe-like operators. See also

https://github.com/JuliaLang/julia/pull/34340

1 Like

however this is not true for a callable object with side effects, e.g

struct MyCallable
end

# this is not good if it actually has side effect
# (::MyCallable)(x) = # blabla mutates x

# if we overload !>, then we can have instead of overloading the callable method
Base.:(!>)(x, ::MyCallable) = # blabla mutates x

# the name can be arbitrary
foo = MyCallable()

x !> foo # explicit calls !>
x |> foo # this will error since (::MyCallable)(x) is not implemented

How so? I think f! and g! can have all the side effects they want. My suggestion is to try and stick to standard tools as much as possible, unless your needs (which I may have misunderstood) are not fulfilled by those, of course.

1 Like

I think I’ve explained the use case. I don’t know what you mean by “standard tools”. There is no side effect analysis tool in Julia and Julia compiler cannot provide side effect information by design.

Please take a look at my example, I cannot make my users to use f! as their name by force, this is just not possible, as a developer I need to provide a way to make user aware of the side effect explicitly by API design rather than asking my users to change their variable name like following

foo! = MyCallable()

this is not what we meant to do here. If you mean a more concrete use case, every circuit object in Yao.YaoBlocks will have side effect (as I already mentioned above).

I’m not talking about in-place functions, I’m talking about in-place callables (or say in-place pipe), again if you take a look at the example, this is not possible to make this explicit by name, because it is not defined by developers. overloading a pipe operator like !> will be the only option to make this API explicit on side effect.

An example of how it is handled is the Base functions that handle IO objects.

read, readline, readuntil etc. aren’t suffixed with !. Why? Because side effects with IO objects are the expected behavior (there is read!(::IO, ::AbstractArray), however, where ! means the modification of the array).

Likewise, a user dealing with quantum computations must be aware of the non-cloning theorem, and additional indication of side effects isn’t necessary.

However, the thing is tho the user should be aware of non-cloning theorem but for simulation this is not always true (since we are simulating), they could use copy(register) as well. If we could have |> and !> then we could split things more explicitly and let |> do the copy version for simulation and !> do the inplace version for performance.

If one want’s to get the expectation in simulation the register has to be copied or it will need to execute again.

And the operators are actually mutating the array. Thus I think a more explicit API would help a lot.

And IIUC, the IO objects don’t use pipe operator very often, do they?

Trying to ensure I get the meat of this proposal. Is it correct to say that adding this to base would add unique available syntax but wouldn’t actually create any new corresponding methods?

Yes it only changes that parser, and the package developers would decide whether to use it.

I think switching pure/mutation behavior based on the pipe operator is an interesting usecase.

Having said that, I don’t think

is a convincing motivation. Ending function names with ! is just a convention for programmers. As you said, it also breaks easily whenever functions are assigned to local variables, especially in higher-order functions. For example, the implementation of Base.foreach does not use f!:

and it doesn’t make sense to do it. f may mutate something or just only do I/O. Another example is the callable pop! ∘ pop! which mutates the input but it is entirely up to the user how it is called.

I could see this being a problem:

x! = 1
x!>0
2 Likes

I would be more concerned of !> already having a meaning as a prefix operator (not greater than).

@Roger-luo is there a specific reason why you don’t want to use a Unicode symbol that is parsed as function in infix position (like )?

1 Like

Yeah I agree. What I mean here is about objects not functions. Objects can use pipe operators as part of their API instead of defining callables.

First one should not use ! convention when creating the instance, this is because only when this object is fed into the method !> (Or whatever other method), the mutation happens, thus the mutation belongs to the pipe operator not the object itself. It doesn’t make sense to ask users to follow this convention when creating the instance. But it will make sense for developers to overload such a inplace pipe operator.

As I’m trying to point out above and even at the beginning, this is not about functions. The inplace pipe operator only provides an option for package developers to follow the ! convention which is consistent with other Julia functions.

In other words, if you treat |> as a function name, there should be a valid function name that can use the side effect convention (no matter it is !> Or |!>)

1 Like

I think the reason why we are not using unicode operators is the same as other pipe-like operators proposal: this operator is heavily across the whole interface, using a unicode operator loses the original purpose of convenience. And yes I think currently we have no choice but use a unicode operator if we want this to be more explicit.

Yeah this makes me think @tkf proposal on |!> is probably better.

I think I might get people confused on the original description. I’m trying to propose a pipe operator, but the thing gets piped doesn’t necessarily need to be a callable.

The mutation behavior is only correlated with the method implemented with this operator, thus one cannot and should not ask users to follow the mutation convention for their variable name when creating the instance.

I think this is a very natural adjoint of current pipe operator for developers who wants to emphasize the mutation behaviour in their API.

1 Like

Functions are objects and every object can potentially be a callable. I don’t think it’s a meaningful distinction for this discussion. For example,

!>(x, ::typeof(somefunction)) = somefunction!(x)

may make sense in some situations even if somefunction isa Function.

Note that this approach doesn’t work always. For example,

struct Composition{F, G}
    f::F
    g::F
end

(c::Composition)(x) = c.f(c.g(x))

∘(f, g) = Composition(f, g)

Then pp = pop! ∘ pop! is an “object” with its call overload mutates the input (and the first thing in the input). What I’m saying is: It is OK to write callables that mutate input.

But again, I do agree that switching pure and mutable implementations using |> and !> is an interesting approach and likely very useful.

Yeah I agree with your example here. I probably didn’t make the description clear and make my points on callables by mistake.

What I’d like here is to have the option that let developer decide if they want to emphasize that piping the object they defined will mutates the input.

But it doesn’t have to be a callable. It’s just because we only have one pipe operator, we have to make it a callable.

For the composition example, I think it’s still under the general Function semantic.

What I’m thinking is more like a subset of Function. let’s say in-place linear operators (like those defined in LinearMaps.jl). These operators will always mutates the input vector by default (for performance consideration)

Even the composition of them will have the semantic of mutation (via a new composite struct). Thus under this context, it’d make more sense to have explicit in place pipe operator.

This is actually exactly what happened in Yao. Non of the blocks are callables, one will need to use an explicit function apply! to perform them on the register, which conceptually is a matrix vector product.

We overload pipe operator to apply! to make it convenient to apply a chain of such blocks. But if let’s say one wants to cache some results in the middle of a simulation process, they need to copy this large vector.

1 Like

OK, I guess we’ve been on the same page from the beginning.

Anyway, let’s hope we can get the new pipe operators…

1 Like