Примечание. Это Урок 25 из серии "Переход с JavaScript на PureScript". Обязательно прочитайте введение в серию, где мы расскажем о целях и общих чертах, а также об установке, компиляции и запуске PureScript. Я буду публиковать новый учебник примерно раз в месяц. Так что заходите почаще, впереди еще много интересного!

Индекс | ‹‹ Введение‹ Урок 24 | Урок 26 › Урок 27 ››

В последнем уроке мы начали рассматривать естественные преобразования в функциональном программировании — что они из себя представляют и каковы их законы. В этом руководстве мы продолжим эту тему, показав, как использовать естественные преобразования в вашем коде. Напомним, что естественное преобразование — это функция, которая переводит функтор, содержащий некоторое 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 * 2
main :: 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 может быть пустым, а нам нужна безопасность. Наконец, мы показали, как естественным образом преобразовать состав нескольких запросов к базе данных, чтобы более эффективно связать их вместе. Этот подход позволяет избежать вложенных функций карты и приводит к более читаемому коду.

В следующем уроке мы перейдем к новой теме — изоморфизмам и преобразованиям данных туда и обратно. Это все на данный момент. Если вам нравятся эти уроки, помогите мне рассказать об этом другим, порекомендовав эту статью и отметив ее в социальных сетях. До следующего раза!