Мяу! Почніть використовувати Cats у своєму проекті прямо зараз

Ніжне знайомство з бібліотекою кішок.

Вступ

Cats - бібліотека, що пропонує абстракції для функціонального програмування в Scala.

В Інтернеті є декілька чудових постів та курсів про Котів (наприклад, пастухові коти та навчальний посібник із вправ Scala), але вони, як правило, вивчають категорії / типи класів, що реалізуються в бібліотеці, а не дають практичних готових до використовувати приклади використання Cats у існуючих базах коду. Цей допис у блозі ледь не дряпає поверхню того, що можуть зробити Кішки, але натомість дає короткий практичний вступ до тих моделей, якими ви найбільше можете скористатися у своєму проекті Scala. Якщо ви використовуєте будь-які монади, такі як «Майбутнє» або «Опція» щодня, велика ймовірність, що Кішки можуть спростити та покращити читабельність вашого коду.

Будь ласка, зверніться до вікі Cats на GitHub, щоб дізнатися, як додати бібліотеку до залежностей вашого проекту. Ми дотримуємось версії 0.9.0 у всій публікації.

Перегляньмо бібліотечний пакет, переглянувши синтаксис, наявний у кожному пакеті.

Помічники щодо варіантів та будь-яких

імпорт кішок.syntax.option._

Імпорт цього пакета дає можливість синтаксису obj.some - еквівалентного Some (obj). Єдина реальна відмінність полягає в тому, що значення вже перенесено на Option [T] від Some [T].

Використання obj.some замість Some (obj) іноді може покращити читабельність одиничних тестів. Наприклад, якщо ви додаєте наступний неявний клас у свій BaseSpec, TestHelper або будь-який інший базовий клас для тестів, який називається:

тоді ви можете використати ланцюговий синтаксис, показаний нижче (припустимо, що ваші одиничні тести базуються на скаламоці; також дивіться допис Бартоша Коваліка):

Це читабельніше, ніж Future.successful (Деякі (користувач)), особливо якщо ця модель часто повторюється в тестовому наборі. Пов’язання .some.asFuture в кінці, а не розміщення його спереду, також допомагає зосередитись на тому, що насправді повертається, а не на очікуваний тип обгортки.

жодна [T], в свою чергу, не є стенограмою для Option.empty [T], яка просто None, але вже оновлена ​​з None.typeto Option [T]. Надання більш спеціалізованого типу іноді допомагає компілятору Scala правильно вивести тип виразів, що містять None.

імпорт кішок.syntax.either._

obj.asRight праворуч (obj), obj.asLeft - ліворуч (obj). В обох випадках тип повернутого значення розширюється вправо або вліво або вліво. Так само, як це було у випадку дещо, ці помічники зручно поєднувати з .asfuture для покращення читабельності одиничних тестів:

Either.fromOption (варіант: Option [A], ifNone: => E), у свою чергу, є корисним помічником для перетворення Варіанту в Ефір. Якщо наданий варіант є Some (x), він стає правильним (x). В іншому випадку він стає лівимз наданим значенням ifNone всередині.

екземпляри пакунків та декартовий синтаксис

імпортувати кішок.речовини. ._

Є кілька класів типів, які є основними для котів (і, як правило, для функціонального програмування на основі категорій), найважливіші з них - Functor, Applicative та Monad. Ми не будемо детально описуватись у цій публікації блогу (див. Наприклад, уже згаданий підручник), але що важливо знати, що для використання більшості синтаксисів Cats вам також потрібно імпортувати неявні екземпляри класу типу для структур, повторно працюючи з.

Зазвичай достатньо лише імпортувати відповідний пакет котів. Наприклад, коли ви робите перетворення на ф'ючерсах, вам потрібно буде імпортувати cats.instance.future._. Відповідні пакети для опцій та списків називаються cats.instance.option._ та cats.instance.list._. Вони забезпечують неявні екземпляри класу типів, які потрібні синтаксису Cats для належної роботи.

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

імпорт кішок.синтаксія.картезіан_

Декартовий пакет надає | @ | синтаксис, який дозволяє отримати інтуїтивну конструкцію для застосування функції, яка приймає більше одного параметра до кількох ефективних значень (наприклад, ф'ючерси).

Скажімо, у нас є 3 ф'ючерси, один типу Int, один типу String, один типу User та метод, що приймає три параметри - Int, String та User.

Наша мета - застосувати функцію до значень, обчислених цими 3 ф'ючерсами. З декартовим синтаксисом це стає дуже простим і стислим:

Як було зазначено раніше, надати неявний екземпляр (а саме, декартовий [майбутнє]), необхідний для | @ | Щоб правильно працювати, слід імпортувати cats.instance.future._.

Ця вище ідея може бути виражена ще коротше, просто:

Результат вищевказаного виразу буде мати тип Future [ProcessingResult]. Якщо будь-яке з прикутих ф'ючерсів виходить з ладу, отримане майбутнє також буде невдалим за тим же винятком, що і перше невдале майбутнє в ланцюжку (це невдала поведінка). Що важливо, всі ф'ючерси працюватимуть паралельно, на відміну від того, що відбуватиметься при розумінні:

У наведеному вище фрагменті (який під кришкою перекладається на виклики flatMap і map), stringFuture не запускається, доки успішно не завершиться intFuture, і таким же чином користувачFuture буде запускатися лише після завершення stringFuture. Але оскільки обчислення не залежать одне від одного, цілком реально їх виконувати паралельно з | @ | замість цього.

Проїзд

імпорт кішок.syntax.traverse._

траверс

Якщо у вас є екземпляр obj типу F [A], який можна відобразити на карті (як Future), і функція fun типу A => G [B], тоді виклик obj.map (fun) дасть вам F [G [ Б]]. У багатьох поширених випадках, наприклад, коли F - це варіант, а G - майбутнє, ви отримаєте варіант [майбутнє [B]], який, швидше за все, не є тим, чого ви хотіли.

Тут іде рішення як траверс. Якщо ви назвете траверс замість карти, наприклад, obj.traverse (весело), ​​ви отримаєте G [F [A]], який у нашому випадку буде майбутнім [Варіант [B]]; це набагато корисніше і простіше обробити, ніж Варіант [Майбутнє [B]].

Як бічна примітка, в об'єкті Future.traverse також є виділений метод Future.traverse, але версія Cats набагато читає і може легко працювати на будь-якій структурі, для якої доступні певні класи типів.

послідовність

послідовність представляє ще простішу концепцію: її можна вважати просто заміною типів з F [G [A]] на G [F [A]], навіть не відображаючи вкладене значення, як це робить траверс.

obj.sequence насправді реалізується в Cats як obj.traverse (ідентичність). З іншого боку, obj.traverse (забава) приблизно еквівалентний obj.map (fun).

плоский перехід

Якщо у вас є obj типу F [A] і функція fun типу A => G [F [B]], тоді виконання obj.map (f) дає результат типу F [G [F [B]]] - дуже навряд чи буде те, чого ви хотіли.

Подорож по об'єкту замість картографування трохи допомагає - натомість ви отримаєте G [F [F [B]]. Оскільки G зазвичай щось подібне до майбутнього, а F - це список чи варіант, то ви закінчилися з майбутнім [Варіант [Варіант [А]] або Майбутнє [Список [Список [А]]] - трохи незручно обробляти.

Рішенням може бути відображення результату за допомогою виклику _.flatten на зразок:

і таким чином ви отримаєте бажаний тип G [F [B]] наприкінці.

Однак є акуратний ярлик для цього під назвою flatTraverse:

і це вирішує нашу проблему назавжди.

Трансформатори Монада

імпорт кішок.дані.ОпціяT

Екземпляр OptionT [F, A] може розглядатися як обгортка над F [Варіант [A]], яка додає пару корисних методів, характерних для вкладених типів, недоступних у F або Option. Найчастіше ваш F буде Future (або іноді гладким DBIO, але для цього потрібна реалізація класів типу Cats, таких як Functor або Monad for DBIO). Обгортки, такі як OptionT, як правило, відомі як монадні трансформатори.

Досить поширеною схемою є відображення внутрішнього значення, збереженого всередині екземпляра F [Варіант [A]], до екземпляра F [Варіант [B]] з функцією типу A => B. Це можна зробити з досить багатослівним синтаксисом люблю:

За допомогою OptionT це можна спростити наступним чином:

Наведена вище карта поверне значення типу OptionT [Future, String].

Щоб отримати основне майбутнє [Option [String]] значення, просто зателефонуйте .value в екземпляр OptionT. Це також життєздатне рішення повністю перейти на OptionT [Майбутнє, A] в параметрах методу / типи повернення і повністю (або майже повністю) викинути Future [Варіант [A]] у деклараціях типу.

Існує кілька способів побудови екземпляра OptionT. Заголовки методів у таблиці нижче трохи спрощені: параметри типу та класи типів, необхідні для кожного методу, пропускаються.

У виробничому коді ви найчастіше використовуєте синтаксис OptionT (...), щоб обернути екземпляр Future [Option [A]] в Option [F, A]. Інші методи, у свою чергу, виявляються корисними для встановлення макетних значень типу OptionT в тестових одиницях.

Ми вже зустріли один із методів OptionT, а саме карту. Існує кілька інших методів, і вони в основному відрізняються підписом функції, яку вони приймають за параметр. Як і у попередній таблиці, очікувані типи класів пропускаються.

На практиці ви, швидше за все, використовуєте карту та semiflatMap.

Як це завжди буває з flatMap та картою, ви можете використовувати його не тільки явно, але й під кришкою для розуміння, як у прикладі нижче:

Екземпляр OptionT [Майбутнє, гроші], повернутий getReservedFundsForUser, додасть Nonevalue, якщо будь-який з трьох складених методів поверне OptionT, відповідний None. В іншому випадку, якщо результат усіх трьох дзвінків містить Деякі, кінцевий результат також міститиме Деякий.

імпортувати кішок.даних

EitherT [F, A, B] - це трансформатор монади для Either - ви можете розглядати це як обгортку над значенням F [Either [A, B]].

Так само, як у наведеному вище розділі, я спростив заголовки методів, пропускаючи параметри типу або їх контекстні та нижчі межі.

Давайте коротко розглянемо, як створити екземпляр EitherT:

Просто для уточнення: EitherT.fromEither загортає надане Either у F, тоді як EitherT.right та EitherT.left загортають значення всередині наданого F управо та вліво відповідно. У свою чергу абоT.pure перетворює надане значення B в право, а потім у F.

Іншим корисним способом побудови екземпляра EitherT є використання методів OptionT toLeft and toRight:

toRight є досить аналогічним згаданому раніше методу Either.fromOption: подібно до того, як fromOptionбудував Either з Option, toRight створює EitherT з OptionT. Якщо оригінальний параметр OptionTstores деяке значення, він буде перетворений в правий; інакше значення, подане як лівий параметр, буде загорнуте в лівий.

toLeft - це аналог toRight, який перетворює деяке значення в лівий і перетворює жодне в правильне, закриваючи задане правильне значення. Це рідше використовується на практиці, але може слугувати, наприклад, для забезпечення унікальності перевірок у коді. Ми повертаємо Left, якщо значення було знайдено, і Right, якщо воно ще не існує в системі.

Методи, доступні в EitherT, дуже схожі на методи, які ми бачили в OptionT, але є деякі помітні відмінності. Ви можете спочатку потрапити в деяку плутанину, якщо мова йде про напр. карта. У випадку з OptionT було досить очевидно, що потрібно зробити: карта повинна перейти через Варіант, який міститься у Future, а потім відобразити карту самої опції. Це трохи менш очевидно у випадку EitherT: чи слід відображати обидва значення Leftand Right або лише значення Right?

Відповідь полягає в тому, що EitherT є упередженим правом, тому звичайна карта насправді має справу з правильним значенням. Це на відміну від стандартної бібліотеки Scala до 2,11, яка, в свою чергу, є неупередженою: в Either немає жодної карти, лише для її лівої та правої проекції.

Сказавши це, давайте коротко розглянемо правильні упереджені методи, які пропонує EitherT [F, A, B]:

Як бічна примітка, в EitherT також існують певні методи (які вам, можливо, знадобляться в якийсь момент), які відображають значення Left, як leftMap, або над значеннями Left and Right, наприклад, fold або bimap.

EitherT дуже корисний для невдалої перевірки ланцюга:

У наведеному вище прикладі ми проводимо різні перевірки щодо елемента по черзі. Якщо будь-яка з перевірок не вдається, отриманий EitherT буде містити значення Left. В іншому випадку, якщо всі чеки дають право (звичайно, ми маємо на увазі Право, загорнуте в EitherT), то кінцевий результат також буде містити Right. Це невдала поведінка: ми ефективно зупиняємо потік для розуміння при першому результаті Left-ish.

Якщо ви замість цього шукаєте перевірку, яка накопичує помилки (наприклад, при роботі з наданими користувачем даними форми), cats.data.Validated може бути хорошим вибором.

Загальні проблеми

Якщо що-небудь не складається, як очікувалося, спочатку переконайтеся, що всі необхідні копії Cats знаходяться в межах сфери - просто спробуйте імпортувати cats.implicits._ і перевірте, чи проблема не зникає. Як уже згадувалося раніше, хоча краще використовувати вузький імпорт, але якщо код не компілюється, іноді варто просто імпортувати всю бібліотеку, щоб перевірити, чи вирішує вона проблему.

Якщо ви використовуєте Futures, не забудьте вказати неявний ExecutionContext в області застосування, інакше Cats не зможе зробити висновок про неявні екземпляри для типів типів Future.

У компілятора досить часто можуть виникнути проблеми з висновком параметрів типу для траверсних та послідовних методів. Очевидним способом вирішення є те, щоб вказати такі типи безпосередньо, як list.traverse [Майбутнє, одиниця] (весело). У певних випадках це може стати досить багатослівним, і кращим способом є спробувати еквівалентні методи traverseU і послідовностіU, як list.traverseU (fun). Вони виконують хитрість на рівні типу (з cat.Unapply, отже, U), щоб допомогти компілятору вивести параметри типу.

IntelliJ інколи повідомляє про помилки в завантаженому кодом коді, навіть якщо джерело передається під масштабом. Одним із таких прикладів є виклики методів cat.data.Nested, які компілюються правильно під scalac, але не вводять перевірку під компілятором презентації IntelliJ. Однак він повинен працювати без проблем під Scala IDE.

Як порада для вашого майбутнього навчання: клас додаткового типу, незважаючи на його ключове значення у функціональному програмуванні, зрозуміти трохи важко. На мою думку, він набагато менш інтуїтивний, ніж Функтор чи Монада, навіть якщо він насправді стоїть прямо між Функтором та Монадою в ієрархії спадкування. Найкращий підхід до розуміння застосунку - спершу зрозуміти, як працює продукт (який перетворює F [A] і F [B] в F [(A, B)]), а не зосереджуватися на деякій екзотичній самій операції.