still wrestling with a type conversion problem


(David Baraff) #1

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…]


(Jacob Bandes-Storch) #2

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
        }
    }

    extension Set: MyEncodable {
        func encoded() -> Any {
            // print("Calling complex encoding for Set")
            return self.map { $0 }
        }
    }

    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…]

_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(David Baraff) #3

I can’t thank you enough for that.

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.

···

——————————————
(after some implementation)

extension Set: MyEncodable {
    func encoded() -> Any {
        return self.map { $0 }
    }
    
    func decoded(_ input: Any) -> Set {
        if let listVal = input as? [Set.Element] {
            return Set(listVal)
        }
        return self
    }
}

C++ blows. Swift rocks.

Thank you again.


(David Sweeris) #4

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…]


(Jacob Bandes-Storch) #5

This looks a little weird — what is `self` here? It might be more
appropriate to use a `static func` or even a failable `init?`.

···

On Tue, Jul 4, 2017 at 7:21 AM, David Baraff <davidbaraff@gmail.com> wrote:

    func decoded(_ input: Any) -> Set {
        if let listVal = input as? [Set.Element] {
            return Set(listVal)
        }
        return self
    }


(David Baraff) #6

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?`.


(David Baraff) #7

I figure out how to do this right. Something like:

protocol DecodableFromAny {
    static func fromAny(_ payload:Any) -> Self?
}

extension Array : DecodableFromAny {
    static func fromAny(_ payload: Any) -> Array? {
        if let x = payload as? [Array.Element] {
            return x
        }
        return nil
    }
}

func decodeMe<T>(_ payload:Any) -> T? {
    if let dt = T.self as? DecodableFromAny {
        return type(of: dt).fromAny(payload) as! T
    } else {
        return payload as? T
    }
}

I got tripped up not realizing I could cast “T.self” to the protocol, and then call it once i invoked type(of:) on that. much much nicer this way.

···

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?`.


(David Baraff) #8

func decodeMe<T>(_ payload:Any) -> T? {
    if let dt = T.self as? DecodableFromAny {
        return type(of: dt).fromAny(payload) as! T
    } else {
        return payload as? T
    }
}

Auggh! I only thought it worked. Luckily, I had help. My 16 year-old son looked at this code, and *he* figured it out: I meant to write
   

    if let dt = T.self as? DecodableFromAny {

as

     if let dt = T.self as? DecodableFromAny.Type {

Now, I swear, it really does work.