Operator member syntax

Hi folks,

Currently, the only way to directly reference an overloaded operator is by providing a contextual type, which may be verbose:

// Explicit contextual type is verbose.
_ = (+) as (Float, Float) -> Float
_ = (+) as (Float) -> Float

// No verbosity if type can be inferred, but inference isn't always possible.
[1, 2, 3].reduce(0, +)

How do folks feel about supporting operator member syntax, just like references to other static members? It's currently not supported, but I wonder if it's just a parser deficiency:

_ = Float.+
_ = Float.(+)
_ = Float.`+`
test.swift:1:10: error: use of unresolved operator '.+'
_ = Float.+
         ^~
test.swift:2:11: error: expected member name following '.'
_ = Float.(+)
          ^
test.swift:3:11: error: expected member name following '.'
_ = Float.`+`
          ^

Requiring backticks around the operator name may be nice to prevent confusion for operators that begin with a leading ., like SIMD.(.<):

_ = Float.`+`
_ = SIMD2<Float>.`.<`
_ = SIMD2<Float>.`.<(_:_:)`

Sorry if this topic has been discussed previously.

I did a search and found results for module-qualified names, which seems orthogonal and doesn't address this kind of ambiguity (e.g. the Swift module defines multiple + operators):

14 Likes

Context: operator member syntax popped up in a discussion regarding the @derivative(of:) and @transpose(of:) attributes for differentiable programming.

These attributes reference a declaration, which may be an operator:

@derivative(of: Float.+)
func addDerivative(lhs: Float, rhs: Float) -> (value: Float, differential: (Float, Float) -> Float) { … }

In practice, qualified operator member syntax isn't necessary for these attributes. Type-checking for @derivative(of:) and @transpose(of:) currently require the operator to be defined in the same type context, where lookup is not ambiguous:

extension Float {
    @derivative(of: +(_:_:))
    static func addDerivative(lhs: Float, rhs: Float) -> (value: Float, differential: (Float, Float) -> Float) {
        (lhs + rhs, { $0 + $1 })
    }
}

But this motivated the discussion nonetheless!

1 Like

It doesn’t seem to extend very far. If there are multiple overloads; like SIMD, prefix/suffix variants; then it’s already back to being ambiguous.

There are also old operators that are declared as global functions, which would dodge this treatment.

It's fair that ambiguity is sometimes unavoidable.

I do think that operator member syntax enables an easy way to refer to unambiguous member operators. This is a big convenience for (common?) cases, like Float.+(_:_:) instead of (+) as (Float, Float) -> Float).

What it'll be if the arguments' types are different, like +(_: Date, _: TimeInterval) -> Date or +(_: Tensor<Float>, _: Float) -> Tensor<Float>?

It looks SIMD.(.<) is this case.
(Or what you intended to link is this?: .<(_:_:) | Apple Developer Documentation)

+1. This seems like something we should "obviously" allow.

I'm also in favour of removing the magic surrounding operators more broadly (e.g. I also think they should be allowed as instance members rather than only as static members - like callAsFunction [operator ()] and dynamicMemberLookup [operator .] may be).

Otherwise, edge-cases like yours emerge where you'd like to treat an operator simply as a function with a weird name, and it becomes frustratingly difficult to do so. I've hit these kind of issues in the past as well.

2 Likes

If a member operator is overloaded for the same parameter arity and parameter labels, then direct references to it with no contextual type would be truly ambiguous.

However, this is no different than overloaded non-operator members:

struct S {
  func foo(_ x: Int) -> Int { x }
  func foo(_ x: Float) -> Float { x }
}

_ = S.foo
$ swift overload.swift
overload.swift:6:5: error: ambiguous use of 'foo'
_ = S.foo
    ^
overload.swift:2:8: note: found this candidate
  func foo(_ x: Int) -> Int { x }
       ^
overload.swift:3:8: note: found this candidate
  func foo(_ x: Float) -> Float { x }
       ^

Operator member syntax doesn't solve ambiguity issues. It just provides a way to reference member operators.

5 Likes

That was indeed the intended link, thanks!

Since SIMD.`.<` has three overloads:

The following should be ambiguous without a contextual type:

_ = SIMD.`.<`
2 Likes

The qualified operator lookup syntax does not solve the overload ambiguity problem. Qualified name lookup for non-operators does not solve that problem either. Thus I think overload ambiguity is almost entirely orthogonal to the operator member syntax proposed here.

There are two reasons qualified operator lookup is desirable:

  1. This always seems like a missing feature, because qualified reference to a static non-operator member is already possible.

    let f = Float.maximum // (Float, Float) -> Float
    let g = Float.+ // error, but why not?
    
  2. It improves clarity for potential language features that retroactively provide a behavior to an existing method. This includes dynamic replacement and differentiable programming.

    @dynamicReplacement(for: Foo.+)
    func +(_: Foo, _: Foo) -> Foo { ... }
    
    @derivative(of: Foo.+)
    func derivativeOfAdd(_: Foo, _: Foo) -> (value: Foo, differential: (Foo, Foo) -> Foo) { ... }
    
1 Like

It's not what I want to say (It's my sentense which was truly ambiguous).

What I wanted to remark was that how to refer function +(_: A, _: B) -> C in proposed syntax.
Which type has this function as a member (A.+ or B.+ or C.+)?

I think the word member operator is not well defined.

Good question. Operator member syntax is relevant only for operators declared as static methods of a particular type, not for operators declared as top-level functions.

infix operator +++

// This operator is a top-level function, not a member of any type.
func +++(_ x: Int, y: Int) -> Int {
  x + y
}

extension Int {
  // This operator is a static member of `Int`.
  // The discussion involves these kinds of operators.
  static func +++(_ x: Int, y: Int) -> Int {
    x + y
  }
}

// Operator member syntax.
_ = Int.`+++`
2 Likes

Depends on where this operator is defined. If it is a top-level operator, the only possible qualified reference prefix would be the module name. If it is a member of type Foo, then a qualified name can be Foo.+ or ModuleName.Foo.+. In other words, this should be consistent with qualified lookup for non-operators, IMO.

3 Likes

I’ve actually thought for a while that we ought to treat prefix operators like their parameter has the label prefix: and postfix operators like postfix: (names subject to bikeshedding). Then +(prefix:), +(_:_:), and +(postfix:) would be unambiguous.

5 Likes

Yes please!!!

I once asked this question and received this as an answer:

1 Like

Yeah, I think so as well. There's a bug where parsing [>, <] fails and you have to wrap it in parens (i.e. [(>), (<)], so that's what we'd have to do here as well i.e. Float.(+) to access it.