thoughtbot/Argo

Unable to compile when using init with a default value

Megatron1000 opened this issue · 3 comments

[Description of the issue you are experiencing]

If I create an init method that has a default value Argo throws one of its usual obscure, misleading, error messages in the decode method. Removing the default value and writing another combination of the init seems to work as a workaround.

Error Messages

Expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions

List all the error messages or warnings you are seeing, if any.

Models

All models involved, including their Decodable implementations.

struct ArghGo {
    
    let id: String
    let locale: Locale
    init(id: String, locale: Locale = Locale.current) {
        self.id = id
        self.locale = locale
    }
    
}

extension ArghGo : Decodable {
    
    static func decode(_ json: JSON) -> Decoded<ArghGo> {
        return curry(ArghGo.init)
            <^> json <| "id"
    }
}

Argo Version

Argo 4.1.1

Dependency Manager

Cocoapods

Hi, @Megatron1000 .

The reason I think is the initialization method. As you can see that this initialization method will actually be expressed as ((String), (Locale)) -> ArghGo in playground. Even if you have the default declaration of the default parameters.

So I think the more appropriate way is not using curry.

You can use other expression like:

extension ArghGo : Decodable {
    
  static func decode(_ json: JSON) -> Decoded<ArghGo> {
	guard let id: String = (json <| "id").value else {
		return .failure(.typeMismatch(expected: "String", actual: String(describing: json)))
	}

	return ArghGo(id: id)
  }
}

If I create an init method that has a default value Argo throws one of its usual obscure, misleading, error messages in the decode method.

Small nitpick, but we're not throwing anything. Swift is the one responsible for the confusing/misleading error messaging here. I feel your pain and would absolutely improve this if I could.

@Arcovv is correct, the problem is that the type of a function with a default value is the same as the type of a function without the default value:

func add(x: Int, y: Int) -> Int {
    return x + y
}

func add3(x: Int, y: Int = 3) -> Int {
    return x + y
}

let f = add // (Int, Int) -> Int
let g = add3 // (Int, Int) -> Int

In fact, when passing functions like this, you'll see that Swift itself strips the default value from the function:

let five = g(x: 2)
// error: missing argument for parameter #2 in call
// let five = g(x: 2)
//                  ^

So the culprit here is passing your initializer to curry, which has the unfortunate side effect of removing your default value. As @Arcovv pointed out, you could get around this by avoiding the use of curry. This is possible because a single argument function is already "curried". You can see this in the implementation of curry for a single argument function.

I'm not sure if it would compile, but I might propose a slightly different syntax from @Arcovv's solution, which would be to just remove the call to curry:

extension ArghGo : Decodable {  
    static func decode(_ json: JSON) -> Decoded<ArghGo> {
        return ArghGo.init
            <^> json <| "id"
    }
}

It's possible that this will still have the same issue, in which case I'd fall back to @Arcovv's suggestion.

Beyond this, if you're dealing with a function that needs more than one argument and also supplies default values, I'd suggest one of two things:

  1. Move the default functions out of the initializer, and into the decode implementation:
struct ArghGo {
    let id: String
    let locale: Locale
    init(id: String, locale: Locale) {
        self.id = id
        self.locale = locale
    }
}

extension ArghGo : Decodable {
    static func decode(_ json: JSON) -> Decoded<ArghGo> {
        return curry(ArghGo.init)
            <^> json <| "id"
            <*> pure(Locale.current)
    }
}

2: add an additional constructor function with the desired type signature:

struct ArghGo {
    let id: String
    let locale: Locale
    init(id: String, locale: Locale = Locale.current) {
        self.id = id
        self.locale = locale
    }

    static func create(id: String) -> ArghGo {
        return ArghGo(id: id)
    }
}

extension ArghGo : Decodable {
    static func decode(_ json: JSON) -> Decoded<ArghGo> {
        return curry(ArghGo.create)
            <^> json <| "id"
    }
}

Either of these solutions will scale to any arbitrary number of arguments with/without default values. I'd probably prefer the first option, but it's really just a matter of personal preference.

I'm going to go ahead and close this due to inactivity but please feel free to reopen if this is still an issue.