0

I have the following method (named: stories) from Combine's book by Ray Wenderlich that fetches stories from Hacker News Public API as follows:

Model Object:

public struct Story: Codable {
  public let id: Int
  public let title: String
  public let by: String
 public let time: TimeInterval
 public let url: String
}

extension Story: Comparable {
  public static func < (lhs: Story, rhs: Story) -> Bool {
  return lhs.time > rhs.time
  }
}

extension Story: CustomDebugStringConvertible {
   public var debugDescription: String {
   return "\n\(title)\nby \(by)\n\(url)\n-----"
  }
}

API Struct:

struct API {
  enum Error: LocalizedError {
   case addressUnreachable(URL)
   case invalidResponse

var errorDescription: String? {
  switch self {
  case .invalidResponse: return "The server responded with garbage."
  case .addressUnreachable(let url): return "\(url.absoluteString) is unreachable."
    }
   }
 }

enum EndPoint {
   static let baseURL = URL(string: "https://hacker-news.firebaseio.com/v0/")!

   case stories
   case story(Int)

   var url: URL {
  switch self {
  case .stories:
    return EndPoint.baseURL.appendingPathComponent("newstories.json")
  case .story(let id):
    return EndPoint.baseURL.appendingPathComponent("item/\(id).json")
     }
   }
 }


var maxStories = 10


private let decoder = JSONDecoder()
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)

func story(id: Int) -> AnyPublisher<Story, Error> {
    URLSession.shared.dataTaskPublisher(for: EndPoint.story(id).url)
        .receive(on: apiQueue)
        .map(\.data)
        .decode(type: Story.self, decoder: decoder)
        .catch{ _ in Empty<Story, Error>() }
        .eraseToAnyPublisher()
}

func mergedStories(ids storyIDs: [Int]) -> AnyPublisher<Story, Error> {
    let storyIDs = Array(storyIDs.prefix(maxStories))
    precondition(!storyIDs.isEmpty)
    let initialPublisher = story(id: storyIDs[0])
    let remainder = Array(storyIDs.dropFirst())
    return remainder.reduce(initialPublisher) { combined, id in //Swift's reduce method
        combined
        .merge(with: story(id: id))
        .eraseToAnyPublisher()
    }
}

func stories() -> AnyPublisher<[Story], Error> {
    URLSession.shared
        .dataTaskPublisher(for: EndPoint.stories.url)
        .map(\.data)
        .decode(type: [Int].self, decoder: decoder)
        .mapError { error -> API.Error in
            switch error {
            case is URLError:
                return Error.addressUnreachable(EndPoint.stories.url)
            default:
                return Error.invalidResponse
            }
    }
    .filter { !$0.isEmpty }
    .flatMap { storyIDs in
        print("StoryIDs are \(storyIDs)") //the print statement that causes the error
       return self.mergedStories(ids: storyIDs)
    }
    .scan([]) { stories, story -> [Story] in
        stories + [story] //<--- Error fires here
    }
    .map { $0.sorted() }
    .eraseToAnyPublisher()
  }
}

Consumer Code:

let api = API()
var subscriptions = Set<AnyCancellable>()
api.stories()
.sink(receiveCompletion: { print($0) },
      receiveValue: { print($0) })
.store(in: &subscriptions)

The method works perfectly without putting in the print("storyIDs are \(storyIDs)") statement, once this print statement is placed, a weird compiler error fires at the line: stories + [story] which says:

'[Any]' is not convertible to 'Array<Story>'

I don't know what does this misleading error mean in such case ?

JAHelia
  • 6,934
  • 17
  • 74
  • 134
  • Rather a lot here depends on methods and types you have not shown us. – matt Jan 09 '20 at 08:13
  • Multiline closures do not take part in type inference, so making the closure to `flatMap` a multiline one, you are somehow causing it to be unable to infer the type parameters of `flatMap`. If you just want to print the values you receive, you can call `.print()`. – Sweeper Jan 09 '20 at 08:24
  • I have added missing parts, if you copy-paste the code it works without the that buggy print statement. please remove the downvote. – JAHelia Jan 09 '20 at 08:27
  • @Sweeper I know about the .print() operator, but what if I need to custom print an object inside one of the operator closures of Combine, what should I do to accomplish this ? – JAHelia Jan 09 '20 at 08:32

1 Answers1

5

Multi-statement closures do not take part in type inference, so making the closure to flatMap a multi-statement one, you are somehow causing it to incorrectly infer the type parameters of scan. You can provide the types it needs by writing them out in the closure:

.flatMap { storyIDs -> AnyPublisher<Story, API.Error> in
    print("StoryIDs are \(storyIDs)")
   return self.mergedStories(ids: storyIDs)
}

If you just want to print the values you receive, you can call .print() as well.

Update:

I played around with this a bit more, and I found that if you put everything before scan into a let constant, and call scan on that constant, the error moves to somewhere else:

let pub = URLSession.shared
    .dataTaskPublisher(for: EndPoint.stories.url)
    .map(\.data)
    .decode(type: [Int].self, decoder: decoder) //<--- Error fires here now!
    .mapError { error -> API.Error in
        switch error {
        case is URLError:
            return Error.addressUnreachable(EndPoint.stories.url)
        default:
            return Error.invalidResponse
        }
}
.filter { !$0.isEmpty }
.flatMap { storyIDs in
    print("StoryIDs are \(storyIDs)")
   return self.mergedStories(ids: storyIDs)
}

return pub.scan([]) { stories, story -> [Story] in
    stories + [story]
}
.map { $0.sorted() }
.eraseToAnyPublisher()

Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent

This time, it caused decode's type arguments to be inferred wrongly. The original error at scan disappeared because now the compiler knows that pub has a definite type, though it had (incorrectly) found an error somewhere else before it could determine the type of pub.

Following this pattern, I made another temporary let constant:

let pub1 = URLSession.shared
    .dataTaskPublisher(for: EndPoint.stories.url)
    .map(\.data)
let pub2 = pub1
    .decode(type: [Int].self, decoder: decoder)
    .mapError { error -> API.Error in
        switch error {
        case is URLError:
            return Error.addressUnreachable(EndPoint.stories.url)
        default:
            return Error.invalidResponse
        }
}
.filter { !$0.isEmpty }
.flatMap { storyIDs in
    print("StoryIDs are \(storyIDs)")
   return self.mergedStories(ids: storyIDs)
}

return pub2.scan([]) { stories, story -> [Story] in
    stories + [story]
}
.map { $0.sorted() }
.eraseToAnyPublisher()

Finally, the compiler shows a useful message at storyIDs in, which leads use to the solution at the start of the answer:

Unable to infer complex closure return type; add explicit type to disambiguate

It even tells us what type we should insert!

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • excellent answer. I don't know why's the downvote for my question. – JAHelia Jan 09 '20 at 08:41
  • @JAHelia I could understand the downvote if it’s before the edit (is it?), as you didn’t provide all the required info. So I guess it’s just that the downvoter hasn’t seen the edit yet, maybe. – Sweeper Jan 09 '20 at 08:44
  • 1
    @JAHelia I've found some more insight on this. See the edit. The Swift compiler is weird :-). – Sweeper Jan 09 '20 at 11:27
  • 1
    @Sweeper Yes, I've got a bug filed on the fact that the compiler is able to tell us what type to insert while claiming at the same time that it cannot infer the type: https://bugs.swift.org/browse/SR-5735 – matt Jan 09 '20 at 18:31