Быстрое каррирование на практике
Я преобразовывал некоторый код из 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