Вы можете столкнуться с карри в повседневном коде, даже не подозревая об этом. Вот немного моих размышлений о карри и о том, как его применять в 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)

Куда пойти отсюда

Вот некоторые из моих любимых постов, чтобы узнать больше о карри

Если вам понравился этот пост, рассмотрите возможность посещения других моих статей и приложений 🔥