Объедините странную ошибку компиляции после помещения оператора print в замыкание оператора flatMap - PullRequest
0 голосов
/ 09 января 2020

У меня есть следующий метод (названный: stories) из книги Комбайна Рэя Вендерлиха, который извлекает истории из Hacker News Publi c API следующим образом:

Объект модели:

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

Код потребителя:

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

Метод работает идеально, не вставляя оператор print("storyIDs are \(storyIDs)") после выполнения этого оператора печати. помещается странная ошибка компилятора: stories + [story], которая говорит:

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

Я не знаю, что означает эта вводящая в заблуждение ошибка в таком случае ?

1 Ответ

2 голосов
/ 09 января 2020

Закрытия из нескольких операторов не участвуют в выводе типа , поэтому, сделав закрытие для flatMap многооператорным, вы каким-то образом заставляете его неправильно выводить параметры типа scan. Вы можете указать нужные типы, написав их в закрытии:

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

Если вы просто хотите напечатать полученные значения, вы также можете вызвать .print().

Обновление:

Я немного поиграл с этим и обнаружил, что если вы поместите все до scan в константу let и вызовете scan для этой константы, ошибка переместится куда-нибудь иначе:

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()

Для метода экземпляра 'decode (type: decoder :)' требуются типы 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') и «JSONDecoder.Input» (он же «Данные») должен быть эквивалентен

. На этот раз это привело к неверному выводу аргументов типа decode. Первоначальная ошибка в scan исчезла, потому что теперь компилятор знает, что pub имеет определенный тип, хотя он (неправильно) нашел ошибку где-то еще, прежде чем смог определить тип pub.

Следуя этой схеме, я сделал еще одну временную let константу:

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()

Наконец, компилятор показывает полезное сообщение на storyIDs in, которое приводит к использованию решения в начале ответа:

Невозможно определить тип возврата сложного замыкания; добавить явный тип для устранения неоднозначности

Он даже говорит нам, какой тип мы должны вставить!

...