Використайте лінзи, щоб оновлювати вкладені записи (це стосується мов із незмінними даними).
Оновлювати поля вкладених незмінних записів доволі дратівливо. Чим глибша структура, тим громіздкіший код для таких випадків. Якщо в нас є, скажімо, особа, яка містить адресу, що має вулицю, а та має номер, то для оновлення номера потрібно створити нову вулицю з новим номером, потім нову адресу з новою вулицею і, нарешті, нову особу з новою адресою. Ми вже заплуталися?
Одне з рішень цієї проблеми - використати лінзи.
Реалізуйте кілька функцій доступу до записів за допомогою лінз. Набір тестів також дозволяє нам взагалі обійтися без лінз, щоб ми могли поекспериментувати з різними підходами.
Лінзи в Roc використовують нечасто, бо синтаксис оновлення записів гарно покриває більшість випадків. Наприклад, розгляньмо цей вкладений запис:
bob = { hair: { color: Brown, length: Long }, mouth: Smiling, height: 6.0 }
Тепер зробимо Бобові стрижку:
new_bob = { ..bob, hair: { ..bob.hair, length: Short } }
Жодних лінз не потрібно! Утім, хоча лінзи в Roc рідкісні, вони поширені в інших мовах, як-от Haskell і Scala, тож нам може бути цікаво дізнатися, як їх можна реалізувати в Roc. Це також гарний спосіб попрактикувати деякі просунуті можливості Roc, як-от параметризовані типи та записи, що містять функції. Ось один зі способів представити лінзу в Roc:
Lens(whole, part) := {
get : whole -> part,
set : whole, part -> whole,
}
Це параметризований тип: whole і part можуть бути будь-якими типами. whole зазвичай є записом, а part може бути полем будь-якого типу. Поле get містить функцію, яка повертає конкретний part із заданого whole, а поле set містить функцію, яка повертає копію заданого whole, але з конкретним part, заміненим на заданий part.
Наприклад, ось лінза, щоб отримати або встановити волосся людини:
hair_lens = {
get: |person| person.hair,
set: |person, hair| { ..person, hair },
}
Ми можемо використати цю лінзу, щоб сфокусуватися на волоссі Боба, ось так:
bob_hair = bob |> hair_lens.get
І ми можемо змінити його волосся ось так:
new_hair = { ..bob.hair, length: Short }
new_bob = bob |> (hair_lens.set)(new_hair)
Примітка: дужки навколо hair_lens.set обовʼязкові: без них Roc шукав би функцію з назвою set у типі hair_lens.
Лінзи стають особливо корисними, коли ми починаємо їх поєднувати, що дає змогу отримувати й встановлювати глибоко вкладені частини. Це можна зробити, реалізувавши таку функцію:
compose : Lens(outer, inner), Lens(inner, part) -> Lens(outer, part)
Коли цю функцію реалізовано, ми можемо використати її, щоб створити лінзу довжини волосся ось так:
length_lens = {
get: |hair| hair.length,
set: |hair, length| { ..hair, length },
}
hair_length_lens = compose(hair_lens, length_lens)
Тепер ми можемо зробити Бобові ще одну стрижку:
set_hair_length = hair_length_lens.set
new_bob = bob |> set_hair_length(Short)
Можливо, нам також захочеться реалізувати функцію, яка дає змогу застосувати будь-яке перетворення до вкладеної частини:
over : Lens(whole, part), whole, (part -> part) -> whole
Коли її реалізовано, ми можемо використати over ось так:
cut_hair = |current_length| match current_length {
Long => Short
Short => Bald
Bald => Bald
}
new_bob = hair_length_lens |> over(bob, cut_hair)
У цій вправі тести перевірять, чи код здатен змінювати різні вкладені частини такого вкладеного запису:
test_person : Person
test_person = {
name: { forenames: "Jane Joanna", surname: "Doe" },
birth: {
born_at: { street: "Longway", house_number: 1024, place: "Springfield", country: "United States" },
born_on: { year: 1984, month: 4, day: 12 },
},
address: { street: "Shortlane", house_number: 2, place: "Fallmeadow", country: "Canada" },
}
Радимо спершу пройти тести, використовуючи синтаксис оновлення записів Roc, а потім спробувати лінзи.