Быстрое каррирование на практике

Я преобразовывал некоторый код из Objc в Swift и обнаружил интересное использование функционального каррирования в Swift. (Детскую площадку можно найти здесь).

В этом приложении я подключаюсь к серверу, на котором реализован механизм истечения срока действия сеанса; Все запросы к серверу могут возвращать ошибку истечения срока сеанса. Если я получаю эту ошибку, мне нужно обновить сеанс (передав имя пользователя и пароль) и снова повторить предыдущее соединение с сервером.

Например, получение сообщений с сервера реализовано как функция, которая принимает в качестве параметров идентификатор пользователя и закрытие завершения. Функция получения сообщений может выглядеть так:

typealias CompletionClousure = ([Post]?, ResponseError?) -> ()
func fetchPosts(userId: String, completion: CompletionClousure) {
  // Fetch the posts using NSURLSession or anything else
  // Call the completion closure with the result
}

Поскольку я собираюсь снова использовать то же определение замыкания, я создал псевдоним типа CompletionClousure, чтобы улучшить читаемость.

ResponseError - это перечисление, которое содержит все различные коды ошибок, которые может вернуть сервер:

enum ResponseError: ErrorType {
  case SessionExpired
  case OtherKindOfError
}

При ошибке ответа .SessionExpired мне нужно обновить сеанс и снова вызвать fetchPosts. Сеанс обновления выполняется с помощью этой функции:

func refreshSession(completion: () -> () ) {
  // Refresh the session using NSURLSession or anything else
  // Use the current username and password to refresh the session
}

Первая реализация: упаковка функции

Моя первая попытка - создать функцию-оболочку вокруг fetchPosts. Эта функция-оболочка инкапсулирует поток «Подключиться к серверу и обновить сеанс, если необходимо»:

func fetchPostsAndRefreshSessionIfNeeded(userId: String, completion: CompletionClousure) {
  fetchPosts(userId) { (posts, error) in
    if let error = error where error == .SessionExpired {
      refreshSession {
        print("Refreshing Session")
        fetchPosts(userId) { posts, error in
          // Display the posts
          completion(posts, nil)
        }
      }
      return
    }
    // Display the posts
    completion(posts, nil)
  }
}

Это не так уж плохо, fetchPostsAndRefreshSessionIfNeeded прекрасно инкапсулирует сеанс обновления. Получение сообщений теперь выполняется с помощью fetchPostsAndRefreshSessionIfNeeded вместо fetchPosts:

fetchPostsAndRefreshSessionIfNeeded("123") { posts, error in
  print("Use posts to fill UI")
}

Основная проблема с описанной выше реализацией заключается в том, что мы должны реплицировать один и тот же код для каждого запроса к серверу, который у нас есть. Это означает, что нам нужно создать fetchCommentsAndRefreshSessionIfNeeded, fetchFriendsAndRefreshSessionIfNeeded и так далее…

Карринг на помощь

Однако мы можем использовать функциональное каррирование, чтобы повторно использовать основной алгоритм, описанный выше. Идея состоит в том, чтобы обновить fetchPostsAndRefreshSessionIfNeeded и сделать его повторно используемым со всеми другими типами запросов к серверу.

Давайте проанализируем подпись fetchPostsAndRefreshSessionIfNeeded:

func fetchPostsAndRefreshSessionIfNeeded(userId: String, completion: CompletionClousure)

Эта функция принимает в качестве параметра userdId, а затем вызывает fetchPosts в своей реализации. Помимо этих двух причин, основной поток fetchPostsAndRefreshSessionIfNeeded будет одинаковым для всех остальных запросов.

Нам действительно нужна функция, способная выполнять любой запрос к серверу. Эта функция воплощает идею обновления сеанса и повторного вызова исходного запроса при необходимости. Рассмотрим следующую реализацию:

func requestAndRefreshIfNeeded(request: CompletionClousure -> (), completion: CompletionClousure)
  completion: (T, ResponseError?) -> ()) {
    request { (posts, error) in
      if let error = error where error == .SessionExpired {
        refreshSession {
          print("Refreshing Session")
          request { (posts, error) in
            // Display the posts
            completion(posts, error)
          }
        }
        return
      }
      // Display the posts
      completion(posts, error)
    }
}

Да, скобок и стрелок слишком много. Тем не менее, реализация requestAndRefreshIfNeeded действительно похожа на fetchPostsAndRefreshSessionIfNeeded, разница в том, что requestAndRefreshIfNeeded не привязан к конкретному запросу.

requestAndRefreshIfNeeded принимает два параметра:

  • request: фактическая функция запроса, эта функция принимает только 1 параметр, CompletionClousure и возвращает void.
  • Завершение: завершение закрытия для вызова.

Затем нам нужно преобразовать наш fetchPosts из двух параметров в только один. Здесь нам может помочь карри. Мы можем определить каррированную версию fetchPosts:

func curriedFetchPosts(userId: String)(completion: CompletionClousure) {
  fetchPosts(userId, completion: completion)
}

Теперь, когда у нас есть посты с каррированной выборкой, мы можем вызвать его параметр с разделением:

//Passing first argument
let curried = curriedFetchPosts(userId: "123")
//Passing second argument
curried { posts, error in
  print("Use posts to fill UI")
}

Вызов ‘curriedFetchPosts (userId:« 123 »)’ возвращает частично примененную функцию. Эта частично примененная функция по-прежнему является функцией типа ‘CompletionClousure -› () ’, того же типа, который ожидает requestAndRefreshIfNeeded.

Давайте использовать эту каррированную версию получения сообщений с requestAndRefreshIfNeeded:

requestAndRefreshIfNeeded(curriedFetchPost(userId: "123")) { (posts, error) in
  print("Use posts to fill UI")
}

Как и ожидалось, мы можем повторно использовать requestAndRefreshIfNeeded с любым другим запросом к серверу, который у нас есть. Например, если у нас есть функция fetchComments, мы можем вызвать ее следующим образом:

func fetchComments(commentId: String, completion: ([Comment]?, ResponseError?) -> ()) {
  completion([Comment()], nil)
}
// Create a curried version of fetchComments
func curriedFetchComments(commentId: String)(completion: ([Comment]?, ResponseError?) -> ()) {
  fetchComments(commentId, completion: completion)
}

Затем мы используем ‘curriedFetchComments (« 123 »)’ в качестве параметра для requestAndRefreshIfNeeded:

requestAndRefreshIfNeeded(curriedFetchComments("123")) { (comments: [Comment]?, error) -> () in
  print("Comments fetched")
}

Примечание о дженериках

Вышеупомянутая реализация requestAndRefreshIfNeeded все еще не является универсальной, поскольку CompletionClosure привязан к массиву [Post].

Я использовал CompletionClousure, так как за ним легче следить. Однако, чтобы сделать requestAndRefreshIfNeeded универсальным, нам нужно заменить псевдоним типа CompletionClousure на универсальную версию. Окончательная версия requestAndRefreshIfNeeded выглядит следующим образом:

func requestAndRefreshIfNeeded<T>(request: ((T, ResponseError?) -> ()) -> (), completion: (T, ResponseError?) -> ()) {
    request { (posts, error) in
      if let error = error where error == .SessionExpired {
        refreshSession {
          print("Refreshing Session")
          request { (posts, error) in
            // Display the posts
            completion(posts, error)
          }
        }
        return
      }
      // Display the posts
      completion(posts, error)
    }
}

Вывод

Мы увидели, как функциональное каррирование помогло сделать код более пригодным для повторного использования и более легким для чтения. Я загрузил игровую площадку, которая содержит все образцы в этом посте. Пожалуйста, найдите это здесь.

Как всегда, для заметок и продолжения вы можете найти меня здесь @ifnotrue