Infix operator with extra optional arguments

Hi,

Assuming I have some thing like this:

julia> struct AA
       val
       end

julia> struct Composed
       members
       config
       end

Is there any acceptable way to enable something like this:

julia> Base.:+(a::AA...;config="default") = Composed(collect(a), config)

julia> AA(1) + AA(2) + AA(3)
Composed(AA[AA(1), AA(2), AA(3)], "default")

while still allowing to set the config attribute to something else than default? For example:

AA(1) + AA(2) + AA(3)  <magic op> config = "new"

or

"new" <magic op> AA(1) + AA(2) + AA(3)

The motivation is only to provide syntatic sugar. It might also give some context that the creation of the real Composed is a bit messy even though it is straight forward (which is why the sugar is desirable).

The extra parameters are config-type parameters so one possible solution is to have a global config variable or an overloadable method (I guess this is basically the same) which returns the wanted config, but this has the obvious drawbacks of global state.

One option could be to just give a curry function and let users define their own composition op should they want to:

julia> compose_from(config) = (a...) -> Composed(collect(a), config)
compose_from (generic function with 1 method)

julia> + = compose_from("test")
#13 (generic function with 1 method)

julia> AA(1) + AA(2) + AA(3)
Composed(AA[AA(1), AA(2), AA(3)], "test")

Could metaprogramming do something nicer here? I’m very inexperienced with it so to me it just looks like it would do the curry solution above but using strings.

Macros (which operate on the AST, not strings) could help here. Eg one can think of a syntax

@withdefaults (+ => (config = "default", )) begin
    AA(1) + AA(2) + AA(3)
end

where you walk the expression in the body and replace the funcalls.

That said, I would try to find another way, eg have config be a property of the operand and let it propagate, as in

AA(1; config = "default") + AA(2) + AA(3)

@Tamas_Papp Thanks alot!

The propagate idea looks pretty nice. My AAs may be created outside the scope of where I want to compose them, but I guess I can just create a new type for this purpose or just use a (named) tuple:

(a = AA(1), config="new") + AA(2) + AA(3) 

There might be even nicer looking permutations of the above possible (e.g <magic op> is a function which returns the type/tuple). Don’t know why I didn’t think of that. Thanks for opening my eyes.

I might try the macro way as well just as an exercise as it feels like I’m missing out on something there (won’t see the nails as nails if I don’t know what a hammer looks like or something).

Frankly, I would just create a wrapper type.

I am not sure. I was a heavy user of macros in Common Lisp, but write them very rarely in Julia; dynamic dispatch and zero cost abstraction are very powerful, and much more transparent and easier to implement/maintain.

Depending on what your want to have in your expressions (only additions or perhaps other operators) and your willingness to hijack another operator with stronger priority (such as |), you might have more or less the syntax you wanted with something like this:

struct AA
    val
end

struct Composed
    members
    config
end

Base.:+(aa::AA...; config="default")  = Composed(collect(aa), config)

Base.:+(c::Composed, aa::AA...) = +(c.members..., aa...; config=c.config)
Base.:|(config, aa::AA) = Composed([aa], config)
julia> AA(1) + AA(2) + AA(3)
Composed(AA[AA(1), AA(2), AA(3)], "default")

julia> "new" | AA(1)
Composed(AA[AA(1)], "new")

julia> "new" | AA(1) + AA(2) + AA(3)
Composed(AA[AA(1), AA(2), AA(3)], "new")

I don’t know about the context of this, therefore I don’t know whether I would really advise you to implement such a solution. Hijacking the | operator might cause subtle bugs if someone uses your AA type in a generic algorithm expecting | to be implemented and behave like a bitwise or.

Yes. On the other hand, beware that macros are a pretty big hammer, and everything might look like a nail :smiley:

Thanks,

I settled for just having

Base.:+(aa::AA...) = +(DefaultConfig(), aa...)
Base.:+(config::Config, aa::AA...) = Composed([aa...], config)

and a feeling of shame and embarassment for not realizing it was that simple.

If it was funny I’d make a meme like this:
Language: Operators are just functions and you can define them anyway you like.
Me: Got it. Super!
Code: a+b == +(a,b)
Me: Impossible to have a of different type than b!!

1 Like

How about locally defining an operator?

julia> let + = (args...) -> +(args...; config="new")
           AA(1) + AA(2) + AA(3)
       end
Composed(AA[AA(1), AA(2), AA(3)], "new")

You can also create a convenience function like withkw(op; kwargs...) = (args...) -> op(args...; kwargs...) then do let + = withkw(+, config="new"); ... end.

3 Likes