Pitch: Introduce (static) callables

Hi all, @dan-zheng and I wrote a proposal to introduce static callables to Swift. The proposal PR is ready (apple/swift-evolution#1009).


Introduce callables

Introduction

Note: throughout this document, let "call-syntax" refer to the syntax of applying a function to arguments: f(x, y, z).

This proposal introduces callables to Swift. Callables are values that define function-like behavior and can be applied using function application syntax.

In a nutshell, we propose to introduce a new declaration syntax with the keyword call :

struct Adder {
    var base: Int
    call(_ x: Int) -> Int {
        return base + x
    }
}

Values that have a call member can be applied like functions, forwarding arguments to the call member.

let add3 = Adder(base: 3)
add3(10) // => 13

Motivation

Currently, in Swift, only a few kinds of values are syntactically callable:

  • Values with function types.
  • Type names (e.g. T can be called like T(...), which is desugared to T.init(...)).
  • Values with a @dynamicCallable type.

However, call-syntax can also be useful for other values, primarily those that behave like functions. This includes:

  • Values that represent functions: mathematical functions, function expressions, etc.
  • Values that have one main use and want to provide a simple call-syntax interface: neural network layers, parsers, efficient bound closures, etc.

Here are some concrete sources of motivation.

Values representing functions

Values of some nominal types exactly represent functions: in the mathematical sense (a mapping from inputs to outputs), or in the context of programming languages.

Here are some examples:

/// Represents a polynomial function, e.g. `2 + 3x + 4x²`.
struct Polynomial {
    /// Represents the coefficients of the polynomial, starting from power zero.
    let coefficients: [Float]
}

Since these types represent functions, naturally they can be applied to inputs. However, currently in Swift, the "function application" functionality must be defined as a method.

extension Polynomial {
    func evaluated(at input: Float) -> Float {
        var result: Float = 0
        for (i, c) in coefficients.enumerated() {
            result += c * pow(input, Float(i))
        }
        return result
    }
}

let polynomial = Polynomial(coefficients: [2, 3, 4])
print(polynomial.evaluated(at: 2)) // => 24

The mathematical notation for function application is simply output = f(input). Using subscript methods achieve a similar application syntax f[x], but subscripts and square brackets typically connote "indexing into a collection", which is not the behavior here.

extension Polynomial {
    subscript(input: Float) -> Float {
        ...
    }
}
let polynomial = Polynomial(coefficients: [2, 3, 4])
// Subscript syntax, may be confusing.
print(polynomial[2]) // => 24

The proposed feature enables the same call syntax as the mathematical notation:

extension Polynomial {
    call(_ input: Float) -> Float {
        ...
    }
}
let polynomial = Polynomial(coefficients: [2, 3, 4])
// Callable syntax.
print(polynomial(2)) // => 24

Bound closures

Variable-capturing closures can be modeled explicitly as structs that store the bound variables. This representation is more performant and avoids the type-erasure of closure contexts.

// Represents a nullary function capturing a value of type `T`.
struct BoundClosure<T> {
  var function: (T) -> Void
  var value: T

  call() { return function(value) }
}

let x = "Hello world!"
let closure = BoundClosure(function: { print($0) }, value: x)
closure() // prints "Hello world!"

A call syntax sugar would enable BoundClosure instances to be applied like normal functions.

Nominal types with one primary method

Some nominal types have a "primary method" that performs their main use. For example: calculators calculate, parsers parse, neural network layers apply to inputs, types representing functions apply to arguments, etc.

For example:

  • Calculators calculate: calculator.calculating(query).
  • Parsers parse: parser.parsing(text).
  • Neural network layers apply to inputs: layer.applied(to: input).
  • Types representing functions apply to arguments: function.applied(to: arguments).

Types that have a primary method usually call that method frequently. Thus, it may be desirable to sugar applications of the main method with call syntax to reduce noise.

Let’s explore neural network layers and string parsers in detail.

Neural network layers

Machine learning models often represent a function that contains an internal state called "trainable parameters", and the function takes an input and predicts the output. In code, models are often represented as a data structure that stores trainable parameters, and a method that defines the transformation from an input to an output in terms of these trained parameters. Here’s an example:

struct Perceptron {
    var weight: Vector<Float>
    var bias: Float

    func applied(to input: Vector<Float>) -> Float {
        return weight • input + bias
    }
}

Stored properties weight and bias are considered as trainable parameters, and are used to define the transformation from model inputs to model outputs. Models can be trained , during which parameters like weight are updated, thus changing the behavior of applied(to:). When a model is used, the call site looks just like a function call.

let model: Perceptron = ...
let ŷ = model.applied(to: x)

Many deep learning models are composed of layers, or layers of layers. In the definition of those models, repeated calls to applied(to:) significantly complicate the look of the program and reduce the clarity of the resulting code.

struct Model {
    var conv = Conv2D<Float>(filterShape: (5, 5, 3, 6))
    var maxPool = MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    var flatten = Flatten<Float>()
    var dense = Dense<Float>(inputSize: 36 * 6, outputSize: 10)

    func applied(to input: Tensor<Float>) -> Tensor<Float> {
        return dense.applied(to: flatten.applied(to: maxPool.applied(to: conv.applied(to: input))))
    }
}

These repeated calls to applied(to:) harm clarity and makes code less readable. If model could be called like a function, which it mathematically represents, the definition of Model becomes much shorter and more concise. The proposed feature promotes clear usage by omitting needless words.

struct Model {
    var conv = Conv2D<Float>(filterShape: (5, 5, 3, 6))
    var maxPool = MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    var flatten = Flatten<Float>()
    var dense = Dense<Float>(inputSize: 36 * 6, outputSize: 10)

    call(_ input: Tensor<Float>) -> Tensor<Float> {
        // Callable syntax.
        return dense(flatten(maxPool(conv(input))))
    }
}

let model: Model = ...
let ŷ = model(x)

There are more ways to further simplify model definitions, but making models callable like functions is a good first step.

Domain specific languages

DSL constructs like string parsers represent functions from inputs to outputs. Parser combinators are often implemented as higher-order functions operating on parser values, which are themselves data structures—some implementations store closures, while some other efficient implementations store an expression tree. They all have an "apply"-like method that performs an application of the parser (i.e. parsing).

struct Parser<Output> {
    // Stored state...

    func applied(to input: String) throws -> Output {
        // Using the stored state...
    }

    func many() -> Parser<[Output]> { ... }
    func many<T>(separatedBy separator: Parser<T>) -> Parser<[Output]> { ... }
}

When using a parser, one would need to explicitly call applied(to:), but this is a bit cumbersome—the naming this API often repeats the type. Since parsers are like functions, it would be cleaner if the parser itself were callable.

call(_ input: String) throws -> Output {
    // Using the stored state...
}
let sexpParser: Parser<Expression> = …
// Callable syntax.
let sexp = sexpParser("(+ 1 2)")

A static counterpart to @dynamicCallable

SE-0216 introduced user-defined dynamically callable values. In its alternatives considered section, it was requested that we design and implement the "static callable" version of this proposal in conjunction with the dynamic version proposed. See its pitch thread for discussions about "static callables".

Prior art

Many languages offer the call syntax sugar:

Unifying compound types and nominal types

A long term goal with the type system is to unify compound types (e.g. function types and tuple types) and nominal types, to allow compound types to conform to protocols and have members. When function types can have members, it will be most natural for them to have a call member, which can help unify the compiler's type checking rules for call expressions.

Proposed design

We propose to introduce a new keyword call and a new declaration syntax–the call declaration syntax.

struct Adder {
    var base: Int
    call(_ x: Int) -> Int {
        return base + x
    }
}

Values that have a call member can be called like a function, forwarding arguments to the call member.

let add3 = Adder(base: 3)
add3(10) // => 13

Detailed design

call member declarations

call members can be declared in structure types, enumeration types, class types, protocols, and extensions thereof.

A call member declaration is similar to subscript in the following ways:

  • It does not take a name.
  • It must be an instance member of a type.

But it is more similar to a func declaration in that:

  • It does not allow get and set declarations inside the body.
  • When a parameter has a name, it is treated as the argument label.
  • It can throw.
  • It can be referenced directly by name, e.g. foo.call.

The rest of the call declaration grammar and semantics is identical to that of function declarations–same syntax for access level, generics, argument labels, return types, throwing, mutating, where clause, etcs. They can be overloaded based on argument and result types. Attributes that can be applied to function declarations can also be applied to call declarations.

To support source compatibility, call is treated as a keyword only when parsing members of a nominal type. Otherwise, it is treated as a normal identifier. See the source compatibility section below.

call-declaration → call-head generic-parameter-clause? function-signature generic-where-clause? function-body?
call-head → attributes? declaration-modifiers? 'call'

Examples

struct Adder {
    var base: Int

    call(_ x: Int) -> Int {
        return base + x
    }

    call(_ x: Float) -> Float {
        return Float(base) + x
    }

    call<T>(_ x: T, bang: Bool) throws -> T where T: BinaryInteger {
        if bang {
            return T(Int(exactly: x)! + base)
        } else {
            return T(Int(truncatingIfNeeded: x) + base)
        }
    }
   
    // This is a normal function, not a `call` member.
    func call(x: Int) {}
}

Call expressions

When type-checking a call expression, the type checker will try to resolve the callee. Currently, the callee can be a value with a function type, a type name, or a value of a @dynamicCallable type. This proposal adds a fourth kind of a callee: a value with a matching call member.

let add1 = Adder(base: 1)
add1(2) // => 3
try add1(4, bang: true) // => 5

When type-checking fails, error messages look like those for function calls. When there is ambiguity, the compiler will show relevant call member candidates.

add1("foo")
// error: cannot invoke ‘add1’ with an argument list of type '(String)'
// note: overloads for 'call' exist with these partially matching parameter lists: (Float), (Int)
add1(1, 2, 3)
// error: cannot invoke 'add1' with an argument list of type '(Int, Int, Int)'

When the type is also @dynamicCallable

A type can both have call members and be declared with @dynamicCallable . When type-checking a call expression, the type checker will first try to resolve the call to a function or initializer call, then a call member call, and finally a dynamic call.

Direct reference to a call member

Like methods and initializers, a call member can be directly referenced, either through the base name and the contextual type, or through the full name.

let add1 = Adder(base: 1)
let f: (Int) -> Int = add1.call
f(2) // => 3
[1, 2, 3].map(add1.call) // => [2, 3, 4]

When a type has both an instance method named "call" and a call member with the exact same type signature, a redeclaration error is produced.

struct S {
    func call() {}
    call() {}
}
test.swift:3:5: error: invalid redeclaration of 'call()'
    call() {}
    ^
test.swift:2:10: note: 'call()' previously declared here
    func call() {}
         ^

When a type does not have a call member but has an instance method or an instance property named "call", a direct reference to call gets resolved to that member.

struct S {
    var call: Int = 0
}
S().call // resolves to the property

A value cannot be implicitly converted to a function when the destination function type matches the type of the call member.

let h: (Int) -> Int = add1 // error: cannot convert value of type `Adder` to expected type `(Int) -> Int`

Implicit conversions are generally problematic in Swift, and as such we would like to get some experience with this base proposal before considering adding such capability.

Source compatibility

The proposed feature adds a call keyword. Normally, this would require existing identifiers named "call" to be escaped as `call`. However, this would break existing code using call identifiers, e.g. func call.

To maintain source compatibility, we propose making call a contextual keyword: that is, it is a keyword only in declaration contexts and a normal identifier elsewhere (e.g. in expression contexts). This means that func call and call(...) (apply expressions) continue to parse correctly.

Here’s a comprehensive example of parsing call in different contexts:

struct Callable {
    // declaration
    call(_ body: () -> Void) {
        // expression
        call() {}
        // expression
        call {}

        struct U {
            // declaration
            call(x: Int) {}

            // declaration
            call(function: (Int) -> Void) {}

            // error: expression in declaration context
            // expected '(' for 'call' member parameters
            call {}
        }

        let u = U()
        // expression
        u { x in }
    }
}

// expression
call() {}
// expression
call {}

Effect on ABI stability

This proposal is about a syntactic sugar and has no ABI breaking changes.

Effect on API resilience

This proposal is about a syntactic sugar and has no API breaking changes.

Alternatives considered

Alternative ways to denote call-syntax delegate methods

Use unnamed func declarations to mark call-syntax delegate methods

struct Adder {
    var base: Int
    // Option: unnamed `func`.
    func(_ x: Int) -> Int {
        return base + x
    }
    // Option: `call` declaration modifier on unnamed `func` declarations.
    // Makes unnamed `func` less weird and clearly states "call".
    call func(_ x: Int) -> Int { … }
}

This approach represents call-syntax delegate methods as unnamed func declarations instead of creating a new call declaration kind.

One option is to use func(...) without an identifier name. Since the word "call" does not appear, it is less clear that this denotes a call-syntax delegate method. Additionally, it’s not clear how direct references would work: the proposed design of referencing call declarations via foo.call is clear and consistent with the behavior of init declarations.

To make unnamed func(...) less weird, one option is to add a call declaration modifier: call func(...). The word call appears in both this option and the proposed design, clearly conveying "call-syntax delegate method". However, declaration modifiers are currently also treated as keywords, so with both approaches, parser changes to ensure source compatibility are necessary. call func(...) requires additional parser changes to allow func to sometimes not be followed by a name. The authors lean towards call declarations for terseness.

Use an attribute to mark call-syntax delegate methods

struct Adder {
    var base: Int
    @callDelegate
    func addingWithBase(_ x: Int) -> Int {
        return base + x
    }
}

This approach achieves a similar effect as call declarations, except that methods can have a custom name and be directly referenced by that name. This is useful for types that want to make use of the call syntax sugar, but for which the name "call" does not accurately describe the callable functionality.

However, we feel that using a @callableMethod method attribute is more noisy. Introducing a call declaration kind makes the concept of "callables" feel more first-class in the language, just like subscripts. call is to () as subscript is to [].

For reference: other languages with callable functionality typically require call-syntax delegate methods to have a particular name (e.g. def __call__ in Python, def apply in Scala).

Use func with a special name to mark call-syntax delegate methods

struct Adder {
    var base: Int
    // Option: specially-named `func` declarations.
    func _(_ x: Int) -> Int
    func self(_ x: Int) -> Int
}

This approach represents call-syntax delegate methods as func declarations with a special name instead of creating a new call declaration kind. However, such func declarations do not convey "call-syntax delegate method" as clearly as the call keyword.

Also, we want to support direct references to call-syntax delegate methods via foo.call. This makes more sense when call-syntax delegate methods are declared with the call keyword, and is consistent with init declarations and direct references (e.g. foo.init).

Use a type attribute to mark types with call-syntax delegate methods

@staticCallable // alternative name `@callable`; similar to `@dynamicCallable`
struct Adder {
    var base: Int
    // Informal rule: all methods with a particular name (e.g. `func call`) are deemed call-syntax delegate methods.
    func call(_ x: Int) -> Int {
        return base + x
    }
}

We feel this approach is not ideal because:

  • A marker type attribute is not particularly meaningful. The call-syntax delegate methods of a type are what make values of that type callable - a type attribute means nothing by itself. In fact, there’s an edge case that needs to be explicitly handled: if a @staticCallable type defines no call-syntax delegate methods, an error must be produced.
  • The name for call-syntax delegate methods (e.g. func call ) is not first-class in the language, while their call site syntax is.

Use a Callable protocol to represent callable types

// Compiler-known `Callable` marker protocol.
struct Adder: Callable {
    var base: Int
    // Informal rule: all methods with a particular name (e.g. `func call`) are deemed call-syntax delegate methods.
    // `StringInterpolationProtocol` has a similar informal requirement for `func appendInterpolation` methods.
    // https://github.com/apple/swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md#proposed-solution
    func call(_ x: Int) -> Int {
        return base + x
    }
}

We feel this approach is not ideal for the same reasons as the marker type attribute. A marker protocol by itself is not meaningful and the name for call-syntax delegate methods is informal. Additionally, protocols should represent particular semantics, but "callable" behavior has no inherent semantics.

In comparison, call declarations have a formal representation in the language and exactly indicate callable behavior (unlike a marker attribute or protocol).

Property-like call with getter and setter

In C++, operator() can return a reference, which can be used on the left hand side of an assignment expression. This is used by some DSLs such as Halide:

Halide::Func foo;
Halide::Var x, y;
foo(x, y) = x + y;

This can be achieved via Swift’s subscripts, which can have a getter and a setter.

foo[x, y] = x + y

Since the proposed call declaration syntax is like subscript in many ways, it’s in theory possible to allow get and set in a call declaration’s body.

call(x: T) -> U {
    get {
        ...
    }
    set {
        ...
    }
}

However, we do not believe call should behave like a storage accessor like subscript . Instead, call ’s appearance should be as close to function calls as possible. Function call expressions today are not assignable because they can't return an l-value reference, so a call to a call member should not be assignable either.

Static call members

Static call members could in theory look like initializers at the call site.

extension Adder {
    static call(base: Int) -> Int {
        ...
    }
    static call(_ x: Int) -> Int {
        ...
    }
}
Adder(base: 3) // error: ambiguous static member; do you mean `init(base:)` or `call(base:)`?
Adder(3) // okay, returns an `Int`, but it looks really like an initializer that returns an `Adder`.

We believe that the initializer call syntax in Swift is baked tightly into programmers' mental model, and thus do not think overloading that is a good idea.

We could also make it so that static call members can only be called via call expressions on metatypes.

Adder.self(base: 3) // okay

But since this would be an additive feature on top of this proposal and that subscript cannot be static yet, we'd like to defer this feature to future discussions.

Unify callable functionality with @dynamicCallable

Both @dynamicCallable and the proposed call members involve syntactic sugar related to function applications. However, the rules of the sugar are different, making unification difficult. In particular, @dynamicCallable provides a special sugar for argument labels that is crucial for usability.

// Let `PythonObject` be a `@dynamicMemberLookup` type with callable functionality.
let np: PythonObject = ...
// `PythonObject` with `@dynamicCallable.
np.random.randint(-10, 10, dtype: np.float)
// `PythonObject` with `call` members. The empty strings are killer.
np.random.randint(["": -10, "": 10, "dtype": np.float])
35 Likes

We've also had requests from performance-minded Swift programmers to be able to avoid the type-erasure inherent to closures and function types. One way we could realize that would be to allow function types to be used as generic constraints in addition to being used as types:

struct BoundClosure<T, F: (T) -> ()>: () -> () {
  var function: F
  var value: T

  call() { return function(value) }
}

let f = BoundClosure({ print($0), x }) // instantiates BoundClosure<(underlying type of closure), Int>
f() // invokes call on BoundClosure

where such a constraint implicitly requires that a conforming type have a call operator with the same signature. Whereas a function value always has a fixed, indirect layout, a type parameterized on a function constraint like this could capture the function object's layout inline, and allow for efficient "protocol-oriented" composition of function objects, similar to how function objects can compose in D or Rust. This might be an interesting future direction to consider as part of this design space.

19 Likes

Just curious, why there's no way to overload () or [] operators like in C++, but it's recommended to use named funcs in Swift instead, like subscript or now call? Are there any fundamental issues with overloading those two?

2 Likes

It’s really great to see a pitch for static callable! This looks like a very nice design. I only have one question - will it be possible to write static call to make metatypes callable?

This sounds really cool. Since static callable supports generic signatures, would there be a way to support higher-rank constraint signatures?

2 Likes

Metatypes are already callable I believe. They are callable as initializers. A static ‘call’ member would lead to confusion IMO.

@rxwei that’s not as general as the pitched callable feature - it only supports returning values of Self.

One enhancement you should mention as aa future direction is to make the key path types callable. I think that would be relatively uncontroverisal.

1 Like

Metatype values are not directly callable. The Type(arguments) call syntax is special-cased to behave as if Type.init(arguments). You have to explicitly write the .init if you're initializing from a dynamic metatype.

2 Likes

At first blush, this seems plausible. We allow higher-ranked requirements in protocols and classes already. You may however still run into some of the type inference problems of higher-ranked polymorphism if a function were overloaded on different higher-ranked call constraints.

Extending KeyPath types to have a ‘call’ member is a great idea!

Can I add call members in an extension? The use of a custom declaration and no attribute makes me think "yes", but it's worth calling out explicitly.

Do types with call members implicitly convert to function types?

This feature is a syntactic sugar and is purely additive. It guarantees source compatibility.

Since func call is no longer valid, this is a source-breaking change. How do you plan to stage that in?

10 Likes

+1 on the feature from me.

Some comments about the writing:

  • Another motivation is that we have a (long term) goal of unifying structural types and nominal types, to allow structural types to conform to protocols. The natural way to do this is to the structural type be sugar for a named type (just like [Int] is sugar for Array<Int>), so functions will become nominal types at some point (when varargs and a long list of other issues is sorted out, we aren't particularly close to this). When that happens, it would be nice to handle function calls just by virtue of Function having a call member.

  • Grammar thing: "it is uneasy" -> "it isn't easy"

  • Writing: "Machine learning models often represent a function that contains parameters" -> most readers will think that 'parameters' refers to function parameters here, not "learned constants values closed over by the semantic function" :-).

  • " If model could be called like a function, like what it represents mathematically, the code would look much simpler:" -> the key principle we care about here is clarify of the code. I'd recommend pointing out that clarity is really harmed by the lack of this feature.

  • super nit: in sexpParser(“(+ 1 2)”) you have curly quotes going on.

  • Wording, instead of " But it is also very different from subscript in that:" I'd suggest a positive form of "but it is more similar to a func declaration in that:"

  • " A value cannot be implicitly converted to a function when the destination function type matches the type of the call member. While it is possible to lift this restriction, it is beyond the scope of this proposal." -> I'd suggest being more direct and saying something like "implicit conversions are generally problematic in Swift, and as such we would like to get some experience with this base proposal before even considering adding such capability. The explicit support should be enough to unblock any usecase, so that addition is just a sugar"

  • " It guarantees source compatibility." -> This is taking call as a keyword, you should mention that.

Overall, I'm very excited to see this come together, thank you for pushing this forward!

-Chris

8 Likes

In the case of subscript, there is special behavior currently tied to being a subscript, since subscripts are one of the only lvalue forms allowed in the language. I agree that having a special declaration syntax for call is not as directly motivated; I wouldn't be against a design where you write func call(...) with either an attribute or a declared conformance to a function constraint to give you the syntax sugar.

2 Likes

Awesome! In thinking about this further, wouldn't the syntax you posted basically be syntactic sugar for something like this (which should be possible immediately when the proposal is implemented):

protocol Bound {
     associatedtype T
     call (_ value: T)
}
struct BoundClosure<B: Bound> {
  var function: B
  var value: B.T

  call() { return function(value) }
}

let f = BoundClosure({ print($0), x }) // instantiates BoundClosure<(underlying type of closure), Int>
f() // invokes call on BoundClosure

This sounds acceptable.

I wouldn't want to have to have a method called call. Often times there would be a more appropriate name for the method call form. For example, monoids may want to make combine callable.

In some cases it may be desirable to make a method available while also providing the callable sugar. A method attribute would be most convenient in these cases as it would avoid the need to write a forwarding wrapper. On the other hand, a method attribute forces you to choose an arbitrary method name for all callable signatures.

One way to preserve the convenience in the former case without forcing a concrete method name when you only want the member visible as callable would be to use _ to discard the nominal visibility of the method @callable func _ (x: Int, y: Int) -> Int. If we don't cringe at the use of _ here it would give us control over both whether a nominal method is also provided as well as what the name of that method and would save us from having to write forwarding wrappers when we do want both.

I'm not suggesting this would be a better design. The pitched design feels more clear and explicit. But that clarity does come at the cost of writing forwarding wrappers when we also want a nominal method.

How often do we think we'll want to expose both direct calls and nominal methods? How do we feel about having to write forwarding wrappers in those cases?

4 Likes

I feel like this increases the surface area of learning Swift quite heavily, as it's likely to become a (ab)used feature for aesthetic reasons. Maybe I'm just not used to it, but it seems to make following code difficult with a special-case syntax.

Does it actually need a new keyword call rather than just allowing unnamed functions?

struct Adder {
    var base: Int
    func(_ x: Int) -> Int {
        return base + x
    }
}

or using self / Self:

struct Adder {
    var base: Int
    func self(_ x: Int) -> Int { // instance method
        return base + x
    }
    func Self(_ x: Int) -> Int { // class method
        return base + x
    }
}
5 Likes

Without trying to exacerbate the bikeshedding nature of the conversation, I too like the clear and explicit design pitched here. I also like that call for () parallels subscript for [].

Agree, there's a lot to like about it. The only downside I see is forwarding wrappers and I'm not sure yet how significant that is.

One concern with a special decl is that it becomes less obvious how to reference the call operation as a function value. With a func call, object.call would just work, whereas you'd need to use object. ` call ` or something along those lines if it were a special form.

1 Like

Wouldn't callable types implicitly convert to function types? Function types feel like syntactic sugar for a callable existential with a specific call signature and no other members. Concrete types implicitly convert to existentials so it seems aligned with that behavior to implicitly convert callable types to functions.

If we have this behavior it isn't clear to me when it would be necessary or desirable to reference the call function directly.

2 Likes

That's a possibility, but we'd have to evaluate the impact on type checking performance if we allowed this to be implicit.

2 Likes

Makes sense, I was wondering if it would be a type checking issue. :crossed_fingers: