Вы можете столкнуться с карри в повседневном коде, даже не подозревая об этом. Вот немного моих размышлений о карри и о том, как его применять в Javascript и Swift.
Взять один параметр в Haskell
В Haskell все функции официально принимают только 1 параметр. Функции со многими параметрами просто каррируются, а значит будут применяться частично. Вызов sum 1
просто возвращает функцию с 1 параметром, затем в эту функцию передается 2. Следующие 2 вызова функций одинаковы.
ghci> sum 1 2 3 ghci> (max 1) 2 3
Я склонен думать о каррированной функции или частично примененной функции как о чем-то, что несет зависимости на каждом этапе приложения. Каждая каррированная функция может быть назначена переменной или передаваться, или как возвращаемое значение.
Карри в Swift для предиката
Когда я пытался сделать свою собственную библиотеку Сигнал, у меня
и Event
фильтр
Тогда должен быть фильтр для Signal
. Идея фильтра заключается в том, что мы должны обновить сигнал, если Event
равно Next
с правильным отфильтрованным значением.
public func filter(f: T -> Bool) -> Signal<T>{
let signal = Signal<T>()
subscribe { result in
switch(result) {
case let .Success(value):
if f(value) {
signal.update(result)
}
case let .Error(error): signal.update(.Error(error))
}
}
return signal
}
2 параметра
Но имея Event
в качестве еще одной монады, я думаю, что она должна быть более инкапсулирована, если эта логика переключения будет перемещена в Event
. Здесь filter
принимает 2 параметра
Event.swift
func filter(f: T -> Bool, callback: (Event<T> -> Void)) {
switch self {
case let .Next(value) where f(value):
callback(self)
case .Failed:
callback(self)
default:
break
}
}
Signal.swift
public func filter(f: T -> Bool) -> Signal<T> { let signal = Signal<T>()
subscribe { event in event.filter(f, callback: signal.update) }
return signal }
карри
С помощью каррирования мы можем сделать filter
более абстрактной функцией и отложить принятие решения о передаче параметра callback
. Это немного увлекает, но я нахожу это полезным
Теперь filter
принимает 1 параметр и возвращает функцию, которая принимает callback
в качестве параметра.
Event.swift
func filter(f: T -> Bool) -> ((Event<T> -> Void) -> Void) {
return { g in
switch self {
case let .Next(value) where f(value):
g(self)
case .Failed:
g(self)
default:
break
}
}
}
Signal.swift
public func filter(f: T -> Bool) -> Signal<T> { let signal = Signal<T>()
subscribe { event in event.filter(f)(signal.update) }
return signal }
Синтаксис карри в Swift 2 и выше
Swift 2 поддерживает функцию синтаксиса карри
func sum(a: Int)(b: Int) -> Int { return a + b } let sumWith5 = sum(5) let result = sumWith5(b: 10)
К сожалению, синтаксический сахар для объявления карри был убран, начиная со Swift 3. Вы можете узнать об этом в разделе Прощание с карри. Но это не имеет большого значения, так как мы можем легко создать функцию карри. Это просто функция, которая возвращает другую функцию.
Использование curry для частичного применения в UIKit
Я использовал эту технику карри в своем приложении Xkcd. См. MainController.swift. MainController
— это ванильный UITabBarController
с ComicsController
и FavoriteController
, встроенными в UINavigationViewController
.
Особенность заключается в том, что при выборе комикса экран сведений о комиксе должен быть помещен поверх стека навигации. Например в ComicsController
/// Called when a comic is selected var selectComic: ((Comic) -> Void)?
Все, что нужно знать ComicsController, — это вызвать замыкание selectComic
с выбранным Comic, и кто-то должен знать, как обрабатывать этот выбор. Вернемся к функции handleFlow внутри MainController.
private func handleFlow() { typealias Function = (UINavigationController) -> (Comic) -> Void let selectComic: Function = { [weak self] navigationController in return { (comic: Comic) in guard let self = self else { return } let detailController = self.makeDetail(comic: comic) navigationController.pushViewController(detailController, animated: true) } } comicsController.selectComic = selectComic(comicNavigationController) favoriteController.selectComic = selectComic(favoriteNavigationController) }
Я объявил Function
как typealias
, чтобы явно указать функцию карри, которую мы собираемся построить.
typealias Function = (UINavigationController) -> (Comic) -> Void
Мы строим selectComic
как каррированную функцию, которая принимает UINavigationViewController
и возвращает функцию, которая принимает Comic
и возвращает Void
. Таким образом, когда мы частично применяем selectComic с a navigationController
, мы получаем другую функцию, которая имеет зависимость navigationController
и готова к назначению свойству selectComic
в comicsController
.
Обещанная карри функция в Javascript
Мне нравится работать с Promise
и async/await
в Javascript. Это позволяет создавать цепочки и легко рассуждать. Поэтому при работе с обратными вызовами в Javascript, например обратными вызовами из нативных модулей в React Native, я склонен преобразовывать их в Promise
.
Например, при работе с HealthKit нам нужно выставлять нативные модули вокруг него.
// @flow import { NativeModules } from 'react-native' type HealthManagerType = { checkAuthorisation: ((string) => void)) => void, authorise: ((boolean) => void)) => void, readWorkout: (Date, Date, () => void)) => void, readDailySummary: (Date, Date, () => void)) => void, readMeasurement: (Date, Date, () => void)) => void } const HealthManager: HealthManagerType = NativeModules.HealthManager export default HealthManager
Мы можем создать функцию toPromise
, которая может преобразовать функцию с обратным вызовом в Promise
.
// @flow const toPromise = (f: (any) => void) => { return new Promise<any>((resolve, reject) => { try { f((result) => { resolve(result) }) } catch (e) { reject(e) } }) } export default toPromise
Однако, как видно из подписи, он работает только с обратным вызовом типа (any) => void
. Другими словами, этот обратный вызов должен иметь ровно 1 параметр, потому что Promise
может либо вернуть значение, либо выдать ошибку.
Чтобы исправить это, мы можем создать карри-функцию, которая может превращать функцию с 1, 2 или 3 параметрами в карри-функцию. Благодаря динамической природе Javascript у нас есть
// @flow function curry0(f: () => void) { return f() } function curry1(f: (any) => void) { return (p1: any) => { return f(p1) } } function curry2(f: (any, any) => void) { return (p1: any) => { return (p2: any) => { return f(p1, p2) } } } function curry3(f: (any, any, any) => void) { return (p1: any) => { return (p2: any) => { return (p3: any) => { return f(p1, p2, p3) } } } } export default { curry0, curry1, curry2, curry3 }
Таким образом, с функцией, имеющей 3 параметра, мы можем использовать curry3
, чтобы частично применить первые 2 параметра. Затем у нас есть функция, которая принимает только обратный вызов, и он превращается в Promise
через toPromise
.
const readWorkout = curry.curry3(HealthManager.readWorkout)(DateUtils.startDate))(DateUtils.endDate)) const workouts = await toPromise(readWorkout)
Куда пойти отсюда
Вот некоторые из моих любимых постов, чтобы узнать больше о карри
- Функции высшего порядка
- Введение в каррирование функций в Swift
- Быстрое каррирование функций
- Каррирование в JavaScript: мне нравится, как он использует
memory
иslice
для постепенного построения более универсальной функции карри.
Если вам понравился этот пост, рассмотрите возможность посещения других моих статей и приложений 🔥