Часть 1 Грокаем функторы  Часть 2 Грокаем монадыЧасть 3 Грокаем монады императивноЧасть 4 Грокаем аппликативные функторыЧасть 5 Грокаем валидацию при помощи аппликативного функтораЧасть 6 Грокаем Traversable   Аппликативный функтор, возможно, наименее известный брат монады, но он столь же важен и решает похожую проблему. В этом посте мы постараемся разобраться, что он из себя представляет. Краткое введение в F# Дополнительно к тому, что мы узнали в прошлых частях, нам понадобиться следующее: В F# определен тип Result<T, E>. Он представляет результат вычисления, который может закончится ошибкой. У него есть 2 конструктора, OK - задает значение типа T, и Error содержит значение типа E в случае ошибки. Паттерн-матчинг для типа Result выглядит так: Тестовый сценарий Предположим, нас попросили написать функцию валидации кредитной карты. Модель будет выглядеть следующим образом: А функция, которую нам надо реализовать, так: К счастью, кто-то уже написал несколько функций для валидации номера карты, срока ее действия и кода CVV: Сейчас нас не интересуют подробности проверки валидна ли карта. Я просто привел самый простой пример проверки номера карты, но, в действительности, он может быть куда сложнее. Главное, на что следует обратить внимание - каждая функция принимает непроверенную строку, а возвращает тип Result<string, string>, который указывает, является ли значение допустимым. Если значение недопустимое, функция вернет строку с сообщением об ошибке. Наша первая реализация Нам повезло! Все, что нужно сделать - каким-то образом скомпоновать эти функции для проверки экземпляра кредитной карты. Давайте попробуем! Хммм, этот код не компилируется. Проблема в том, что мы пытаемся записать значения типа Result<string, string> в поля объекта CreditCard, но все его поля имеют тип string. Кажется, я вижу что-то на букву M Будучи знакомыми с Монадой, мы можем попытаться решить проблему с ее помощью. Каждая функция проверки принимает string и преобразует ее в Result<string, string>, и мы, по-видимому, хотим объединить несколько таких функций в цепочку. Мы знаем, что для такого типа композиции можно использовать функцию bind (по-другому flatMap). Например, bind для монады Result будет выглядеть так: Перепишем функцию validateCreditCard используя bind Выглядит аккуратно, но все равно не компилируется! Вызов bind ожидает функцию, которая принимает проверенное значение из предыдущего шага вычислений. В первом случае это будет валидный номер карты, который передается на вход функции validateExpiry. Однако функция validateExpiry ожидает не номер карты, который мы уже проверили и обернули в Result, она принимает непроверенный срок действия. И при этом нам еще надо как-то сохранить номер до конца вычислений, чтобы мы могли создать корректный экземпляр CreditCard. Мы можем добиться этого, накапливая промежуточные результаты проверки по мере продвижения. Ой! Это определенно более запутанный код, чем нам хотелось бы. На каждом этапе нам приходится писать лямбда-выражение, которое принимает верное значение с предыдущего шага, проверяет еще один фрагмент данных и складывает все в кортеж, пока мы, наконец, не получим все части для создания CreditCard. Наша и без того упрощенная задача валидации утонула в море лямбда-выражений и промежуточных кортежей. Представьте тот беспорядок, что мы получили бы, будь у CreditCard больше полей, которые требуют проверки. Нужно решение, которое избавит нас от необходимости во всем этом утилитарном коде. Аппликативный функтор спешит на помощь Другой способ аккумулировать значения — частичное применение. Мы можем взять функцию от n аргументов и вернуть функцию от n - 1 аргументов. Для примера  напишем функцию createCreditCard, которая работает с простыми строковыми значениями: Мы можем постепенно аккумулировать значения, применяя их к функции. numberApplied это функция с сигнатурой или, если проименовать параметры Таким образом мы можем сохранить номер карты на будущее без необходимости создавать кортеж. Давайте изобретем функцию apply, которая позволяет использовать частичное применение для значений, заключенных в некую структуру, такую как Result, и поместим ее перед каждым аргументом Вам может быть интересно, зачем оборачивать createCreditCard в Ok. Функция validateCreditCard возвращает тип Result<CreditCard, string>, значит метод apply должен так же работать над типом Result. Соответственно первый вызов apply тоже должен принимать тип Result, чтобы запустилась вся цепочка вычислений. Еще одна интересная деталь - Result содержащий функцию, а не значение. Именно в эту функцию при помощи частичного применения мы будем добавлять все результаты вызовов apply, выполняя на каждом шаге проверку нового аргумента. Как всегда при написании функции будем отталкиваться от сигнатуры и позволим типам вести нас. При каждом вызове apply нам нужно 2 аргумента: Result<(T -> V), E> и Result<T, E>. Мы попытаемся получить функцию типа T -> V и значение типа T и применить значение к функции, если оба контейнера содержат Ok. Может показаться, что тип T -> V это функция от одного аргумента, однако, никто не мешает нам трактовать сам тип V как еще одну функцию. Таким образом мы можем работать с функцией от любого количества аргументов, при условии что первый аргумент соответствует типу содержащемуся в Result, который мы пытаемся применить.Сигнатура метода apply должна выглядеть так: И даже столь абстрактной информации достаточно, чтобы ее реализовать В итоге мы просто сопоставляем с образцом и функцию и значение и проверяем все 4 возможных варианта. Если оба контейнера содержат Ok, мы применяем функцию к значению, как и хотели, и результат опять помещаем в Ok. Во всех остальных случаях у нас есть по крайней мере одна ошибка, поэтому мы возвращаем ее. Последний случай интересен тем, что мы получаем 2 ошибки. В этой конкретной реализации мы просто возвращаем первую. Тесты Напишем тесты в интерактивном сеансе F#, чтобы убедится, что функция apply работает корректно, и, заодно, лучше понять, что же тут происходит. Выглядит неплохо. Если все входные значения валидны, мы получаем корректный объект CreditCard. Посмотрим что произойдет, если один из параметров будет неверным. Превосходно! Как мы и ожидали. Последнее, что если на вход подать несколько неверных значений. Мы спроектировали функцию таким образом намеренно. Функция выдает ошибку уже при первом неправильном значении. Вы можете обоснованно усомниться, правильно ли это? Может лучше было бы возвращать сразу все ошибки валидации? В следующем посте мы увидим, как можно этого добиться. (по традиции вариант с тестами от меня: прим. переводчика) Мы только что изобрели аппликативный функтор Функция apply определенная на объекте, это то, что делает его аппликативным функтором. Надеюсь, наглядная демонстрация решения проблемы помогла вам понять его суть лучше, нежели простое созерцание сигнатуры функции или чтение о законах аппликативных функторов. Аппликативный функтор похож на монаду тем, что позволяет нам определенным образом комбинировать функции. Но он полезен в том случае, если результаты вычисления этих функций независимы, тогда как монада берет результат вычисления одной функции и использует его как аргумент для другой. Немного причешем наш код Нам пришлось обернуть функцию createCreditCard в Ok в начале нашей цепочки вычислений, если вас это раздражает, мы можем избавиться от этой операции. Если вы уже грокнули функторы, вы знаете, что функция map определенная для типа Result делает его функтором. Мы знаем, что map принимает функцию и вызывает ее на экземпляре Result, если он содержит Ok. Мы можем использовать это, чтобы запустить цепочку вычислений. Этот вариант выглядит неуклюжим, потому что мы нарушили поток вычисления - вызов функции createCreditCard оказался где-то между ее аргументами. Чтобы исправить это, можно определить для map инфиксный оператор <!>, который используется следующим образом (в F# определять свои операторы просто как - let (<!>) f x = Result.map f x. Главное сильно не увлечься этим) прим. переводчика) И в завершение, часто используют оператор <*> для функции apply (таким же образом: let (<*>) f a = apply a f прим. переводчика) Не пугайтесь, если вам кажется, что стало только хуже, это просто символы. Грокнуть аппликативный функтор - означает понять, как работает метод apply и какие реальные проблемы он решает, а не преисполнится эзотерического синтаксиса. Я привел здесь этот пример только потому, что довольно часто можно увидеть как аппликативный функтор используется подобным образом. Аппликативный функтор в дикой природе Всякий раз когда у вас появляется необходимость вызвать функцию с несколькими аргументами, при этом аргументы обернуты в нечто вроде типа Result - аппликативный функтор это как раз то, что вам нужно. Множество типов помимо Result могут быть аппликативным функтором, все, что нужно для этого сделать - реализовать подходящую функцию apply. Например, мы можем написать такую функцию для Option. Как уже было сказано ранее, может быть несколько способов реализовать такую функцию, поэтому убедитесь, что вы выбрали тот, который соответствует вашим потребностям. Протестируйте себя Проверьте, сможете ли вы написать функцию apply для типа Option. Ответ спрятан под катом, постарайтесь не подглядывать, сперва не попробовав реализовать все самостоятельно. Решение похоже на решение для Result, но None не несет никакой дополнительной информации, поэтому во всех случаях, когда по крайней мере одно значение равно None, нам остается только вернуть None. Чему мы научились? Реализовав функцию apply мы получили возможность применять аргументы обернутые в Result к функции, которая принимает обычные строковые аргументы. Это позволило нам использовать частичное применение в качестве средства постепенного накопления данных и, в конечном итоге, писать код так, как будто мы имеем дело с обычными аргументами, а не с типом Result, который может содержать и ошибку.