Closed maxschmeling closed 8 years ago
I realized I can do something like this:
extension Animal: Decodable {
static func decode(j: JSON) -> Decoded<Animal> {
let dict = j.JSONObject() as! [String:AnyObject]
let type = dict["type"] as! String
if type == "cat" {
return Cat.decode(j)
} else if type == "dog" {
return Dog.decode(j)
}
return curry(Animal.init)
<^> j <| "name"
}
}
Is this the most appropriate thing to do in this scenario?
I guess this won't actually work because the Swift compiler can't convert Decoded<Cat>
to Decoded<Animal>
. I feel like I'm just missing something simple because of my inexperience with the language.
@maxschmeling I think you'd need to use type erasure to allow that.
This article by @rnapier might help: http://robnapier.net/erasure
Thanks @paulyoung
I don't think you'll need to go as far as type erasure, instead you should be able to use <|>
to flip between the different subclasses (kinda turning Animal
into a class cluster). I might try something like this:
class Animal {
let name: String
init(name: String) {
self.name = name
}
}
extension Animal: Decodable {
static func decode(j: JSON) -> Decoded<Animal> {
return Cat.decode(j) <|> Dog.decode(j)
}
}
final class Cat: Animal {
let remainingLives: Int
init(name: String, remainingLives: Int) {
self. remainingLives = remainingLives
super.init(name: name)
}
}
extension Cat { // Note that I'm _not_ making Cat decodable
static func decode(j: JSON) -> Decoded<Animal> {
let cat = curry(self.init)
<^> j <| "name"
<*> j <| "remainingLives"
return cat as Decoded<Animal> // This might not be needed, I might try just returning the expression above first
}
}
final class Dog: Animal {
let trained: Bool
init(name: String, trained: Bool) {
self.trained = trained
super.init(name: name)
}
}
extension Dog { // Note that I'm _not_ making Dog decodable
static func decode(j: JSON) -> Decoded<Animal> {
let dog = curry(self.init)
<^> j <| "name"
<*> j <| "trained"
return dog as Decoded<Animal> // This might not be needed, I might try just returning the expression above first
}
}
Since the types for the Cat
and Dog
initializers is different, this should be all you need. If there are overlapping animal subclasses, you could add a simple validator to the beginning of your subclass decode
functions:
func validateType(expectedType: String, json: JSON) -> Bool {
let type: String? = (json <| "type").value
return type == expectedType
}
// later, in Dog.decode:
guard validateType("dog") else { return .typeMismatch("dog", actual: j) }
@gfontenot maybe I'm doing something wrong, but the compiler says:
Cannot convert return expression of type 'Decoded<Dog>' to return type 'Decoded<Animal>'.
on return dog as Decoded<Animal>
I'm also having trouble getting the type erasure working, but still working on it. Trying to wrap my head around how it would work in this scenario.
Sorry, I had spiked this code out here on GitHub. I was able to get this to compile by:
Cat
and Dog
decode functions in favor of global functions that performed the same. This removed an error about ambiguous uses of decode
. It also removed an error about how you can't override functions in extensions.Decoded<Animal>
, instead just returning the result of the parser expression.The resulting code (that compiles for me):
class Animal {
let name: String
init(name: String) {
self.name = name
}
}
extension Animal: Decodable {
static func decode(j: JSON) -> Decoded<Animal> {
return decodeCat(j) <|> decodeDog(j)
}
}
final class Cat: Animal {
let remainingLives: Int
init(name: String, remainingLives: Int) {
self.remainingLives = remainingLives
super.init(name: name)
}
}
func decodeCat(j: JSON) -> Decoded<Animal> {
return curry(Cat.init)
<^> j <| "name"
<*> j <| "remainingLives"
}
final class Dog: Animal {
let trained: Bool
init(name: String, trained: Bool) {
self.trained = trained
super.init(name: name)
}
}
func decodeDog(j: JSON) -> Decoded<Animal> {
return curry(Dog.init)
<^> j <| "name"
<*> j <| "trained"
}
Type erasure really shouldn't be needed here, because we can use covariance to return Decoded<Cat>
as Decoded<Animal>
.
@gfontenot the compiler errors were making me think that isn't possible. Working on attempting the latest code sample. I appreciate the help.
I was able to convert this into my real-world code and it's compiling. Will do some testing to make sure it's all working, but it appears to do what I need.
I really appreciate the help.
@maxschmeling Glad it's working for you! Don't hesitate to re-open if you need more help or if you have any other questions.
@gfontenot just wanted to follow up and say that once I added in the typeMismatch
guard, everything worked perfectly. Thanks again for the help.
how can I go about implementing this with a fairly large model? you know, the type where we have to split curry in intermediate vars to avoid "Expression too complex" errors. This is my model
func decodeCar(j: JSON) -> Decoded<AdDetailModel> {
let a = curry(CarAdDetailModel.init)
<^> j <| "ad_id"
<*> j <| "phone"
<*> j <| "city_name"
<*> j <| "city_area"
<*> j <| "seller_comments"
<*> j <| "price"
<*> j <| "url_slug"
<*> j <|| "pictures"
<*> j <| "is_featured"
<*> j <| "ad_listing_id"
return a
<*> (j <| "last_updated" >>- toNSDate(""))
<*> j <| "featured_request"
<*> j <| "ad_saved"
<*> j <| "make"
<*> j <| "model"
<*> j <| "version"
}
There is a return type mismatch compiler error
@hunaid-hassan-confiz I'm not sure what you're asking. Can I see your model? Also probably the implementation of toNSDate
?
Nevermind. My model is unusually large. 25+ properties. I was having trouble getting rid of the compiler error "Expression too complex", which I managed to somehow but then having to write an initialiser for every class explicitly was too much of work also Argo hits its limit at 15+ properties after which the compiler error just doesn't go away whatever you do. So I am planning to move to alternative. I haven't decided which though
Argo hits its limit at 15+ properties after which the compiler error just doesn't go away whatever you do.
To be clear, Argo has zero limits on the number of properties. I believe what you're hitting is the limitations (imposed by the compiler) on the number of curry
instances we can provide via Curry.framework. We recently updated Curry.framework to be able to handle up to 21 arguments, although that won't help you here given that your model has 25+.
TBH, I constantly question the design of models that have that many properties. I don't know the domain you're working in at all, but I'd highly suspect that you could refactor into multiple objects which might improve the overall design as well as make it easier to use with Argo.
You are right. I am not very happy with the current design of the models. A lot of those properties can be grouped together in smaller structs but its a legacy system I have to work with. It indeed is a compiler limitation. Hope it goes away soon. Thanks for the time though :)
I have JSON that is analogous to this:
I would love to have an
Animal
type with aname
property and then a subclass forCat
with aremainingLives
property andDog
with atrained
property.It doesn't appear to be possible with
Argo
. Am I missing anything? If not, is this something that you think might fit intoArgo
? I would be happy to work on it with a little guidance. (I'm very new to Swift)