Combining functors, broadcasting, and pipes

I’ve got another behavior I don’t quite understand. Here’s an MWE:

begin
	struct MyStruct
		x
	end
	function(ms::MyStruct)(y)
		ms.x*y
	end
end

ms1=MyStruct(2)

ms1(2) # works

2 |> ms1 # works

ms1.((3,4)) # works

broadcast(ms1, (3,4)) # works

(3,4) .|> ms1 # does not work (throws MethodError: no method matching length(::MyStruct) ...

(3,4) .|> (ms1,) # works

(3,4) .|> x->ms1(x) # [EDIT: this works too]

I don’t understand why the .|> approach requires that ms1 be wrapped in a tuple as if it were an array, tuple, or other iterable.

P.S. This came about because I’m playing with ray transfer matrices and paraxial raytracing and tried to build a construct like

ray1 |> distance1 |> lens1 |> distance2

which worked great for a single ray, but broke down when I tried to do

@. rays  |> distance1 |> lens1 |> distance2

Thanks!

I think the infix syntax is a bit of a distraction here. If you define a new function:

julia> function pipe(x, f)
         x |> f
       end
pipe (generic function with 1 method)

then I hope you’ll agree that:

(3, 4) .|> ms1

should be equivalent to:

pipe.((3, 4), ms1)

When written without the |> infix, I think it becomes more obvious why this doesn’t work: you are broadcasting over both the input (3, 4) and your struct ms1 itself. Julia doesn’t know how to iterate over a MyStruct, so this broadcast fails.

Wrapping ms1 in a 1-element tuple fixes the issue because you are now broadcasting over that tuple, which works just fine. Ref(ms1) would also work, and is a bit more idiomatic in Julia.

You can tell Julia to treat your struct as a scalar in broadcasting operations with:

Base.broadcastable(s::MyStruct) = Ref(s)

at which point all of your examples should now work as intended:

julia> (3,4) .|> ms1
(6, 8)
6 Likes

Thank you, that absolutely works and is what I needed.

EDIT: Also, thanks for the explanation. After a bit of reflection, I think some of my confusion came from thinking, in the context of pipes, of the functor as a function. Functions are not broadcasted over (they are protected by Base.broadcastable in the same way you advised I do for my struct (see https://github.com/JuliaLang/julia/blob/539f3ce943f59dec8aff3f2238b083f1b27f41e5/base/broadcast.jl#L678), but a functor is not a function (specifically isa(ms1, Function) == false) even though it has a method.

1 Like