Does anybody use the syntax `a <operator> b = ...` to define new methods for operators?

I know that the julia docs mentions that operators are just regular functions that have a special syntax, so the expressions a <operator> b and <operator>(a, b) are equivalent. What i did not realized, is that we can use the former in a function definition, for example, a..b = a:b-1. My initial thought was that this syntax would be invalid, but i was wrong. I was amazed because i think this syntax is very intuitive and more close to how we define operators in math.

julia> a..b = a:b-1
.. (generic function with 1 method)

julia> 1..4
1:3

julia> import Base: *

julia> x::Symbol * y::Symbol = Symbol("$x" * "$y")
* (generic function with 368 methods)

julia> :foo * :bar
:foobar

I searched the docs to find any mention of defining methods for operators using this syntax, but i did not find anything, so my guess is that most users maybe unaware of this and is not commonly used.

2 Likes

Personally I hate it and never use it because I prefer functions to be unambiguously written, and = looks too much like assignment. I try to use function () end as much as possible and use ()-> when it’s just shorter. I would be happier if I could write fn } as shorthand of function end in the future, an explicit end of a block expression doesn’t bother me and sometimes saves parenthesizing.

This is more a personal quirk, though, this syntax doesn’t seem like a big deal. The only problematic typo I’ve seen is _ being mistyped as -, so a_b=1 turns into a-b=1 (ack!). a..b == a:b-1 could also be mistyped as a..b = a:b-1, but I’m sure people generally know not to.

1 Like

I’m more of a math person, so it makes sense to me that operators can be defined this way, because is very close to how we define them in math. Of course some times this can be bad for code readability, and using an unambiguous code style (like defining functions with only function () end) is preferred for big projects.

I see your point about it looking like math, but that feature is limited to binary operations on variables. For example, the parser can’t figure out x^y + 3 = blahblah(x, y, z). When I first heard about Julia I saw a Ted talk where it was claimed the code can look like math, along with a graphic of an expression with an integral and a gradient. I learned quickly it was just a weirdly named method rather than separate integration and gradient operators, and I swung back towards a “no fancy stuff” style, maybe a bit too much.

To be fair, the expression x^y + 3 = blahblah(x, y, z) makes more sense as an equation than a operator definition, because i don’t think would be valid neither in math or julia to define a binary operator where the left-hand expression is not a binary operator expression, like x^y. Of course it would be totally valid as an equation.

What are the rules for what sorts of characters/strings can be used as operators in this sense?

julia> acb = a * b
ERROR: UndefVarError: a not defined
Stacktrace:
 [1] top-level scope
   @ REPL[1]:1

julia> (a)c(b) = a * b
ERROR: syntax: "c(b)" is not a valid function argument name around REPL[2]:1
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

julia> a.c.b = a * b
ERROR: UndefVarError: a not defined
Stacktrace:
 [1] top-level scope
   @ REPL[4]:1

Edit: a few more experiments:

julia> a⨥b = a .+ b
⨥ (generic function with 1 method)

julia> a⌣b = a * abs(b)
ERROR: UndefVarError: b not defined
Stacktrace:
 [1] top-level scope
   @ REPL[7]:1

julia> a…b = a:0.1:b
… (generic function with 1 method)

Edit2:
I guess it’s only those that are listed here: julia/julia-parser.scm at master · JuliaLang/julia · GitHub
Note that many of those operators aren’t defined at all in base, but I assume the developers list them here so that if someone does define them, the operator precedence rules will be consistent with mathematical practice.

I don’t know of a documented list of operators, but they’re rooted in julia-parser.scm, similar operators will be grouped together. The groups do have differences; they may be parsed differently and the precedence rules among the groups are set in stone. You can also modify these operators to make new ones. I don’t do this though, it’s so hard to type.

I pretty sure it will only works for expressions that are parsed as a call expression, for example:

julia> Meta.@dump x * y
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol *
    2: Symbol x
    3: Symbol y

julia> Meta.@dump x.y
Expr
  head: Symbol .
  args: Array{Any}((2,))
    1: Symbol x
    2: QuoteNode
      value: Symbol y

As you can see, it doesn’t work with something like the . operator because is specially parsed. One limitation that i thinked of was the use of where, but you can still use it with this syntax if you wrap the expression with parenthesis:

julia> x::T ± y::T where {T <: Number} = (x + y, x - y)
ERROR: UndefVarError: T not defined
Stacktrace:
 [1] top-level scope
   @ REPL[16]:1

julia> (x::T ± y::T) where {T <: Number} = (x + y, x - y)
± (generic function with 1 method)

julia> 2 ± 3
(5, -1)
1 Like

I have never used it because I believe Base.:(*) better highlights the intent that I am modifying a function from Base. With a * b I am restricted to importing before defining because a Base.:(*) b does not work :man_shrugging:

1 Like