Треки
/
Elm
Elm
/
Вправи
/
Фабрика скарбів
Фабрика скарбів

Фабрика скарбів

Навчальна вправа

Вступ

Фантомні типи

У Elm типи можуть бути узагальненими, а це зазвичай додає гнучкості інтерфейсу. Наприклад, тип Maybe a може містити значення будь-якого типу.

type Maybe a = Nothing | Just a

Звернімо увагу, що в наведеному вище прикладі параметр типу a використовується з обох боків знака рівності =. Кажемо, що a звʼязаний з якимись даними всередині визначення типу.

Техніка фантомного типу

Однак у деяких випадках параметр типу трапляється лише в лівій частині рівності.

type Distance unit = Distance Float

У наведеному вище визначенні Distance параметр unit вільний і не звʼязаний з жодними даними в типі. Такий тип ще називають фантомним типом.

Це напрочуд корисно, коли ми хочемо накладати обмеження під час компіляції. Наприклад, ми хочемо переконатися, що додаємо лише відстані з однаковою одиницею виміру.

-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)

-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float

-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot

-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0

-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048

-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)

Код поза цим модулем не має доступу до внутрішньої будови типу Distance. Користувачі можуть додати два значення Distance Meter, але не можуть додати Distance Meter до Distance Foot.

import Distance exposing (Distance)

-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter

-- Does not compile
errDist = Distance.add Distance.meter Distance.foot

Розширювані записи як фантомні типи

Немає жодних обмежень на те, які типи можуть бути фантомними, тож типи-записи теж підходять. Коли записи використовують як фантомні типи, вони здатні виражати складні обмеження, які можуть перетворюватися за допомогою функцій.

Додамо до модуля Distance нову одиницю відстані під назвою LegoBlock. Фізичні блоки, як обʼєкти, що підлягають регулюванню, завжди мають дві властивості: їхні відстані недробові та невідʼємні.

type LegoBlock = LegoBlock

fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0

Поля nonFractional і nonNegative належать запису, який не існує поза аргументом типу, тож доречно надати їм тип (), який називають одиничним типом і який не несе жодної інформації, крім самого факту своєї наявності.

Отримати довільні відстані LegoBlock, звісно, можна, наприклад після обчислення різниць або відношень відстаней.

negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0

threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50

crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37

Усі наведені вище значення коректні й скомпілюються, проте нас особливо цікавлять такі значення, як fourStuds, що мають обидві властивості, nonFractional і nonNegative, бо вони представляють фізичні блоки, які можна поєднувати за допомогою

combineLegoBlocks
  :  Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add

Додамо кілька функцій, які дадуть користувачам змогу створювати й уточнювати відстані LegoBlock.

newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist

floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))

ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))

absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)

Звернімо увагу, що floorDistance, ceilingDistance і absDistance можуть працювати з одиницями, відмінними від LegoBlock, і загалом не роблять жодних припущень щодо вхідних властивостей: вони лише гарантують, що вихідні дані матимуть певну властивість, або nonFractional, або nonNegative.

Погляньмо на деякі результати.

import Distance exposing (Distance)

-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds

-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud

-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)

-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud

-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)

-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud

-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))

-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))

Загалом floorDistance і ceilingDistance дають різні результати, але однакові гарантії. У цьому й полягає сила техніки фантомного типу: надавати користувачам гнучкий вибір, зберігаючи сильні гарантії.

Вказівки

Ми, Володар Зла, дуже пишаємося якістю TreasureChest, які знаходяться в наших злих підземеллях по всьому світу. Підтримувати цю якість нелегко: наші менеджери підземель раз у раз псують створення скарбів, тож ми вирішуємо надати Elm API, щоб підкорити їх нашій волі.

Є дві умови, від яких ми не відступимо:

  1. кожна TreasureChest має бути захищена надійним паролем щонайменше з 8 символів
  2. у конкретному підземеллі кожна TreasureChest має містити унікальний скарб

Наші менеджери підземель запропонують список варіантів «пароль/скарб», і створяться лише придатні TreasureChest.

За цими критеріями є два можливі способи створити захищені скрині зі списку пропозицій:

  1. вилучити ті, у яких ненадійні паролі, а потім вилучити ті, у яких однакові скарби
  2. вилучити ті, у яких однакові скарби, а потім вилучити ті, у яких ненадійні паролі

Ці два способи можуть дати різні результати (наприклад, для [("strong_password", GoldStatue), ("1234", GoldStatue)]), але нам байдуже, як саме, тож ми хочемо дати менеджерам підземель вирішувати самим.

API, який залишає користувачам певний вибір, але все одно гарантує властивості кінцевого результату? Це якраз ідеально пасує до техніки фантомних типів!

1. Надайте заповнювач для скринь зі скарбами

Тип TreasureChest і його супутня функція getTreasure уже надані, але нам потрібно придумати тип для пропозиції скрині.

Реалізуйте тип Chest, реалізуйте makeChest і виправте сигнатури типів secureChest та uniqueTreasures. Сигнатури типів makeChest і makeTreasureChest уже надані серед вимог, не змінюйте їх.

Chest має містити ті самі дані, що й TreasureChest, і мати два аргументи типу: один для treasure і фантомний тип-запис для conditions. Зауважте, що оскільки Chest використовує фантомний тип, він має бути непрозорим, аби його не можна було використати деінде поза модулем TreasureFactory. У цьому випадку він навіть зовсім не експонований.

Відредагуйте сигнатури типів secureChest і uniqueTreasures, щоб додати обмеження, використовуючи розширювані записи як фантомні типи. secureChest має приймати Chest без конкретних умов і повертати Maybe Chest із додатковою умовою securePassword : () у своєму фантомному записі. uniqueTreasures має приймати List Chest без конкретних умов і повертати List Chest із додатковою умовою uniqueTreasure : (), доданою до фантомного запису.

Note

Тести Elm мають доступ до експонованих функцій, але не до сигнатур типів, тому тести не можуть перевірити, що ми використовуємо правильні сигнатури. Звісно, найсильніший доказ того, що сигнатури правильні, це успішно скомпілювати й запустити модуль разом із тестами, але ми створили правило аналізатора, яке перевірить наші сигнатури типів, щойно ми надішлемо своє рішення.

2. Виберіть захищені скрині

Коли скрині готові, нам треба вибрати з-поміж них захищені.

Реалізуйте secureChest, яка повертає варіант Just лише для скринь із паролем із 8 або більше символів. У тих Chest, які відповідають цій умові, до фантомного типу має бути додана додаткова умова securePassword : ().

3. Виберіть унікальні скарби

У підземеллі дозволені лише найрідкісніші скарби, адже навіть одна копія робить скарб дешевим.

Реалізуйте uniqueTreasures, яка приймає масив Chest і повертає масив тих Chest, у яких унікальний скарб у вхідному масиві. Якщо скарб трапляється у вхідних даних двічі, його не має бути у вихідних. Вхідні Chest не мають мати конкретних умов, а вихідні мають мати додану додаткову умову uniqueTreasure : ().

4. Насолоджуйтеся кінцевим результатом

Насолоджуйтеся найкращими з TreasureChest, на які шукачі пригод злітатимуться, як мухи на мед.

Реалізуйте makeTreasureChest, яка приймає Chest, що є одночасно захищеною та унікальною, і створює TreasureChest. Оскільки TreasureChest є непрозорим типом, це буде єдиний спосіб створити його, і навіть кепські менеджери підземель не зможуть його зіпсувати.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Elm Exercism

Час розпочати Фабрика скарбів?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Elm, а також 28 концепцій110 вправ та справжнє наставництво від людей, і все це безкоштовно.