I’m trying to provide some custom serialization from within a generic class.
(Briefly put, I want to automatically convert types like Set to an array upon serialization, and the reverse when I read it back.)
While trying things out, I was suprised by this:
public func encode<T>(_ value:T) -> Any {
// print("Calling simple encoding for value")
return value
}
public func encode<T>(_ value:Set<T>) -> Any {
// print("Calling complex encoding for value")
return value.map { $0 }
}
···
——————————————
The above transforms a Set to a list, but leaves everything else alone. As expected:
print(type(of: encode(3))) // Int
print(type(of: encode( Set([3,4,5])))) // Array<Int>
Good. But now I do this:
func genericEncode<T>(_ value:T) -> Any {
return encode(value)
}
print(type(of: genericEncode(3))) // Int
print(type(of: genericEncode( Set([3,4,5])))) // Set<Int>
Aha. Inside my generic function, I’ve “lost” the ability to overload based on type. In retrospect, this is perhaps not to surprising. Still, is there anyway that I can provide specialized versions of my functions which will take effect when called from *within* a generic function?
I’m basically trying to leave any type that is happy to go to/from UserDefaults alone, but for some set of types which isn’t JSON serializable (e.g. Set) I’d like to provide a simple pathway. I don’t aim to hit every type in the world, but right now, I’m *totally* stymied when trying to do the serialization from within a generic class, as I lost any ability to specialize which function gets called.
[I’m from the C++ world of templates and metaprogramming, and I’m shifting from one set of constraints to another. The swift type system with generics is cool and powerful, but man i’m still trying to really wrap my head around it. Not that C++ was any easier…]
Using a protocol with an "as?" cast convinces the compiler to do a dynamic
lookup for a protocol conformance based on the value's actual type:
protocol MyEncodable {
func encoded() -> Any
}
func encode<T>(_ value: T) -> Any {
if let value = value as? MyEncodable {
// print("Calling complex encoding for value")
return value.encoded()
} else {
// print("Calling simple encoding for value")
return value
}
}
print(type(of: encode(3))) // Int
print(type(of: encode(Set([3,4,5])))) // Array<Int>
func genericEncode<T>(_ value:T) -> Any {
return encode(value)
}
print(type(of: genericEncode(3))) // Int
print(type(of: genericEncode( Set([3,4,5])))) // *Array<Int>*
(You could've just used a protocol for everything, and done away with
encode() as a free function, except that you can't make .encoded() work on
any arbitrary type without an explicit conformance, and "extension Any:
MyEncodable" isn't allowed.)
-Jacob
···
On Mon, Jul 3, 2017 at 10:44 PM, David Baraff via swift-users < swift-users@swift.org> wrote:
I’m trying to provide some custom serialization from within a generic
class.
(Briefly put, I want to automatically convert types like Set to an array
upon serialization, and the reverse when I read it back.)
While trying things out, I was suprised by this:
public func encode<T>(_ value:T) -> Any {
// print("Calling simple encoding for value")
return value
}
public func encode<T>(_ value:Set<T>) -> Any {
// print("Calling complex encoding for value")
return value.map { $0 }
}
——————————————
The above transforms a Set to a list, but leaves everything else alone.
As expected:
print(type(of: encode(3))) // Int
print(type(of: encode( Set([3,4,5])))) // Array<Int>
Good. But now I do this:
func genericEncode<T>(_ value:T) -> Any {
return encode(value)
}
print(type(of: genericEncode(3))) // Int
print(type(of: genericEncode( Set([3,4,5])))) // Set<Int>
Aha. Inside my generic function, I’ve “lost” the ability to overload
based on type. In retrospect, this is perhaps not to surprising. Still,
is there anyway that I can provide specialized versions of my functions
which will take effect when called from *within* a generic function?
I’m basically trying to leave any type that is happy to go to/from
UserDefaults alone, but for some set of types which isn’t JSON serializable
(e.g. Set) I’d like to provide a simple pathway. I don’t aim to hit every
type in the world, but right now, I’m *totally* stymied when trying to do
the serialization from within a generic class, as I lost any ability to
specialize which function gets called.
[I’m from the C++ world of templates and metaprogramming, and I’m shifting
from one set of constraints to another. The swift type system with
generics is cool and powerful, but man i’m still trying to really wrap my
head around it. Not that C++ was any easier…]
I totally missed that you can impart behavior to all the types that are part of a generic type by doing a protocol on the generic type itself (i.e. extension Set: MyEncodable).
This approach is really what I wanted because it lets an outside user create their own type and then make it be serializable without *me* (as the author of this facility) knowing they’ve done that.
IIRC, C++ treats templates a bit like "code gen macros" in that it creates a different version of the function/type for every value of the template parameter. Swift uses protocol witness tables (essentially vtables, if I'm remembering my terminology correctly) to achieve something roughly similar, but not the same. An important difference, as you've discovered, is that generic types are only as constrained as they need to be in their scope. That is, if you have a function which takes an unconstrained generic parameter, `T`, it doesn't matter if you give in an `Int` at the call site; within that function it's just a `T`. More to the point, if you then pass it on to another function as you have in your code, it has to go to the version that just takes any type, because at the call site for `encode` it's not constrained to be anything else.
Anyway, I hope that helps explain why Swift was behaving differently than you were expecting.
- Dave Sweeris
···
On Jul 3, 2017, at 22:44, David Baraff via swift-users <swift-users@swift.org> wrote:
[I’m from the C++ world of templates and metaprogramming, and I’m shifting from one set of constraints to another. The swift type system with generics is cool and powerful, but man i’m still trying to really wrap my head around it. Not that C++ was any easier…]
Yes, it is weird. What’s going on is this: we invoke the decoded function in the protocol by writing
defaultValue.decoded(someObscureAny)
in this case, we’re saying “take my Any, and give me back the same type of thing as defaultValue, but if you can’t, then give me back defaultValue”. So now you’ve imparted the type information of defaultValue to be able to call the correct function. And i agree this is strange. i’d much rather have
static func decoded(_ input:Any, defaultValue:Set) -> Set {
}
but when I tried that, I got a compiler error about needing to do something which has type constraints. I think a static function in the protocol doesn’t lock things down enough?
···
On Jul 4, 2017, at 5:48 PM, Jacob Bandes-Storch <jtbandes@gmail.com> wrote:
On Tue, Jul 4, 2017 at 7:21 AM, David Baraff <davidbaraff@gmail.com <mailto:davidbaraff@gmail.com>> wrote:
func decoded(_ input: Any) -> Set {
if let listVal = input as? [Set.Element] {
return Set(listVal)
}
return self
}
This looks a little weird — what is `self` here? It might be more appropriate to use a `static func` or even a failable `init?`.