Equality of functions

I think there's a reasonable argument to be made that since a function with argument labels can be converted to a "function existential" (i.e. Swift's current function types) such a function should also be able to fulfill a function constraint. However, even if this is allowed it will often be inadvisable to use labels on callAsFunction.

Agreed. In fact, if it is not explicitly disallowed to have argument labels on callAsFunction methods, then we'd run into the following issue with function constraints that ignore labels:

struct Foo {
    func callAsFunction(bar: Int) -> Bool { return bar > 5 }
    func callAsFunction(baz: Int) -> Bool { return baz < 5 }
}

func call(f: (Int) -> Bool, x: Int) -> Bool {
    return f(x)
}

let y = Foo()

call(y, 5) // error: ambiguous conformance to (Int) -> Bool

I suppose that this isn't that problematic, though, since a client can always trivially disambiguate:

call(y.callAsFunction(bar:), 5)

Did I understand correctly that in your example, struct Foo does not declare conformance to (Int) -> (Int)?

Because that would be one more special thing about function protocols. Currently any other protocol requires conformance to be explicitly declared, even if type already satisfies all the requirements:

protocol Foo { func foo() }
struct FooImpl { func foo() {} }
let foo: Foo = FooImpl() // ERROR
1 Like

Ah good point, I didn't think about it from the angle of explicitly declaring conformance. Implementing callAsFunction does seem subtly different to me than implementing a method that happens to have the same name as something in a protocol, but I can see the argument for requiring struct Foo: (Int) -> Bool.

Then, if we wanted to allow labeled-argument callAsFunction definitions to conform, we'd need the declaration-site disambiguation syntax that has come up in the past (and IIRC already exists privately):

struct Foo: (Int) -> Bool {
    @satisfying((Int) -> Bool) func callAsFunction(bar: Int) -> Bool { ... }
}

Would @satisfying(<function type>) be applicable to any member function with a matching type signature? And if so, would that make callAsFunction redundant? In the world where conformance to function types is declared explicitly, I may be convinced that requiring no argument labels is the right solution.

Not really, IMO. p1 is a shorthand for referencing p1(x:), assuming there's no ambiguity, which is a value of type (Float) -> Float today, or in the future could be considered an anonymous type that conforms to any (Float) -> Float. Even this is something we've discussed deprecating, because it's led to confusing behavior in the past. At a fundamental level, argument labels are part of declaration names, not of types.

Another thing that I cannot get my head around, is the relation between argument and result types and the protocol. They don't look like associated types, because they are specified from the outside. To me they look more like generic parameters. But Swift currently does not have generic protocols, and, as far as I understood, is not planning to have.

Improving the UI of generics mentions the following syntax, which may somewhat resolve this:

typealias CollectionOf<T> = Collection where Self.Element == T

Should we say that (Int) -> Void is a syntax sugar for Callable where Self.Args == (Int), Self.Result == Void? Or (Int) -> Void is a non-nominal protocol and notions of both generic args and associated types do not apply.

As an example, let's say we want to write a generic middleware that prints all the args and result of a function call.

With generic arguments, it would look something like this (variadic keyword is from https://github.com/technicated/swift-evolution/blob/variadic-generics/proposals/XXXX-variadic-generics.md):

struct FunctionLogger<variadic Args, Result, F: (Arg) -> Result = any (Args) -> Result>: (Args) -> Result {
    var base: F

    func callAsFunction(_ args: Args) -> Result {
        print(args)...
        let result = base(args)
        print(result)
        return result
    }
}

And with associated types:

struct FunctionLogger<F: Callable>: Callbable {
    typealias Args = F.Args
    variadic typealias Result = F.Result

    var base: F

    func callAsFunction(_ args: Args) -> Result {
        print(args)...
        let result = base(args)
        print(result)
        return result
    }
}

Can the same struct conform to multiple function protocols? With generic arguments - yes, because (Int) -> Void and (Float) -> Void are different protocols. With associated types, there is only one protocol Callable and there can be only one conformance.

You can think of a function type constraint as being like a protocol with associated types, but I'm not sure that's the best way to model them, because functions have modifiers that aren't types, such as inout/owned/shared arguments, throws, and maybe eventually async, pure, or other effect modifiers. I would think of a function constraint as being an ad-hoc requirement that a type has a callAsFunction method with a matching signature.

2 Likes

Here's a class I have that is Equatable, but. the compiler won't generate a Hashable implementation. The AppFile class is Hashable.

class FileRowData: Equatable {
	var sectionName: String?
	var file: AppFile?
	init(name: String?, file: AppFile?) {
		self.sectionName = name
		self.file = file
	}
	static public func == (lhs: FileRowData, rhs: FileRowData) -> Bool {
		if lhs.sectionName != nil && lhs.sectionName == rhs.sectionName { return true }
		if rhs.file != nil && lhs.file != nil && lhs.file?.fileId == rhs.file?.fileId { return true }
		return false
	}
}

I implemented Equatable, but I have no need for Hashable, have no idea why one can't be generated, and do not want to spend time writing dead code.

This is the first example I found. A search finds 46 conformances to Equatable in this project. I tried Hashable when originally implemented, but it didn't work for any of these instances.

The compiler does not autogenerate conformances to Equatable or Hashable for classes, because for mutable classes, it isn't safe to, since modifying a class's fields would change the hash and equality behavior of the "same" value.

1 Like

Off-topic: Why did you implement it as a class, and not a struct? Do you care about identity?

Should it be possible to downcast function existential to an existential with more protocols? Probably yes, works now for regular protocols.

let x1: Hashable & () -> Void = { print(42) }
let x2: () -> Void = { print(42) }
let y1 = (x1 as () -> Void) as? (Hashable & () -> Void) // .some(x1)
let y2 = x2 as? (Hashable & ()->Void) // nil

What would be the type(of: x)? Now it is (() -> Void).self. But if we treat () -> Void as an existential container, then it should return a type of the underlying compiler-generated struct, and that raises questions about compatibility.

Should code compiled with support of functions as protocols be binary compatible with code compiled with old compiler? In that case, can they have different runtimes, or we can assume that they have a shared runtime that supports all the new features? Is it possible to export two implementations of type(of:) from the runtime library - one for old code, one for new code? Probably @_silgen_name can help.

If we maintain compatibility, what should type(of:) report for the function created in old code? Some placeholder struct? Single struct for all function types, or one per type?

Also, when we have regular struct in an existential container and it is converted to Any, then original struct is placed into existential buffer and type of the original struct is recorded in Any, not the type of the existential container. But if we should maintain binary compatibility with older versions, we may have to break this rule and put function existential container into existential buffer of Any. Then new version of type(of:) and bunch of other functions should be ready to handle this and unpack two existential containers.

Intensional equality oughta be called “coincidental equality”. It is almost never what you actually want (cf, the “in algebra” reference), and only happens to work if you can convince some suitable reduction machinery to gin up two identical ASTs - or you introduce a rewrite mechanism to force its hand.

It’s also not relevant to the core issue here. I suggest we move away from the language of mathematics when discussing this topic. While it’s certainly cool to bring up constructive math (and all the fun stuff Baire Spaces let you do) none of that generalizes.

1 Like

Discussion seems to get stuck a bit.

@Joe_Groff, @anandabits, any comments regarding type(of:) behaviour?

I am not and expert on the runtime implementation so I don’t have an informed opinion on your questions about runtime representation. That said, I do agree with your analysis of the expected behavior as a user.

I would expect it to return some anonymous type. We probably shouldn't make any guarantees about what kind of type it produces.

I was also thinking that a more incremental approach to this sort of thing might be, instead of introducing functions as a whole new kind of constraint, would be to allow the use of closure literals with protocols that look like function constraints, something like how Java lambdas work. If you have a protocol with a callAsFunction requirement, like:

protocol CallableWithInt {
  func callAsFunction(_: Int)
}

then, in a context that has a CallableWithInt constraint, we could allow you to use a closure literal as a value of an anonymous type conforming to the protocol, while also allowing synthesis of Equatable/Hashable/Comparable/Codable if the closure context values conform.

2 Likes

I like this idea! It's related to a more general idea I have been thinking about for providing syntactic sugar for anonymous structs. The idea is to use double braces {{ }} to denote an anonymous struct, a capture list to initialize properties, and support trailing anonymous types similar to how we support trailing closures.

When the protocol only has a single method (or possibly subscript) requirement, the body could be used to fulfill the requirement:

protocol Monoid {
    associatedtype Value
    var empty: Value
    func combine(_ lhs: Value, _ rhs: Value) -> Value
}
extension Sequence {
    func fold<M: Monoid>(_ monoid: M) -> Element
       where M.Value == Element
}
[1, 2, 3].fold {{ [empty = 0] in $0 + $1 }}

Ideally this could be made to work in cases such as Monoid & Hashable where memberwise-synthesizable conformances are assumed and the compiler understands that the single requirement being fulfilled is combine.

In cases where multiple requirements must be implemented manually, the body could also support explicit declarations:

[1, 2, 3].fold {{
   typealias Value = Int
   var empty: Int { 0 }
   func combine(_ lhs: Int, _ rhs: Int) -> Int { 
       lhs + hrs
   }
}}

To provide a concrete real world example applied to SwiftUI:

NavigationLink("Details") {{
    VStack {
        // details view content
    }
}}

I haven't thought much about whether that exact syntax is viable or not, but I think sugar along these lines is very interesting. Declaring a nominal type can be sometimes be inconvenient, especially considering the restriction on declaring them inside a function in generic contexts.

For my use cases having function types as structural and not requiring explicit naming is quite useful. Allowing only nominal function protocols to equatable would definitely decrease value of the feature, but we could live with that.

Also, I need support for generic function, which in this context, implies generic protocols, which Swift currently does not have. In combination with generic type alias syntax mentioned in Improving the UI of generics, this may work, but it adds even more boilerplate code:

typealias CollectionOf<T> = Collection where Self.Element == T

protocol Predicate: Hashable {
    associatedtype Value
    func callAsFunction(_ value: Value) -> Bool
}
typealias PredicateOf<T> = Predicate where Self.Value == T

I've been thinking how exactly would I handle the need for declaring function types as nominal types. And assuming generic type aliases with constraints are available, I would probably just do this:

protocol Function0Protocol {
    associatedtype Result
    func callAsFunction() -> Result
}
typealias Function0<R> = Function0Protocol where Self.Result == R

protocol Function1Protocol {
    associatedtype Result
    associatedtype Arg0
    func callAsFunction(_ x0: Arg0) -> Result
}
typealias Function1<A0, R> = Function1Protocol where Self.Arg0 == A0, Self.Result == R

protocol Function2Protocol {
    associatedtype Result
    associatedtype Arg0
    associatedtype Arg1
    func callAsFunction(_ x0: Arg0, x1: Arg1) -> Result
}
typealias Function2<A0, A1, R> = Function2Protocol where Self.Arg0 == A0, Self.Arg1 == A1, Self.Result == R

...

Generic protocols are unnecessary. A generic function could be modeled as a protocol with a generic callAsFunction requirement, which would in fact be something our structural function types don’t support today which protocols could.

Terms of Service

Privacy Policy

Cookie Policy