diff --git a/ch12-ru.md b/ch12-ru.md index 39f2e15..1ba0688 100644 --- a/ch12-ru.md +++ b/ch12-ru.md @@ -2,7 +2,7 @@ К этому моменту вы уже видели в нашем Контейнерном Цирке, как мы укротили свирепого [функтора](ch08-ru.md#мой-первый-функтор), подчиняя его нашей воле и заставляя его выполнять любые операции, какие нам заблагорассудится. Вы были поражены талантом жонглирования множеством опасных эффектов одновременно, используя [применение](ch10-ru.md) функций для сбора результатов. Были приведены в изумление, когда контейнеры исчезали в воздухе, [соединяясь](ch09-ru.md) вместе. На побочном шоу побочных эффектов мы видели контейнеры [составленными в композицию](ch08-ru.md#немного-теории). И совсем недавно мы расширили границы естественного и [преобразовали](ch11-ru.md) один тип в другой прямо на ваших глазах. -А теперь, для нашего следующего трюка, мы изучим проходы. Мы увидим, как типы парят друг над другом, как если бы они были воздушными гимнастами, держащими в своих руках наши значения. Мы будем крутить эффектами, как местами на вращающемся аттракционе. Когда наши контейнеры переплетутся, как конечности акробата, мы сможем использовать этот интерфейс для того, чтобы выровнять их и привести в порядок. Мы будем свидетелями разных эффектов в самом разном порядке. Подайте мои штаны и свисток, мы начинаем! +А теперь, для нашего следующего трюка, мы изучим проходы. Мы увидим, как типы парят друг над другом, как если бы они были воздушными гимнастами, держащими в своих руках наши значения. Мы будем крутить эффектами, как креслами на вращающемся аттракционе. Когда наши контейнеры переплетутся, как конечности акробата, мы сможем использовать этот интерфейс для того, чтобы выровнять их и привести в порядок. Мы будем свидетелями разных эффектов в самом разном порядке. Подайте мои шаровары и свисток, мы начинаем! ## Типы застряли друг в друге @@ -21,7 +21,7 @@ map(tldr, ['file1', 'file2']); // [Task('hail the monarchy'), Task('smash the patriarchy')] ``` -В этом примере мы читаем несколько файлов и получаем массив `Task`, с которым неясно, как поступить. Как нам применить `fork` к каждой из них? Было бы здорово, если бы мы могли просто поменять типы местами так, чтобы получить `Task Error [String]` вместо `[Task Error String]`. Тогда у нас было бы единственное значение «из будущего», которое содержало бы сразу все результаты. Это соответствовало бы нашим асинхронным потребностям намного больше, чем оперировать несколькими значениями, которые поступят к нам в том порядке, в котором им заблагорассудится. +В этом примере мы читаем несколько файлов и получаем массив `Task`, с которым неясно, как поступить. Как нам применить `fork` к каждой из них? Было бы здорово, если бы мы могли просто поменять типы местами так, чтобы получить `Task Error [String]` вместо `[Task Error String]`. Тогда у нас было бы единственное значение «из будущего», которое содержало бы сразу все результаты. Это соответствовало бы нашим асинхронным потребностям намного больше, чем оперирование несколькими значениями, которые поступят к нам в том порядке, в котором им заблагорассудится. Вот еще один пример непростой ситуации: @@ -58,7 +58,7 @@ const sequence = curry((of, x) => x.sequence(of)); Давайте начнем со второго аргумента. Это должен быть *Traversable*, в котором «содержится» *Applicative*, что звучит довольно ограничивающе, но обычно это условие несложно выполнить. Это и есть `t (f a)`, который превращается в `f (t a)`. Весьма выразительно, не так ли? -А вот первый аргумент — это функция, которая помогает нам сконструировать значение (тот самый `of`). Она нужна для того, чтобы вывернуть значения, которые не пожелают `map`иться (подробнее об этом — через минуту). _На самом деле наличие его как аргумента `sequence` — это просто костыль, который нужен только в нетипизированном языке, где компилятор не сможет подобрать подходящую функцию, исходя только из типа._ +А вот первый аргумент — это функция, которая помогает нам сконструировать значение (тот самый `of`). Она нужна для того, чтобы вывернуть значения, которые не пожелают `map`иться. _На самом деле, наличие его как аргумента `sequence` — это просто костыль, который нужен только в нетипизированном языке, где компилятор не сможет подобрать подходящую функцию, исходя только из типа._ Используя `sequence`, мы можем переставлять типы точно и ловко, будто канатоходец. Но как это работает? Давайте, к примеру, рассмотрим реализацию для типа `Either`: @@ -71,7 +71,7 @@ class Right extends Either { } ``` -Ах да, поскольку `$value` является функтором (а оно должно быть, как минимум, аппликативным функтором), мы можем просто применить `map` к `$value`, отображая `Either.of`, а затем вернуть его, таким образом, «перепрыгивая» через тип. +Ах да, поскольку `$value` является функтором (а оно должно быть, как минимум, аппликативным функтором), мы можем просто применить `map` к `$value`, отображая `Either.of`, а затем вернуть его, таким образом «перепрыгивая» через тип. Вы могли заметить, что мы полностью проигнорировали `of`, переданный в аргументах. Он передается для случая, когда отображение не к чему применять, как в случае `Left`: @@ -88,7 +88,7 @@ class Left extends Either { ## Ассортимент эффектов -Различный порядок подталкивает нас к тому, чтобы видеть в типах разный смысл. К примеру, `[Maybe a]` — коллекция возможно имеющихся значений, а `Maybe [a]` — это возможно имеющаяся коллекция значений. Первое означает, что мы толерантны к отсутствию некоторых значений, и нас вполне удовлетворят те, что имеются. Второе же выражает подход «всё или ничего». Подобным образом, `Either Error (Task Error a)` мог бы выражать валидацию на стороне клиента, а `Task Error (Either Error a)` — валидацию на стороне сервера. Реорганизация типов даёт нам возможность получать разные эффекты. +Различный порядок подталкивает нас к тому, чтобы видеть в типах разный смысл. К примеру, `[Maybe a]` — коллекция возможно имеющихся значений, а `Maybe [a]` — это возможно имеющаяся коллекция значений. Первое означает, что мы толерантны к отсутствию некоторых значений, и нас вполне удовлетворят те, что имеются. Второе же выражает подход «всё или ничего». Подобным образом `Either Error (Task Error a)` мог бы выражать валидацию на стороне клиента, а `Task Error (Either Error a)` — валидацию на стороне сервера. Реорганизация типов даёт нам возможность получать разные эффекты. ```js // fromPredicate :: (a -> Bool) -> a -> Either e a @@ -100,7 +100,7 @@ const partition = f => map(fromPredicate(f)); const validate = f => traverse(Either.of, fromPredicate(f)); ``` -В этом примере рассмотрены две разные функции, одна основана на применении `map`, другая — на `traverse`. Первая, `partition`, произведёт для нас массив значений `Left` или `Right`, в соответствии с предикатом. Такое поведение полезно в том случае, если мы планируем что-либо делать со значениями Left, а не просто отфильтровывать их сразу же при получении. А вторая функция, `validate`, вернёт нам либо `Left` с первым элементом, который не соответствует предикату, либо `Right` со списком всех элементов, если предикату удовлетворяют все. Выбирая другой порядок типов, мы получаем другое поведение. +В этом примере рассмотрены две разные функции: одна основана на применении `map`, другая — на `traverse`. Первая — `partition` — произведёт для нас массив значений `Left` или `Right` в соответствии с предикатом. Такое поведение полезно в том случае, если мы планируем что-либо делать со значениями Left, а не просто отфильтровывать их сразу же при получении. А вторая функция `validate` вернёт нам либо `Left` с первым элементом, который не соответствует предикату, либо `Right` со списком всех элементов, если предикату удовлетворяют все. Выбирая другой порядок типов, мы получаем другое поведение. Давайте посмотрим, как функция `traverse` определена для `List`, чтобы разобраться в работе `validate`. @@ -113,7 +113,7 @@ traverse(of, fn) { } ``` -Она просто применяет `reduce` к этому списку. Функция, с которой производится свертка — это `(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f)`, и выглядит она несколько страшно, поэтому давайте рассмотрим её подробнее. +Она просто применяет `reduce` к этому списку. Функция, с которой производится свёртка — это `(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f)`, и выглядит она несколько страшно, поэтому давайте рассмотрим её подробнее. 1. `reduce(..., ...)` @@ -160,7 +160,7 @@ traverse(Task.of, tldr, ['file1', 'file2']); // Task(['hail the monarchy', 'smash the patriarchy']); ``` -Используя `traverse` вместо `map`, нам удалось собрать вместе все эти непослушные `Task` в один аккуратный массив результатов. Это сработало как `Promise.all()` (если вам доводилось его использовать), за исключением того, что `traverse` — это не какая-то специальная и единственная в своём роде функция — нет, она доступна для каждого *traversable* типа. Такие математические интерфейсы, как правило, отражают большинство преобразований, которые мы хотели бы произвести, и позволяют делать это универсальным способом, способствующим взаимодействию и композиции, а не подталкивая к разработке разрозненных библиотек, в которых для очередного типа все функции изобретаются заново. +Используя `traverse` вместо `map`, нам удалось собрать вместе все эти непослушные `Task` в один аккуратный массив результатов. Это сработало как `Promise.all()` (если вам доводилось его использовать), за исключением того, что `traverse` — это не какая-то специальная и единственная в своём роде функция; нет — она доступна для каждого *traversable* типа. Такие математические интерфейсы, как правило, отражают большинство преобразований, которые мы хотели бы произвести, и позволяют делать это универсальным способом, способствующим взаимодействию и композиции, а не подталкивая к разработке разрозненных библиотек, в которых для очередного типа все функции изобретаются заново. Давайте напоследок наведём порядок в последнем примере. @@ -176,11 +176,11 @@ const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria ## Законы и архитектурный беспорядок -А теперь, прежде чем вы пуститесь в осуждение и ударите по кнопке Backspace, как молотком по столу, чтобы поскорее оставить этот раздел, задержитесь ещё на минуту и осознайте, насколько полезные гарантии дают вашему коду законы. +Не торопитесь пропускать раздел с законами — задержитесь ещё на минуту, чтобы разобраться, какие гарантии они могут дать вашему коду. -Я предполагаю, что целью большинства программных архитектур является попытка наложить полезные ограничения на код, сузить возможности, подводя нас к правильным ответам (и как проектировщиков), и как читателей. +Я предполагаю, что целью большинства программных архитектур является попытка наложить полезные ограничения на код, сузить возможности, подводя нас к правильным ответам как в процессе проектирования, так и в процессе чтения. -Но интерфейс без законов — просто косвенность. Как и для любой другой математической структуры, нам следует огласить свойства *Traversable* (как минимум, ради поддержания собственного здравомыслия). Это служит той же цели, что и инкапсуляция, поскольку защищает данные, и это позволяет нам заменять типы, реализующие интерфейс, на другие, пока они подчиняются тем же законам. +Но интерфейс без законов — просто косвенность. Как и для любой другой математической структуры, нам следует огласить свойства *Traversable* (как минимум, ради поддержания собственного здравомыслия). Дополнение интерфейсов законами служит той же цели, что и инкапсуляция (поскольку защищает данные), и это позволяет нам заменять реализующие интерфейс типы другими, пока они подчиняются тем же законам. Следуйте за мной, у нас тут есть несколько законов, с которыми необходимо разобраться. @@ -198,7 +198,7 @@ identity2(Either.of('stuff')); // Identity(Right('stuff')) ``` -Здесь всё довольно ясно. Если мы поместим `Identity` в наш функтор, а затем вывернем его наизнанку при помощи `sequence`, то должны получить такой же результат, как если бы просто поместили его снаружи. Мы возьмём `Either` в качестве подопытного, поскольку на нём будет просто проверить выполнение закона. Вас могло удивить, что мы используем именно `Identity`, хотя могли бы использовать любой другой функтор. Вспомните: [категория](ch05-ru.md#теория-категорий) определяется морфизмами между объектами, ассоциативной композицией и тождественным морфизмом. В категории функторов натуральные преобразования — это морфизмы, а `Identity`, как ни странно, тождество. Функтор `Identity` имеет настолько же важное значение в процессе демонстрации выполнения законов, как и функция `compose`. На самом деле, нам пора перестать ходить вокруг да около, и вооружиться функтором [Compose](ch08-ru.md#немного-теории) из восьмой главы: +Здесь всё довольно ясно. Если мы поместим `Identity` в наш функтор, а затем вывернем его наизнанку при помощи `sequence`, то должны получить такой же результат, как если бы просто поместили его снаружи. Мы возьмём `Either` в качестве подопытного, поскольку на нём будет просто проверить выполнение закона. Вас могло удивить, что мы используем именно `Identity`, хотя могли бы использовать любой другой функтор. Вспомните: [категория](ch05-ru.md#теория-категорий) определяется морфизмами между объектами, ассоциативной композицией и тождественным морфизмом. В категории функторов натуральные преобразования — это морфизмы, а `Identity`, как ни странно, тождество. Функтор `Identity` имеет настолько же важное значение в процессе демонстрации выполнения законов, как и функция `compose`. На самом деле, нам пора перестать ходить вокруг да около и вооружиться функтором [Compose](ch08-ru.md#немного-теории) из восьмой главы: ### Композиция @@ -214,7 +214,7 @@ comp2(Either.of, Array)(Identity(Right([true]))); // Compose(Right([Identity(true)])) ``` -Этот закон требует от композиции предсказуемого поведения: если мы поменяем порядок построения композиции функторов, нас не должно поджидать никаких сюрпризов, поскольку композиция тоже является функтором. Мы выбрали для проверки произвольные значения `true`, `Right`, `Identity` и `Array`. Существуют способы автоматизироваль проверку выполнения законов с помощью property-based тестирования, этой цели служат такие библиотеки, как [quickcheck](https://hackage.haskell.org/package/QuickCheck) или [jsverify](http://jsverify.github.io/). +Этот закон требует от композиции предсказуемого поведения: если мы поменяем порядок построения композиции функторов, нас не должны поджидать никакие сюрпризы, поскольку композиция тоже является функтором. Мы выбрали для проверки произвольные значения `true`, `Right`, `Identity` и `Array`. Существуют способы автоматизировать проверку выполнения законов с помощью property-based тестирования; этой цели служат такие библиотеки, как [quickcheck](https://hackage.haskell.org/package/QuickCheck) или [jsverify](http://jsverify.github.io/). Из вышеупомянутого закона естественным образом следует возможность [совмещать проходы](https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf) _(fuse traversals)_, что здорово сказывается на производительности. @@ -248,7 +248,7 @@ traverse(A.of, A.of) === A.of; ## Итог -*Traversable* — это мощный интерфейс, который дает нам возможность реорганизовывать типы также легко, как если бы мы могли мыслями двигать мебель и менять интерьер. Мы можем получать различные эффекты в разном порядке, а также разглаживать неприятные «морщины» из типов, которые препятствуют применению `join`. В следующей главе мы изучим один из самых мощных интерфейсов в функциональном программировании, а может быть и во всей алгебре: [Глава 13: Monoids bring it all together](ch13-ru.md) _(ещё не опубликована в оригинальной книге)_. +*Traversable* — это мощный интерфейс, который даёт нам возможность реорганизовывать типы так же легко, как если бы мы могли мыслями двигать мебель и менять интерьер. Мы можем получать различные эффекты в разном порядке, а также разглаживать неприятные «морщины» из типов, которые препятствуют применению `join`. В следующей главе мы изучим один из самых мощных интерфейсов в функциональном программировании, а может быть и во всей алгебре: [Глава 13: Monoids bring it all together](ch13-ru.md) _(ещё не опубликована в оригинальной книге)_. ## Упражнения