Быстрое функциональное программирование - "Необязательное связывание" и "Дополнительная карта"

Я работаю над Функциональным программированием в Swift, и у меня действительно нет хорошего способа понять различия в концепции, представленной в Раздел опций.

Образец при работе с опциями имеет тенденцию:

if let thing = optionalThing {
    return doThing(thing)
}
else {
    return nil
}

Эта идиома выполняется лаконично со стандартной библиотечной функцией map

map(optionalThing) { thing in doThing(thing) }

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

В книге описывается функция map:

func map<T, U>(optional: T?, f: T -> U) -> U?
{
    if let x = optional {
        return f(x)
    }
    else {
        return nil
    }
}

И также помогает нам определить необязательную функцию привязки. Примечание: в книге используется оператор >>=, но я решил использовать именованную функцию, потому что она помогает мне увидеть сходство.

func optionalBind<T, U>(optional: T?, f: T -> U?) -> U?
{
    if let x = optional {
        return f(x)
    }
    else {
        return nil
    }
}

Реализация для обоих этих методов выглядит идентично мне. Единственное различие между ними - это аргумент функции, который они принимают:

  • map принимает функцию, которая преобразует T в U
  • optionalBind принимает функцию, которая преобразует T в необязательный U

Результат "гнездования" этих вызовов функций вредит моему мозгу:

func addOptionalsBind(optionalX: Int?, optionalY: Int?) -> Int?
{
    return optionalBind(optionalX) { x in
        optionalBind(optionalY) { y in
            x + y
        }
    }
}

func addOptionalsMap(optionalX: Int?, optionalY: Int?) -> Int?
{
    return map(optionalX) { x in
        map(optionalY) { y in
            x + y
        }
    }
}

  • Функция addOptionalsBind выполняет именно то, что вы ожидаете от нее.
  • Функция addOptionalsMap не компилируется:

    'Int??' не конвертируется в 'Int?'

Я чувствую, что я близок к пониманию того, что происходит здесь (и необязательное целое снова завернуто в факультативное? Но как? Почему? Hu?), Но я также достаточно далеко от понимания того, что я не совсем уверен в умный вопрос.

Ответ 1

То, что происходит, может быть более ясным с более подробной реализацией addOptionalsMap. Начните с самого внутреннего вызова map, вместо того, что у вас есть, вместо этого используйте это:

let mappedInternal: Int? = map(optionalY) { (y: Int) -> Int in
    return x + y
}

Закрытие, предоставляемое map, принимает Int и возвращает и Int, тогда как вызов map сам возвращает необязательный параметр: Int?. Никаких сюрпризов! Отпустите один шаг и посмотрите, что произойдет:

let mappedExternal: ??? = map(optionalX) { (x: ???) -> ??? in
    let mappedInternal: Int? = map(optionalY) { (y: Int) -> Int in
        return x + y
    }
    return mappedInternal
}

Здесь мы можем увидеть наше значение mappedInternal сверху, но есть несколько типов left undefined. map имеет подпись (T?, T -> U) -> U?, поэтому нам нужно только выяснить, что T и U в этом случае. Мы знаем, что возвращаемое значение замыкания mappedInternal равно Int?, поэтому U здесь становится Int?. T, с другой стороны, может оставаться не факультативным Int. Подставляя, получаем следующее:

let mappedExternal: Int?? = map(optionalX) { (x: Int) -> Int? in
    let mappedInternal: Int? = map(optionalY) { (y: Int) -> Int in
        return x + y
    }
    return mappedInternal
}

Закрытие T -> U, которое оценивается как Int -> Int?, а целое выражение map заканчивается отображением Int? в Int??. Не то, что вы имели в виду!


Контрастируйте это с помощью версии optionalBind, полностью заданной по типу:

let boundExternal: ??? = optionalBind(optionalX) { (x: ???) -> ??? in
    let boundInternal: Int? = optionalBind(optionalY) { (y: Int) -> Int? in
        return x + y
    }
    return boundInternal
}

Посмотрите на те типы ??? для этой версии. Для optionalBind нам нужно T -> U? замыкание и иметь возвращаемое значение Int? в boundInternal. Таким образом, оба T и U в этом случае могут быть просто Int, а наша реализация выглядит так:

let boundExternal: Int? = optionalBind(optionalX) { (x: Int) -> Int? in
    let boundInternal: Int? = optionalBind(optionalY) { (y: Int) -> Int? in
        return x + y
    }
    return boundInternal
}

Ваша путаница может возникнуть из-за того, что переменные могут быть "подняты" как дополнительные. Легко видеть при работе с одним слоем:

func optionalOpposite(num: Int?) -> Int? {
    if let num = num {
        return -num
    }
    return nil
}

optionalOpposite можно вызвать либо с переменной типа Int?, как это явно предполагает, либо не необязательной переменной типа Int. В этом втором случае необязательная переменная неявно преобразуется в необязательную (то есть отмененную) во время вызова.

map(x: T, f: T -> U) -> U? делает подъем в возвращаемом значении. Поскольку f объявлен как T -> U, он никогда не возвращает необязательный U?. Но возвращаемое значение map как U? означает, что f(x) возвращается к U? при возврате.

В вашем примере внутреннее закрытие возвращает x + y, a Int, которое поднимается до Int?. Это значение затем снова поднимается на Int??, что приводит к несоответствию типа, поскольку вы объявили addOptionalsMap для возврата Int?.

Ответ 2

  • отображает функцию, преобразующую T в U
  • optionalBind принимает функцию, которая преобразует T в необязательный U

Совершенно верно. В этом вся разница. Рассмотрим действительно простую функцию, lift(). Он преобразует T в T?. (В Haskell эта функция будет называться return, но это слишком запутанно для программистов, не относящихся к Haskell, и, кроме того, return - это ключевое слово).

func lift<T>(x: T) -> T? {
    return x
}

println([1].map(lift)) // [Optional(1)]

Великий. Теперь, если мы снова это сделаем:

println([1].map(lift).map(lift)) // [Optional(Optional(1))]

Хммм. Итак, теперь у нас есть Int??, и это боль, с которой приходится иметь дело. На самом деле мы бы предпочли только один уровень необязательности. Давайте построим функцию для этого. Мы будем называть его flatten и сглаживать двойную опцию до одного необязательного.

func flatten<T>(x: T??) -> T? {
    switch x {
    case .Some(let x): return x
    case .None : return nil
    }
}

println([1].map(lift).map(lift).map(flatten)) // [Optional(1)]

Высокий. Только то, что мы хотели. Вы знаете, что .map(flatten) случается очень много, поэтому дайте ему имя: flatMap (это то, что называют такие языки, как Scala). Несколько минут игры должны доказать вам, что реализация flatMap() является в точности реализацией bindOptional, и они делают то же самое. Возьмите опцию и что-то, что возвращает необязательный, и получите только один уровень "необязательной" из него.

Это очень распространенная проблема. Это настолько распространено, что у Haskell есть встроенный оператор для него (>>=). Это настолько распространено, что у Swift также есть встроенный оператор, если вы используете методы, а не функции. Он назывался факультативно-цепочкой (это настоящий позор, что Swift не распространяется на функции, но Swift любит методы намного больше, чем любит функции):

struct Name {
    let first: String? = nil
    let last: String? = nil
}

struct Person {
    let name: Name? = nil
}

let p:Person? = Person(name: Name(first: "Bob", last: "Jones"))
println(p?.name?.first) // Optional("Bob"), not Optional(Optional(Optional("Bob")))

?. действительно просто flatMap (*), что на самом деле просто bindOptional. Почему разные имена? Ну, оказывается, что "карта, а затем сглаженная" эквивалентна другой идее, называемой монадической связью, которая думает об этой проблеме по-другому. Да, монады и все такое. Если вы думаете о T? в качестве монады (какой она есть), то flatMap оказывается обязательной операцией связывания. (Так что "bind" - это более общий термин, применимый ко всем монадам, тогда как "плоская карта" относится к деталям реализации. Я считаю, что "плоская карта" легче научить людей, но YMMV.)

Если вы хотите еще более длинную версию этого обсуждения и как он может применяться к другим типам, чем Optional, см. Flattenin 'Your Mappenin'.

(*) .? также может выглядеть как map в зависимости от того, что вы проходите. Если вы пройдете T->U?, тогда он flatMap. Если вы пройдете T->U, тогда вы можете думать о нем как о map, или вы все еще думаете, что это flatMap, где U неявно продвигается до U? (что будет делать Swift автоматически).