Skip to content

Commit

Permalink
Merge pull request #21 from ivanzusko/feature/ch11
Browse files Browse the repository at this point in the history
Feature: Chapter 11
  • Loading branch information
ivanzusko committed Jun 2, 2024
2 parents 526bf50 + 8479079 commit e97c95a
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 1 deletion.
2 changes: 1 addition & 1 deletion ch10-uk.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ IO.of(compose).ap(u).ap(v).ap(w) === u.ap(v.ap(w));

Ми майже завершили з контейнерними API. Ми навчилися мапити (`map`) функції, поєднувати їх в ланцюги (`chain`), і тепер ще й використовувати `ap`. У наступному розділі ми дізнаємося, як краще працювати з кількома функторами та розбирати їх за принципами.

[Глава 11: Трансформація Знову, Звісно](ch11.md)
[Глава 11: Трансформація Знову, Природньо](ch11.md)

## Вправи

Expand Down
255 changes: 255 additions & 0 deletions ch11-uk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Розділ 11: Знову перетворення, природньо

Ми збираємося обговорити *природні перетворення* в контексті практичної корисності в повсякденному коді. Так вже сталось, що вони є основою теорії категорій і абсолютно незамінні при застосуванні математики для розмірковування про наш код і його рефакторинг. Тому, я вважаю своїм обов'язком повідомити вас про прикру несправедливість, свідками якої ви станете, безсумнівно, через обмеженість моїх можливостей. Почнемо.

## Прокляття Цьому Гнізду

Я хотів би торкнутися питання гніздування (Тут гра слів, бо "nesting" може також бути перекладене як "вкладеність"). Не інстинктивне бажання, яке відчувають майбутні батьки, коли вони прибирають і переставляють речі з нав'язливою ідеєю, але... ну, насправді, якщо подумати, то це не так вже й далеко від істини, як ми побачимо в наступних розділах... У будь-якому випадку, що я маю на увазі під *вкладеністю*, це коли два або більше різних типи зібрані разом навколо значення, як новонародженого, так би мовити.

```js
Right(Maybe('b'));

IO(Task(IO(1000)));

[Identity('bee thousand')];
```

До цього часу нам вдавалося уникнути цього поширеного сценарію за допомогою ретельно підібраних прикладів, але на практиці, коли пишеш код, типи схильні заплутуватись між собою, як навушники під час екзорцизму. Якщо ми не будемо ретельно тримати наші типи організованими по ходу справи, наш код стане таким заплутаним, як бородатий поет в котячому кафе.

## Ситуаційна Комедія

```js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment)))
const saveComment = compose(
map(map(map(postComment))),
map(map(validate)),
getValue('#comment'),
);
```

Вся банда в зборі, на превеликий жах нашого типового підпису. Дозвольте мені коротко пояснити код. Ми починаємо з отримання користувацького вводу за допомогою `getValue('#comment')`, що є дією, яка отримує текст з елемента. При цьому може виникнути помилка при пошуку елемента або значення рядка може не існувати, тому повертається `Task Error (Maybe String)`. Після цього ми повинні скористатись `map` як для `Task`, так і для `Maybe`, щоб передати наш текст у `validate`, який у свою чергу, повертає нам `Either` `ValidationError` або наш `String`. Далі ми використовуємо мапінг для того, щоб відправити `String` у нашому поточному `Task Error (Maybe (Either ValidationError String))` в `postComment`, який повертає наш кінцевий `Task`.

Яка жахливий безлад. Колаж абстрактних типів, аматорський типовий експресіонізм, поліморфний Поллок, монолітний Мондріан. Існує багато рішень цієї поширеної проблеми. Ми можемо скомпонувати типи в один монструозний контейнер, сортувати та "обʼєднати" (`join`) кілька, гомогенізувати їх, деконструювати їх і так далі. У цьому розділі ми зосередимося на гомогенізації їх за допомогою *природних перетворень*.

## Все Природньо

*Природнє перетворення* — це "морфізм між функторими", тобто функція, яка оперує самими контейнерами. Типово це функція `(Functor f, Functor g) => f a -> g a`. Особливістю цієї функції є те, що ми не можемо з будь-якої причини заглядати всередину нашого функтора. Подумайте про це як про обмін високо засекреченою інформацією — дві сторони не знають, що знаходиться в запечатаному конверті з позначкою "цілком таємно". Це структурна операція. Функторіальна зміна костюма. Формально, *природнє перетворення* — це будь-яка функція, для якої виконується наступне:

<img width=600 src="images/natural_transformation.png" alt="схема природнього перетворення" />

або в коді:

```js
// nt :: (Functor f, Functor g) => f a -> g a
compose(map(f), nt) === compose(nt, map(f));
```

Як діаграма, так і код кажуть одне й те саме: Ми можемо спочатку виконати наше природнє перетворення, а потім використати `map`, або спочатку застосувати `map`, а потім виконати наше природнє перетворення та отримати той самий результат. До речі, це випливає з [вільної теореми](ch07-uk.md#вільно-як-у-теоремі), хоча природні перетворення (і функції) не обмежуються функціями на типах.

## Принципові Перетворення Типів

Як програмісти, ми знайомі з перетвореннями типів. Ми перетворюємо типи, як наприклад, `Strings` в `Booleans` і `Integers` в `Floats` (хоча в JavaScript є тільки `Numbers`). Різниця тут полягає в тому, що ми працюємо з алгебраїчними контейнерами і у нас є деяка теорія в нашому розпорядженні.

Давайте розглянемо деякі з них як приклади:

```js
// idToMaybe :: Identity a -> Maybe a
const idToMaybe = x => Maybe.of(x.$value);

// idToIO :: Identity a -> IO a
const idToIO = x => IO.of(x.$value);

// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);

// ioToTask :: IO a -> Task () a
const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform()));

// maybeToTask :: Maybe a -> Task () a
const maybeToTask = x => (x.isNothing ? Task.rejected() : Task.of(x.$value));

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);
```

Бачите ідею? Ми просто змінюємо один функтор на інший. Нам дозволено втрачати інформацію по ходу справи, доки значення, яке ми будемо мапити, не втрачається в процесі зміни форми. У цьому полягає вся суть: `map` повинен продовжувати працювати, відповідно до нашого визначення, навіть після перетворення.

Один із способів поглянути на це полягає в тому, що ми трансформуємо наші ефекти. У цьому світлі, ми можемо розглядати `ioToTask` як перетворення синхронного на асинхронне або `arrayToMaybe` від недетермінованості до можливої невдачі. Зверніть увагу, що ми не можемо перетворити асинхронне на синхронне в JavaScript, тому ми не можемо написати `taskToIO` - це було б надприродним перетворенням.

## Функція Заздрість

Припустимо, ми хочемо використовувати деякі функції з іншого типу, як наприклад `sortBy` для `List`. *Природні перетворення* надають чудовий спосіб перетворити в цільовий тип, знаючи, що наш `map` буде працювати належним чином.

```js
// arrayToList :: [a] -> List a
const arrayToList = List.of;

const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f));
const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList); // law applied
```

Трішки поворушимо носом, тричі стукнемо чарівною паличкою, додамо `arrayToList`, і вуаля! Наш `[a]` стає `List a`, і ми можемо використовувати `sortBy`, якщо хочемо.

Також стає легше оптимізувати або об'єднувати операції, переміщуючи `map(f)` ліворуч від *природнього перетворення*, як показано в `doListyThings_`.

## Ізоморфний JavaScript

Коли ми можемо повністю переходити туди й назад без втрати будь-якої інформації, це вважається *ізоморфізмом*. Це просто красиве слово, що означає "зберігає ті самі дані". Ми кажемо, що два типи є *ізоморфними*, якщо ми можемо надати *природні перетворення* "до" і "від" як доказ:

```js
// promiseToTask :: Promise a b -> Task a b
const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));

// taskToPromise :: Task a b -> Promise a b
const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));

const x = Promise.resolve('ring');
taskToPromise(promiseToTask(x)) === x;

const y = Task.of('rabbit');
promiseToTask(taskToPromise(y)) === y;
```

Q.E.D. `Promise` і `Task` є *ізоморфними*. Ми також можемо написати `listToArray`, щоб доповнити наш `arrayToList` і показати, що вони теж ізоморфні. Як контрприклад, `arrayToMaybe` не є *ізоморфізмом*, оскільки втрачає інформацію:

```js
// maybeToArray :: Maybe a -> [a]
const maybeToArray = x => (x.isNothing ? [] : [x.$value]);

// arrayToMaybe :: [a] -> Maybe a
const arrayToMaybe = x => Maybe.of(x[0]);

const x = ['elvis costello', 'the attractions'];

// не ізоморфні
maybeToArray(arrayToMaybe(x)); // ['elvis costello']

// але є природнім перетворенням
compose(arrayToMaybe, map(replace('elvis', 'lou')))(x); // Just('lou costello')
// ==
compose(map(replace('elvis', 'lou')), arrayToMaybe)(x); // Just('lou costello')
```

Вони дійсно є *природними перетвореннями*, оскільки `map` з обох боків дає однаковий результат. Я згадую про *ізоморфізми* тут, в середині розділу, але нехай це не вводить вас в оману, це надзвичайно потужна і всеосяжна концепція. У будь-якому разі, продовжимо.

## Ширше Визначення

Ці структурні функції жодним чином не обмежуються перетвореннями типів.

Ось декілька різних прикладів:

```hs
reverse :: [a] -> [a]

join :: (Monad m) => m (m a) -> m a

head :: [a] -> a

of :: a -> f a
```

Закони природних перетворень діють також і для цих функцій. Єдине, що може збити вас з пантелику, це те, що `head :: [a] -> a` можна розглядати як `head :: [a] -> Identity a`. Ми можемо вставляти `Identity` де завгодно, доводячи закони, оскільки можемо, в свою чергу, довести, що `a` є ізоморфна `Identity a` (бачите, я казав, що *ізоморфізми* є всепроникними).

## Одне Рішення Вкладеності

Повернімося до нашого комедійного підпису типу. Ми можемо додати деякі *природні перетворення* до викликаючого коду, щоб зробити кожен тип, що змінюється, однорідним, а отже, таким, що може бути приєднаяним (з використанням `join`).

```js
// getValue :: Selector -> Task Error (Maybe String)
// postComment :: String -> Task Error Comment
// validate :: String -> Either ValidationError String

// saveComment :: () -> Task Error Comment
const saveComment = compose(
chain(postComment),
chain(eitherToTask),
map(validate),
chain(maybeToTask),
getValue('#comment'),
);
```

Отже, що ми тут маємо? Ми просто додали `chain(maybeToTask)` і `chain(eitherToTask)`. Обидва мають однаковий ефект; вони природньо перетворюють функтор, який тримає наш `Task`, на інший `Task`, а потім `join`ять їх. Як шипи проти голубів на підвіконні, ми уникаємо вкладеності прямо на джерелі. Як кажуть у місті світла, "Mieux vaut prévenir que guérir" - краще запобігти, ніж лікувати.

## У Підсумку

*Природні перетворення* — це функції, які застосовуються до наших функторів. Вони є надзвичайно важливою концепцією в теорії категорій і почнуть з'являтися всюди, як тільки буде прийнято більше абстракцій, але наразі ми обмежилися кількома конкретними застосуваннями. Як ми побачили, ми можемо досягати різних ефектів, перетворюючи типи з гарантією, що наша композиція буде працювати. Вони також можуть допомогти нам з вкладеними типами, хоча вони мають загальний ефект гомогенізації наших функторів до найменшого спільного знаменника, який на практиці є функтором з найбільш мінливими ефектами (в більшості випадків `Task`).

Це безперервне і нудне сортування типів - ціна, яку ми платимо за їх матеріалізацію - виклик їх з ефіру. Звичайно, неявні ефекти набагато підступніші, і тому ми ведемо цю справедливу боротьбу. Нам знадобиться ще кілька інструментів, перш ніж ми зможемо впоратися з більшими злиттями типів. Далі ми розглянемо, як змінити порядок наших типів за допомогою *Traversable*.

[Розділ 12: Перехід через Камінь](ch12-uk.md)


## Вправи

{% exercise %}
Напишіть природнє перетворення, яке перетворює `Either b a` у `Maybe a`

{% initial src="./exercises/ch11/exercise_a.js#L3;" %}
```js
// eitherToMaybe :: Either b a -> Maybe a
const eitherToMaybe = undefined;
```


{% solution src="./exercises/ch11/solution_a.js" %}
{% validation src="./exercises/ch11/validation_a.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}


---


```js
// eitherToTask :: Either a b -> Task a b
const eitherToTask = either(Task.rejected, Task.of);
```

{% exercise %}
Використовуючи `eitherToTask`, спростіть `findNameById`, щоб видалити вкладені `Either`.

{% initial src="./exercises/ch11/exercise_b.js#L6;" %}
```js
// findNameById :: Number -> Task Error (Either Error User)
const findNameById = compose(map(map(prop('name'))), findUserById);
```


{% solution src="./exercises/ch11/solution_b.js" %}
{% validation src="./exercises/ch11/validation_b.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}


---


Нагадуємо, що в контексті вправи доступні наступні функції:

```hs
split :: String -> String -> [String]
intercalate :: String -> [String] -> String
```

{% exercise %}
Напишіть ізоморфізм між String та [Char].

{% initial src="./exercises/ch11/exercise_c.js#L8;" %}
```js
// strToList :: String -> [Char]
const strToList = undefined;

// listToStr :: [Char] -> String
const listToStr = undefined;
```


{% solution src="./exercises/ch11/solution_c.js" %}
{% validation src="./exercises/ch11/validation_c.js" %}
{% context src="./exercises/support.js" %}
{% endexercise %}

0 comments on commit e97c95a

Please sign in to comment.