Computed property getter and setter in generic struct not dispatching function calls to specialized implementations?

I have a generic struct with a couple of functions that just print a string.
I added an extension to that struct with a specialization for a specific type on the generic and re-implemented the functions to print a different string.

If I call these specialized functions on an instance of the struct directly then it correctly calls the specialized versions of the function I call if I instantiate the generic with the matching type.

If, however, define a computed property in the struct definition whose getter or setter call the same functions then the specialized version of the function is never called even when the computed property is accessed on an instance of the struct instantiated for the specialized type.

This didn't match my expectation so I don't know if my expectations are wrong, it's a bug in Swift 5.3 (Xcode 12b2), or simply a limitation in Swift.

Here's a super simple example that does nothing but shows the issue:

// macOS playground source
// swift 5.3, Xcode 12b2

import Cocoa

struct Foo<A> {
	var value: A? {
		get {
			getter()
			return nil
		}
		set { setter() }
	}

	func getter() {
		print("internal getter A")
	}

	func setter() {
		print("internal setter A")
	}
}

// make a specialization for Strings but change the string printed out
extension Foo where A == String {
	func getter() {
		print("External getter A")
	}

	func setter() {
		print("External setter A")
	}
}

print("\nFirst an Int instance, functions first then computed property\n")
var i = Foo<Int>()
i.getter()
i.setter()
i.value = 42
String(describing: i.value)

print("\nNow the string specialization, functions first then computed property\n")
var s = Foo<String>()
s.getter()
s.setter()
s.value = "hello world"
String(describing: s.value)

print("\n\nyou can see that the last two calls to the computed property in the second example used the *internal* implementation of the `getter()` and `setter()` functions instead of the external specialized version")

Curious if I should report this as a bug or if it's a design limitation or ???
I just wasted a lot of time chasing down a bug because of this :-/

TIA

oh, the output would help:

First an Int instance, functions first then computed property

internal getter A
internal setter A
internal setter A
internal getter A

Now the string specialization, functions first then computed property

External getter A
External setter A
internal setter A
internal getter A

you can see that the last two calls to the computed property in the second example used the internal implementation of the getter() and setter() functions instead of the external specialized version

It is the correct behaviour. Swift selects an overload based on the context that invokes it:

  • Inside value, you don't have any constraint, so only (unconstrained) internal accessors are available,
  • When invoking Foo<String>, you have both internal and external ones.

This is the basis of static dispatch. It behaves like this for at two main reasons:

  • The overload is selected at compile time.
  • The generic functions may not be specialised.

If the compiler decides not to specialise value implementation, Foo<String> needs to use the same implementation, and choice of getter and setter, as Foo<Int>, and so they'll use internal accessors which are available on both.

In contrast, dynamic dispatch selects overload at runtime. This is only used in class method and protocol requirement. Though if you make getter and setter protocol requirements, it'll still use the same internal accessors because the functions that satisfy protocols are shared among generic (e.g., Foo<Int> and Foo<String>) so it'll still use internal accessor that is available on all choices of A.

If you want to differentiate the generic parameter, the quickest way would be to put them all inside getter and setter:

func getter() {
  if A.self == String.self {
    ...
  } else {
    ...
  }
}
1 Like

Thank you for the reply. I thought it might be that I am still too set in my ObjC dynamic dispatch ways.

I'm a little surprised because it seems like all the types are known at compile time so I'm not entirely sure why the compiler can't figure it out.

I don't understand your comment "The generic functions may not be specialised" but maybe I'm just not up on the terminology. Isn't this specializing generic functions:

protocol Silly {}

struct Bar {
	func Hello<T>(arg: T) {
		print("Hello<T>")
	}
}

extension Bar {
	func Hello<T>(arg: T) where T: Silly {
		print("Hello<T: String>")
	}
}

It compiles but I didn't make a complete working test example. Thanks.

Not quite. Structs, enums, final classes (or final methods), and methods declared in protocol extensions use static dispatch. As T isn't used in your Hello methods, you are having the same problem as before. Whereas non-final methods and methods declared in the protocol extension. There may be some cases I'm missing. Swift uses three different dispatch methods (static, witness table and message sending). You may want to look at this document from the Swift repo: https://github.com/apple/swift/blob/a24f5d37beab51852569423e59c1e8f293b52a18/docs/SIL.rst#dynamic-dispatch

There are some articles online as well, like this one, which is from 2017. I don't know how accurate it still is.

To make your example give the desired result you may want something like

protocol Silly {
    func dynamicHello()
}

extension Silly {
    func staticHello() {
        print("Static")
    }
}

struct Bar {}

extension Bar: Silly {
    func dynamicHello() {
        print("Dynamic")
    }
}
1 Like

When I said specialization, I mean when the compiler decides to replace every instance of A in the code with the generic parameter (in this case, String) and generate a new binary.

With static dispatch, Swift essentially uses a pointer to each function. In contrast, functions with dynamic uses lookup table* at runtime, and incur more overhead.

Now, both internal and external accessors have different binary (duh), and so separate addresses. If the compiler specializes value for Foo<String>, it could surely use external accessors because it now knows the type of A. If it does, however, the generated binary for Foo<String>.value will use pointers to external accessors when calling those accessors, which is different from binary for Foo<A>.value that uses pointers to internal accessors. So it becomes visible whether or not the compiler generates and uses a specific binary tailored to Foo<String>.value.

Swift wants the specialization process to be transparent to the programmer, so even if Foo<String>.value is specialized, it'd use internal accessors to match the generic, default, Foo<A>.value.

* IIRC, the actual lookup table differs between class methods and protocol requirements, but that's irrelevant to our discussion. @atfelix already links the relevant parts anyway.

This all made sense to me and I expected static dispatch and thought (and still think) that the compiler has enough information to use the appropriate specializations.

Then you said this which I don't follow:

Swift wants the specialization process to be transparent to the programmer, so even if Foo<String>.value is specialized, it'd use internal accessors to match the generic, default, Foo<A>.value .

Yes, that transparency is the magic I want :)
But the the italicized "so even if… it'd use internal accessors" part is what still doesn't make sense to me. It should use the specialization generated for the Foo.value case and not the internal ones.

I can see that this example was overly simplified from the considerably more complicated actual code and that I went too far and now the functions aren't using A.

My first more complicated example that's closer to the real code is here: https://gist.github.com/GeekAndDadDad/967972d329b354b7fa5fe241895c596d

What I don't understand is why the compiler doesn't use the generated specialization for references to the functions when those references are made inside the get and set implementations for a generic property in the struct. This is what broke my expectations. It's a getter and setter for the generic A and so should use the specializations for all functions referenced that are generic on A also, or so it seems to me. ¯_(ツ)_/¯

Thanks for your help.

I will write some more test code. I'm now curious if it will "do the right thing" (as my still-learning brain sees that) when calling these generic functions that have been specialized inside closures or non-generic functions (seems like that must work) defined inside the base struct definition.

The compiler is ensuring the behaviour is the same everywhere.

public func description<T>(of something: T) -> String {
  return "I don’t know what it is."
}
public func description(of text: String) -> String {
  return "It says, “\(text)”"
}

public func describe<T>(_ something: T) {
  print(description(of: something)) // ← always the T variant.
}

public func demonstrate() {

  // Selecting between overloads
  print(description(of: 0)) // I don’t know what it is.
  print(description(of: String)) // It says, “Hello, world!””

  // Calling a generic function
  describe(0) // I don’t know what it is.
  describe("Hello, world!") // I don’t know what it is.
  // ↑ Here the compiler does have access to the information
  // it would have needed to select the other overload,
  // but it chooses not to for consistency.
}

Now, in another module, we copy and past that last function identically (but I’ll fix the comments to describe what happens):

import BaseModule

public func demonstrate() {

  // Selecting between overloads
  print(description(of: 0)) // I don’t know what it is.
  print(description(of: String)) // It says, “Hello, world!””

  // Calling a generic function
  describe(0) // I don’t know what it is.
  describe("Hello, world!") // I don’t know what it is.
  // ↑ Here the compiler has no way of knowing the internals
  // of `describe(_:)`’s implementation, so it cannot inline it,
  // and is therefore also incapable of shifting its implementation
  // to call a different overload.
}

Notice that the result of the function is the same no matter which module you write it in. That would no longer be the case if the compiler were to attempt “specializing” the function the way you describe. And it would be very confusing if the same code did different things in different places.

The compiler will never select an overload based on knowledge outside the current scope, because it knows that knowledge will not always be available.

If you want the implementation to change based on the dynamic type, then you will have to either use a class, a protocol conformance, or embed the dynamism in the function itself. In your case, you can do this:

struct Foo<A> {
  var value: A? {
    get {
      getter()
      return nil
    }
    set { setter() }
  }

  func getter() {
    if A.self == String.self {
      print("specific getter A")
    } else {
      print("general getter A")
    }
  }

  func setter() {
    if A.self == String.self {
      print("specific setter A")
    } else {
      print("general setter A")
    }
  }
}

With dynamic dispatch, the compiler doesn’t have to worry about consistency, because it knows that regardless of what information is or isn’t available to it at compile time, everything will be known when it comes time to actually make the decision while the program is running. The compiler is still capable of inlining when it has access to the implementation, and then optimizing away the unreachable half of the if, but this way it ensures that the same thing still happens even where it can’t do any inlining.

Very interesting reply!

Is your description(of text: String) -> String { ... }
equivalent to the function being defined in an extension with where T == String? Seems like it is and so this is completely equivalent. Assuming so…

The way you've defined these I'm not sure that the T in the description function is the same as the T in the describe function. If they are both defined inside a generic scope then they it would be clear they were.

I was thinking after I posted about extensions in the same module (or even file) vs outside the module in which the caller was calling the function.

The module boundary seems like it has to be a hard wall for static dispatch, but I'm less sure about within the same module.

Thanks for your contribution to my ignorance reduction :slight_smile:

Time to write more test code…

So extensions on generic structs aren't that useful as extension points on the functions of a struct. You have to write all the functions again in the specialization.

Here's my latest test which added Protocols to the mix to see if they helped.

// macOS playground source
// swift 5.3, Xcode 12b2

import Cocoa

protocol TestProtocol {
	associatedtype PT
	func testPFunc() -> PT
	func getter() -> PT
}

// make a specialization for Strings but change the string printed out
extension Foo where A == String {
	func getter() -> A {
		print("External getter A")
		return subValue as? A ?? defaultValue
	}

	mutating func setter(_ newValue: A) {
		print("External setter A")
		self.subValue = newValue
	}

// if this is commented out then the version in the struct definition below calls the internal `getter()`
// So it's not just the getter/setter that has this behavior (which is good).
// any second level call through a non-specialised function will only call the version of the called function in the same scope
// as the non-specialized function.
// If this is commented in, then it calls the `getter()` in this specialization, as expected.
	
//	func testPFunc() -> A {
//		print("External testPFunc()")
//		return getter()
//	}
	
}

struct Foo<A>: TestProtocol {
	typealias PT = A
	
	var subValue: Any? = nil
	let defaultValue: A
	
	init(defaultValue: A) {
		self.defaultValue = defaultValue
	}
	
	var value: A {
		get {
			return getter()
		}
		set { setter(newValue) }
	}
	
	func getter() -> A {
		print("internal getter A")
		return subValue as? A ?? defaultValue
	}

	mutating func setter(_ newValue: A) {
		print("internal setter A")
		self.subValue = newValue
	}
	
	func tryme() -> A {
		print("internal tryme")
		return getter()
	}

	func testPFunc() -> A {
		print("Internal testPFunc()")
		return getter()
	}
}



print("\nFirst and Int instance, functions first then computed property\n")
var i = Foo<Int>(defaultValue: 41)
i.getter()
i.setter(43)
print(i)
i.value = 42
print(i.value)
print(i.tryme())
print(i.testPFunc())

print("\nNow the string specialization, functions first then computed property\n")
var s = Foo<String>(defaultValue: "sorry charlie")
s.getter()
s.setter("set value")
print(s)
s.value = "hello world"
print(s.value)
print(s.tryme())
print(s.testPFunc())

print("\n\nyou can see that the last two calls to the computed property in the second example used the *internal* implementation of the `getter()` and `setter()` functions instead of the external specialized version")

I checked the git you post, it's the same idea that inside value will treat A as unconstrained, and so will always use unconstrained map.

Likewise inside describe, it will always treat T as unconstrained regardless of what T actually is.

@GeekAndDad Now we have 3 scenarios, maybe you can choose one before moving on.

Thanks. I can't do the hard-coded test for a type (if type(of:) == String type stuff), unfortunately.

The gist is a simplification of the real world situation where a colleague wrote a UserDefaults property wrapper (generic) that I was trying to extend to handle arbitrary (non-property-list-types such as an enum) by extending that struct for each type. Unfortunately this doesn't seem to be possible to do cleanly with generic struct extension specializations. I'll have to think about if there's a way to use a protocol to make a clean way to implement this.

Thank you both for your assistance!

It's not the goal of generic to do things differently (based on the generic parameter). The goal of generic is more like to do things the same way.

If you want to refer to the different functions, use dynamic dispatch, like a class method or a protocol requirement.

Even then, generic struct conforming to a protocol will satisfy the protocol in the same way, so in your (now fourth) scenario, Foo.testPFunc will satisfy TestProtocol with the unconstrained Foo.testPFunc.

It's not the goal of generic to do things differently (based on the generic parameter). The goal of generic is more like to do things the same way .

Interesting perspective. In the real project I'm actually trying to write an adaptor (or extension to the struct implementing the property wrapper) to make the utility library property wrapper for UserDefaults to behave the same way across more types which is kind of what I thought generics were for. (In this case saving an enum to/from UserDefaults by getting the raw value from a particular enum type, converting it to a valid property list type and saving it to UserDefaults and then the reverse for reading).

Let's tie things up a bit.

As noted, the binary implementation of Foo<A>.value will always be internal accessor because the pointer pointing to Foo<A>.getter is hardcoded in the binary, and not in some lookup table. If the binary implementation of Foo<String>.value chooses external accessors, then you can see whether the compiler specializes Foo<String>.value based on which accessor it uses. If the generic binary is used, you'll see that it uses internal accessors. If the specialized binary is used, you'll see that it uses external accessors. This is not transparent as you can see whether the compiler specializes it.

To make sure that it's transparent, it needs to use the same hardcoded internal Foo<A>.getter.

It'd be even more confusing if the compiler uses dynamic dispatch when a function is invoked in the same module, but use static dispatch when the function is invoked cross-module.

If you need different functions to achieve it, I'm not sure I can agree that it's behaving the same way. Rather, it sounds like they behave differently to achieve the same goal.

:)

So would you say that protocols are what should be used for this type of "same behavior / different implementations" and generics are… something to use to operate on different types using the same implementation, perhaps?

And so for this particular problem I'll need to use protocols and generics for different parts of the solution...

of course adding property wrappers into the mix like my colleague on this project did just adds a whole other level of complication… Is it Friday yet…? :open_mouth:. IT IS! :smiley:

Likely you'll need to use protocol of some kind. You're trying to choose an implementation w.r.t. the type of A. As I mentioned earlier, you can do

if A.self == String.self {
  ...
}

which is essentially free if the compiler specialize the accessors, and will just incur some overhead if it does not. The behaviour will otherwise be indistinguishable of course.

Otherwise, you can also make map a class method, or protocol requirements on A (not generic). Or you can split them into Foo1 and Foo2.

Thanks. I can't do the if approach in this case because that code is in the utility framework and the enums that don't work are defined at the app layer. So the extension for those types needs to be up in the app layer (not my design).

Terms of Service

Privacy Policy

Cookie Policy