
Примечание. Это Урок 25 из серии "Переход с JavaScript на PureScript". Обязательно прочитайте введение в серию, где мы расскажем о целях и общих чертах, а также об установке, компиляции и запуске PureScript. Я буду публиковать новый учебник примерно раз в месяц. Так что заходите почаще, впереди еще много интересного!
В последнем уроке мы начали рассматривать естественные преобразования в функциональном программировании — что они из себя представляют и каковы их законы. В этом руководстве мы продолжим эту тему, показав, как использовать естественные преобразования в вашем коде. Напомним, что естественное преобразование — это функция, которая переводит функтор, содержащий некоторое a, в другой функтор, содержащий это a (т.е. F a -> G a).
Я позаимствовал план этой серии и образцы кода JavaScript с разрешения из курса egghead.io Профессор Фрисби представляет компонуемый функциональный JavaScript Брайана Лонсдорфа — спасибо, Брайан! Фундаментальное предположение заключается в том, что вы смотрели его видео по теме, прежде чем заняться эквивалентной абстракцией PureScript, представленной в этом руководстве. Брайан исключительно хорошо раскрывает представленные концепции, и я считаю, что лучше, если вы разберетесь с их реализацией, не выходя из JavaScript.
Вы найдете текст и примеры кода для этого руководства на Github. Если вы прочитали что-то, что, по вашему мнению, можно было бы объяснить лучше, или пример кода, который нуждается в рефакторинге, сообщите мне об этом в комментарии или отправьте мне запрос на включение. Кроме того, прежде чем уйти, пожалуйста, поставьте звезду, чтобы помочь мне опубликовать эти уроки.
Случай 1: когда функция не поддерживает конструктор вашего типа
Что делать, если библиотечная функция не поддерживает конструктор вашего типа? Вы сами это пишете? Вместо этого рассмотрите возможность использования естественного преобразования. В примере Брайана нет экземпляра chain для массивов Javascript. Следовательно, мы не можем разделить символ напрямую:
/* This won't work - no instance of chain on arrays */
['hello', 'world']
.chain(x => x.split(''))
Чтобы решить эту проблему, он использовал естественное преобразование, чтобы превратить массив в список, зная, что chain существует в конструкторе типа List.
const res = List(['hello', 'world'])
.chain(x => List(x.split('')))
Терминальный выход
List [ "h", "e", "l", "l", "o", "w", "o", "r", "l", "d" ]
Array PureScript имеет экземпляр bind (инфиксный оператор >==), который эквивалентен chain выше. Таким образом, мы можем воздействовать на массив напрямую, чтобы разбить его слова на символы. Но представьте, что у вас List слов! Теперь у нас проблема, потому что split из Data.String.Common поддерживает только массивы! Итак, аналогично решению Брайана, мы используем естественные преобразования между List и Array. Мы выполняем их с помощью fromFoldable; доступны как в Данные.Массив, так и в Данные.Список. Подпись типа для fromFoldable в Data.Array:
fromFoldable :: forall f. Foldable f => f ~> Array
Он преобразует любую структуру Foldable в Array. Например:
fromFoldable (Just 1) = [1]
fromFoldable (Nothing) = []
Обратите внимание на ~> в сигнатуре типа fromFoldable. Это инфиксный оператор для NaturalTransformation, и это сигнал не только для компилятора, но и для всех, кто читает ваш код, что эта функция выполнит естественное преобразование. С этим объяснением давайте посмотрим, как мы можем преобразовать список слов в символы в PureScript:
import Data.Array (fromFoldable) as A import Data.List (fromFoldable) import Data.String.Common (split)wordList :: List String wordList = ("hello" : "world" : Nil)main :: Effect Unit main = do log "\nSplit on characters from a wordList" logShow $ fromFoldable $ (A.fromFoldable wordList) >>= \x -> split (Pattern "") x
В main, работая справа налево, шаги следующие: 1. преобразовать wordList в массив, используя Data.Array.fromFoldable; 2. выполнить разбиение на символы, связав массив с split; 3. преобразовать Array обратно в List ~ with ~Data.List.fromFoldable.
Случай 2: Доступ к произвольным элементам в складной структуре
Большинство языков функционального программирования не предполагают, что индекс в складной структуре существует, прежде чем пытаться получить к ней доступ. Например, как функция может быть чистой при попытке вернуть первый элемент пустого массива? Решение состоит в том, чтобы вернуть конструктор типа, учитывающий эту возможность, обычно Either или Maybe.
Вспомогательные функции в PureScript Data.Array используют конструктор типа Maybe для устранения этой возможности. Например, если массив непуст, вспомогательная функция Data.Array.head возвращает первый элемент Just a. В противном случае возвращается Nothing.
Имея это в виду, давайте портируем второй пример Брайана на PureScript:
numbers :: Array Int numbers = [2, 400, 5, 1000]largeNumbers :: Array Int -> Array Int largeNumbers = filter (\x -> x > 100)larger :: Int -> Int larger = \x -> x * 2main :: Effect Unit main = do log "\nProve that head is a natural transformation" log $ (show $ larger <$> head (largeNumbers numbers)) <> " == " <> (show $ head $ larger <$> (largeNumbers numbers))
В main, используя коммутативный закон, описанный в предыдущем уроке, мы доказываем, что head является естественным преобразованием. То есть map larger $ head (largeNumbers numbers) == head $ map larger (largeNumbers numbers). С нашими знаниями о естественных преобразованиях мы выбираем larger <$> head (largeNumbers numbers), потому что он быстрее работает с большими массивами и дает тот же результат, что и head $ larger <$> (largeNumbers numbers).
Случай 3: привязка нескольких запросов к базе данных
Наш последний пример использования для естественных преобразований включает в себя задачу запроса пользователя и, если запрос успешен, выполнение другого запроса для лучшего друга этого пользователя. Представьте, что эта база данных пользователей имеет следующие поля:
type Id = Int
type User = { id :: Id , name :: String , bestFriendId :: Id }
В этом примере все становится немного сложнее, потому что любой запрос может вернуть ошибку, если пользователь id меньше 3. Поэтому мы учтем эту ошибку, используя конструктор Either в нашей задаче:
fake :: Id -> User fake x = { id: x, name: "user" <> (show x) , best_friend_id: (x + 1) }dbFind :: Id -> TaskE Error (Either Error User) dbFind id = let query :: Id -> Either Error User query id_ = if (id_ > 2) then Right $ fake id_ else Left "not found" in taskOf $ query id
Если мы применим dbFind к пользователю id из 3, мы получим обратно Task(Right {id: 3, name: "user3", bestFriendId: 4}). В следующем запросе нам нужен лучший друг user3, имя которого user4.
Один из подходов, предполагающий, что мы не знаем о естественных преобразованиях, состоит в том, чтобы сопоставить любое с Task(Right {id: 3, name: "user3", bestFriendId: 4}), чтобы получить user3. Затем промойте и повторите, чтобы найти и вернуть user4:
notFound :: User notFound = {id: -1, name: "notFound", bestFriendId: -1}main :: Effect Unit main = do void $ launchAff $ let s = "\nFind best friend record (no natural transformations): " eitherUser = either (\x -> notFound) identity user = \x -> map eitherUser (dbFind x) bestFriend = user 3 >>= \x -> user x.bestFriendId in bestFriend # fork (\e -> Console.error $ s <> e) (\p -> Console.log (s <> (show p)))
Выход терминала:
Find best friend record (no natural transformations): { bestFriendId: 5, id: 4, name: "user4" }
Я могу засвидетельствовать, что этот код было сложно написать, не говоря уже о том, чтобы следовать! Более того, когда запись о пользователе не существует, мы возвращаем {id: -1, name: "notFound", bestFriendId: -1}, а это не то, что нам нужно. Скорее, вычисление должно завершиться ошибкой и отказаться от второго запроса.
Правильный подход — использовать естественное преобразование eitherToTask, чтобы превратить это внутреннее Either (т. е. Task(Right {id: 3, name: "user3", bestFriendId: 4})) в задачу Task(Task {id: 3, name: "user3", bestFriendId: 4}. Затем привяжите его, чтобы получить user3, промойте и повторите, чтобы получить user4:
eitherToTask :: forall a. Either Error a -> TaskE Error a eitherToTask = either (\e -> taskRejected e) (\a -> taskOf a)main :: Effect Unit main = do void $ launchAff $ let s = "\nFind best friend record (natural transformations): " in do (dbFind 3) >>= eitherToTask >>= \user -> (dbFind user.bestFriendId) >>= eitherToTask # fork (\e -> Console.error $ s <> e) (\p -> Console.log (s <> (show p)))
Намного лучше! Наши eitherToTask нашли их идентификаторы и вернули правильные результаты, несмотря на то, что мы преобразовали их в задачи. Перед вызовом fork у нас остался Task(Task {user: 4, name: "user4", bestFriendId: 5}). Функция fork решает обе задачи, и наш вывод в консоль:
Find best friend record (natural transformations): { bestFriendId: 5, id: 4, name: "user4" }
Резюме
В этом руководстве мы рассмотрели три варианта использования естественных преобразований в нашем повседневном коде. Первый случай показал, как можно использовать естественные преобразования для преобразования структуры данных в соответствии с типом, требуемым функцией. В примере мы показали, как List естественным образом преобразуется в Array с помощью Data.Array.fromFoldable. Второй пример сосредоточен на превращении первого элемента Array в конструктор типа Maybe. Это преобразование полезно в тех случаях, когда Array может быть пустым, а нам нужна безопасность. Наконец, мы показали, как естественным образом преобразовать состав нескольких запросов к базе данных, чтобы более эффективно связать их вместе. Этот подход позволяет избежать вложенных функций карты и приводит к более читаемому коду.
В следующем уроке мы перейдем к новой теме — изоморфизмам и преобразованиям данных туда и обратно. Это все на данный момент. Если вам нравятся эти уроки, помогите мне рассказать об этом другим, порекомендовав эту статью и отметив ее в социальных сетях. До следующего раза!