Infix operator

I have defined an infix operator using \parallel and it works for 2 arguments, but I get a strange error when I try for 3 arguments (as infix). Is there a trick to get multiple args to work like 1+2+3+4 … etc.

thanks
note this is not the intended function, just something I put in while working out how to get infix working…

using Printf

(∥)(x, y) = begin
  @printf("X : %s, %s\n", x, y)
  return 2
end
(∥)(x, y, z...) = begin
  @printf("Y : %s, %s, %s\n", x, y, z) 
  return 2 + length(z)
end

@printf("2() = %s\n",  (∥)(1, 2))
@printf("2   = %s\n",  1 ∥ 2)
@printf("3() = %s\n",  (∥)(1, 2, 3))
@printf("3   = %s\n",  1 ∥ 2 ∥ 3 )

the output is

jon@arch:~/julia$ julia args.jl 
X : 1, 2
2() = 2
X : 1, 2
2   = 2
Y : 1, 2, 3, ()
3() = 3
X : 1, 2
ERROR: LoadError: TypeError: non-boolean (Int64) used in boolean context
Stacktrace:
 [1] top-level scope
   @ ~/julia/args.jl:15
in expression starting at /home/jon/julia/args.jl:15

Now that you raise the question, I’m not certain, but I always assumed infix notation always called a method with 2 arguments. So 1 ∥ 2 ∥ 3 would actually do 2 calls in sequence: (1 ∥ 2) ∥ 3. That’s suggested by your last printed line before the error being X : 1, 2 instead of Y : 1, 2, 3, ().

The error is odd, though, (1 ∥ 2) should return a 2, and (2) ∥ 3 should have run. Is it possible that the last ∥ operator was miswritten as the boolean operator ||? That would throw that error if not written with trues and falses.

This is viewed as a comparison, not a function call: https://github.com/JuliaLang/julia/blob/master/src/julia-parser.scm#L16 . And chained comparisons are like &&, they stop early:

julia> Meta.@lower 1 < 2 < 3
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope`
1 ─ %1 = 1 < 2
└──      goto #3 if not %1
2 ─ %3 = 2 < 3
└──      return %3
3 ─      return false
))))

Most ordinary infix operators are parsed pairwise, the exceptions are +,*,++:

julia> :(1 ∥ 2 ∥ 3) |> dump
Expr
  head: Symbol comparison
  args: Array{Any}((5,))
    1: Int64 1
    2: Symbol ∥
    3: Int64 2
    4: Symbol ∥
    5: Int64 3

julia> :(1 < 2 < 3) |> dump
Expr
  head: Symbol comparison
  args: Array{Any}((5,))
    1: Int64 1
    2: Symbol <
    3: Int64 2
    4: Symbol <
    5: Int64 3

julia> :(1 / 2 / 3) |> dump
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol /
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol /
        2: Int64 1
        3: Int64 2
    3: Int64 3

julia> :(1 ++ 2 ++ 3) |> dump
Expr
  head: Symbol call
  args: Array{Any}((4,))
    1: Symbol ++
    2: Int64 1
    3: Int64 2
    4: Int64 3

julia> :(1 || 2 || 3) |> dump
Expr
  head: Symbol ||
  args: Array{Any}((2,))
    1: Int64 1
    2: Expr
      head: Symbol ||
      args: Array{Any}((2,))
        1: Int64 2
        2: Int64 3
3 Likes

OK, thanks macabbot … that explains it.

I tried my example substituting ∥ with ++ … and it worked as I expected.
I tried using ►but that is in some pointer grouping that only works for pairs (I think - it definitely didn’t work).

So the follow up question - is there any documentation about how julia treats different characters in regard to operator preferences other than looking julia-parser.scm?

I must say that just grouping the various “operator” characters to assign a way of interpreting their use independent of how their use is implemented … rather strange. I get that Julia is heavily skewed to “mathematical” use, but I would have thought it was a (general purpose) programming language first.

Jon

So is the error because comparisons are expected to return true/false? If so, how is that rule implemented when the operator may not even have a method defined?

I think this happens before thinking about methods. The parser knows, and e.g. Meta.@lower x ≤ y ∥ z ≢ w shows what this means, before any types are known.

Am not sure, there might be some?

I don’t know that it’s about mathematical skew. Once you have any infix operators it seems you must have precedence rules, which the parser must know. And if you want if 0 <= x < 10 to work, then the parser also needs to know that certain symbols are like that — it’s completely unlike (0<=x)<10, and also can’t be a 3-arg method of <, since they need not match.

These could be mapped to some Base.comparison(0, <=, x, <, 1) instead, that might be more flexible. For instance ifelse(0 <= x < 10, y, z) is a trap as it secretly has a branch, which you have to write ifelse((0 <= x) & (x < 10), y, z) to avoid… it’s possible this hypothetical Base.comparison could specialise so as to avoid that. And what you wanted could be obtained by defining Base.comparison(x, ::typeof(∥), y, ::typeof(∥), z) = .... Edit – no, this won’t work, as 0 <= x < f(y) only evaluates f when necessary. Or rather, it you did this, it would have to work more like Base.literal_pow and only happen for simple cases.

I agree it’s a bit of a puzzle to guess which category a given unicode symbol lives in. Looking at expressions via dump is often the quick way.

1 Like

I’m guessing that these parsing rules are set in stone because it may be overly complicated if users had more control. For example, note that the operator categories determine precedence. If we had control, we’d have to exactly and explicitly specify a new operator’s precedence in the hierarchy. And what if two packages defined different precedences for the same operator? Then the parser would have to adapt to different global scopes.

Looking up languages with custom infix operators, I ran across R and Haskell. R’s all look like %cu$tom_text% and all have equal precedence. Haskell’s has similarly flexible naming, but you explicitly specify precedence (ranked 0 to 9) AND associativity (left, right, non-).

A notable Haskell complaint I read was that many custom infix operators can make for very opaque code, especially if they were wordless. For example, could you tell what 1.1 %~| 2 ~|% "three" does at a glance? Or recall their associativity and precedence to figure out the order of operations?

It seems Julia avoided this problem by not letting people define arbitrary infix operators or their characteristics, a much more conservative approach than R’s. Instead, we can overload a finite list of vaguely math-y characters or suffix them to create new ones that inherit the originals’ categories.

1 Like