1

The following code doesn't compile:

import Foundation
import PlaygroundSupport

protocol C : Decodable {
}

class A : C {
    let code: Int

}

class B : C {
    let blob: Int
}

var c : C.Type
let a = A.self
let b = B.self
let i = 100
if i == 100 {
    c = a
}
else if i < 100 {
    c = b
}
do {
    let data = String("{\"code\": 123}").data(using: .utf8)!
    let j = try JSONDecoder().decode(c, from: data)
    print(j)
}
catch let error as NSError{
    print(error.debugDescription)
}

The error is:

cannot invoke 'decode' with an argument list of type '(C.Type, from: Data)' let j = try JSONDecoder().decode(c, from: data)

What would be the proper way to declare the c variable so it could receive or type A or type B and still complies with JSONDecoder().decode(...) signature?

avaz
  • 523
  • 2
  • 8
  • 20
  • This is technically possible using the same protocol extension trick from https://stackoverflow.com/a/45239416/2976878 – though if you're just going to force cast the result to `A`, why not try to decode an `A`? – Hamish Sep 12 '18 at 16:46
  • @Hamish Can that trick really be used to call `decode`, though? That doesn't feel possible (but my imagination may be insufficient here :D) – Rob Napier Sep 12 '18 at 16:50
  • @RobNapier Yup, see my comment on your answer :) – Hamish Sep 12 '18 at 16:52
  • Whoa; never mind. You're right. – Rob Napier Sep 12 '18 at 16:52
  • Hi @Hamish the downcast part should not be in question, it was a test that I was doing but it is not related with what I expect as final result from this piece of code. About the protocol extension, cool, seems to be an approach, also I read the answers in the other SO question that you pointed out, I think I understood the situation but still didn't completely got the reasoning about it. Fine that was defined that `protocol`s in Swift can't be used as a type, but I don't get why? (Maybe this is a question for 'https://forums.swift.org/') – avaz Sep 12 '18 at 18:56
  • 1
    @avaz Ah okay, I see (re: downcasting). It's not that protocols can't be used as types, it's that if you substitute a protocol type for a generic placeholder, the type `T.Type` becomes the type that describes the protocol type (`P.Protocol`), rather than the conforming types of the protocol (`P.Type`). And in your case, you want to talk in terms of the conforming types (i.e `A` and `B`), as those are things that can actually be decoded. The current situation of mixing generics, protocols & metatypes definitely isn't intuitive though – hopefully things will be better in the future. – Hamish Sep 12 '18 at 21:31
  • @Hamish I see, indeed it is not intuitive. Based on your gist to get things to work in this case you had to trick JSONEncoder to accept the C.Type (i.e. Protocol type). This means that currently any place with a generic that is expecting a .Type protocols can't be passed as an argument. Seems that the trick you did to make it happen should be done by the language as a protocol will never be decodable itself. I thought in accept the gist as the answer but unfortunately after start to use it I realised that the new method in `JSONEncoder` leaks internal details so is not a good idea for me. – avaz Sep 13 '18 at 12:21

1 Answers1

1

(Bah; never mind my old answer. Hamish is right.)

Add an extension:

extension C {
    static func decode(data: Data) throws -> C {
        return try JSONDecoder().decode(Self.self, from: data)
    }
}

Decode with it:

let j = try c.decode(data: data)

EDIT: I'm not certain what you mean by it not compiling against your code. Perhaps because you have another bug (you don't initialize c in all legs). Here is what I'm describing, based on your code:

import Foundation
import PlaygroundSupport

protocol C : Decodable {
}

// Adding extension
extension C {
    static func decode(data: Data) throws -> C {
        return try JSONDecoder().decode(Self.self, from: data)
    }
}

class A : C {
    let code: Int

}

class B : C {
    let blob: Int
}

var c : C.Type = A.self // You have to initialize this for all legs
let a = A.self
let b = B.self
let i = 100
if i == 100 {
    c = a
}
else if i < 100 {
    c = b
}
do {
    let data = String("{\"code\": 123}").data(using: .utf8)!
    // Rewrite this line
    let j = try c.decode(data: data)
    print(j)
}
catch let error as NSError{
    print(error.debugDescription)
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • This [is technically possible](https://gist.github.com/hamishknight/257e18a4a1ee607999a7a95f59daef9c), albeit perhaps not the best solution for OP. – Hamish Sep 12 '18 at 16:48
  • @Hamish Thanks. Fixed. I think I've actually done this before, too. The things that fall out of my head… – Rob Napier Sep 12 '18 at 16:54
  • @RobNapier the solution presented in your answer doesn't compile against the code that I provided. I'm down voting so people does not get confused. – avaz Sep 13 '18 at 07:58
  • @Hamish the gist is very interesting thanks for that. – avaz Sep 13 '18 at 09:41
  • @avaz I'm not certain what you mean by "doesn't compile against he code that I provided." I've added the full playground code based on your latest version (without the unnecessary downcasting). – Rob Napier Sep 13 '18 at 14:56
  • Hi @RobNapier, thanks for the edit, the way that was before was incomplete and as I said, could mislead people. Indeed was the `c` variable not being properly initialized but it was due the changes that you suggested. Anyway, thanks again. Your solution as the one from Hamish works but is too hacky for my needs so for my now I will take another approach and wait for changes in the language to come. – avaz Sep 14 '18 at 13:02