Фото від ian dooley на Unsplash
Це ілюстрація не має відношення до статті, я просто хотів знайти щось, щоб вставити, хехехе
Сьогодні (на момент написання цього блогу) я їхав автобусом по своїх справах, і під час поїздки мене осінило, що минулого тижня я обговорював з другом таке:
.let()
та.also()
— це аналог (counterpart)Optional
в Java, і я в цьому абсолютно впевнений. До того ж, `T.let()
це аналог
Optional.map()
`
І я досить впевнений, що T.also()
— це теж аналог якоїсь методи в класі Optional
в Java.
Але тоді у мене не було часу, щоб детально пояснити, як саме вони взаємодіють.
Тепер же настав час пояснити це чіткіше (набагато чіткіше), як вони є один для одного аналогами.
Тому я вирішив написати цей блог, щоб пояснити це разом, а потім відправлю його другу, щоб він теж ознайомився 😁
Все почалося того дня минулого тижня…
.let()
та.also()
— для чого ви їх використовуєте? Я бачив, як ви неодноразово застосовували ці методи в коді.
Це було запитання, яке я поставив своєму другові минулого тижня, коли ми зустрілися в одному з відкритих місць.
Зазвичай ми використовуємо їх ось так …
Він відповів і відкрив свій Macbook, показуючи код, де використовуються .also()
та .let()
.
Що я побачив, був код на Kotlin (напевно), який виглядав ось так:
class SomeClass {
fun someMethod(customer: Customer): SomeOutput {
// Пропущено для стислості
customer.address?.let { this.setAddress(customer.address) }
// Пропущено для стислості
}
}
Давайте ж розглянемо підписи методів
also()
таlet()
, щоб зрозуміти, як вони працюють.
Саме так я запропонував, і друг не вагався, натиснув CMD + Left Click, щоб подивитися визначення цих методів.
Ось що ми побачили на екрані:
/**
* Викликає задану функцію [block], передаючи в неї значення `this` як аргумент і повертає значення `this`.
*
* Для детальнішої інформації дивіться документацію для [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#also).
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
/**
* Викликає задану функцію [block], передаючи в неї значення `this` як аргумент і повертає результат цієї функції.
*
* Для детальнішої інформації дивіться документацію для [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let).
*/
/**
* @kotlin.internal.InlineOnly
public inline fun T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
Ми вирішили абстрагуватися від імплементації цих двох методів і зосередитись на чомусь більш загальному, а саме на підписах методів.
T.also(T -> Unit): T
// та
T.let(T -> R): R
Але стоп! Є одна річ, яку я пам'ятаю з курсу "Математика для програмістів", який викладав брат Дев (@Rawitat Pulam), і це я запам'ятав на все життя:
методи екземплярів класів завжди мають екземпляр цього класу як аргумент
Тому, якщо я розберу .also()
та .let()
і напишу їх у вигляді нових підписів функцій, вони будуть виглядати так (я навмисно використовую слово функція, а не метод):
Я зробив припущення і написав це у вигляді нотації для мови Haskell:
also :: (T, (T -> Unit)) -> T
// та
let :: (T, (T -> R)) -> R
Примітка:
also:
отримує на вхід: пару типів T і функцію, яка приймає T і повертає Unit
повертає на виході: Tlet:
отримує на вхід: пару типів T і функцію, яка приймає T і повертає R
повертає на виході: R
Що я одразу помітив — підпис функції let()
у будь-якій мові виглядає так само, як підпис функції .map()
, тобто це завжди буде виглядати як T -> (T -> R) -> R
.
Я так і сказав своєму другу, і він погодився.
Примітка:
map
в Haskell:
-map :: [a] -> (a -> b) -> [b]
map
в Java:
-Stream.map(Function): Stream
-Optional.map(Function): Stream
map
в TypeScript:
-Array.map(T -> R): Array
Але того дня я залишив .also()
осторонь, бо тоді я не міг пригадати, чи зустрічав я раніше подібне.
Але є одна річ, яку я помітив і про яку поговорив з другом:
Той код, що ти показував, насправді треба було б писати з використанням
.also()
, а не.let()
.
І ось чому:
Метод .also()
приймає функцію, яка повертає Unit
(це еквівалент Void
або Undefined
у деяких мовах), що означає, що ця функція виконується і завершується, і ми не використовуємо результат цієї функції для подальших дій.
… Тепер давайте повернемося до того прикладу коду, що показав друг:
customer.address?.let { setAddress(customer.address) }
Тут ми передаємо () -> this.setAddress(/* адреса клієнта, яку потрібно встановити */)
, що означає, що в даному випадку нам слід замінити його на наступне:
customer.address?.also { setAddress(customer.address) }
Друг кивнув, підтверджуючи, що погоджується з цією ідеєю.
І от, знову повертаємось до сьогоднішнього дня...
Після того, як я задумався над усім цим в автобусі, я повернувся додому і відразу ж сів за комп'ютер, щоб перевірити, чи є метод у класі Optional
, який має такий самий підпис, як у .also()
в Kotlin.
І ось, я знайшов один метод, який точно відповідає вимогам — це Optional.ifPresent()
.
public final class Optional {
/*
* Методи опущені для стислості
*/
/**
* Якщо значення присутнє, виконує задану дію з цим значенням,
* в іншому випадку нічого не робить.
/*
* @param action дія, яка буде виконана, якщо значення присутнє
* @throws NullPointerException якщо значення присутнє і передана дія є
* {@code null}
*/
public void ifPresent(Consumer action) {
if (value != null) {
action.accept(value);
}
}
/*
* Методи опущені для стислості
*/
}
Consumer, так?
Consumer
— це функціональний інтерфейс, який приймає параметр типу T і нічого не повертає.
Consumer
еквівалентнийT -> Void
абоT -> Unit
.Це трохи відступ від теми (просто хотів згадати)
Говорячи про
Consumer
, не можна не згадати його найближчого друга —Supplier
, який еквівалентний() -> T
.Ми можемо помітити, що вони просто міняються місцями:
-Consumer
: приймає аргументT
, але не повертає нічого (Void)
-Supplier
: не приймає аргументів, але повертаєT
Назви цих інтерфейсів підібрані дуже вдало, бо з їх імен можна легко здогадатися:
Consumer
споживаєT
і не повертає нічого, аSupplier
не споживає нічого, але постачаєT
.
Приклад T.let()
vs Optional.map()
та T.also()
vs Optional.ifPresent()
.
Примітка для фанатів FP:
Багато хто з фанатів функціонального програмування, прочитавши до цього моменту, може вже здогадатися (а може навіть давно знати), що насправді класи
Optional
і методи.also()
/.let()
були створені для того, щоб виступати якOption
чиMaybe
монади в мовах функціонального програмування, з якими ці мови дружать вже давно.
Мені здається, що я достатньо попліткував на цю тему, тому зупинюся на цьому і закінчу з описом .let()
та .also()
в Kotlin як аналогів класу Optional
в Java.
Дякую, що прочитали до цього моменту 🙇🏻♂️️️🙇🏻♂️️️🙇🏻♂️️️
Якщо є питання чи хочете щось обговорити, залишайте коментарі нижче, або можете підключитися до мене через Linkedin для бесіди:
👉 https://linkedin.com/in/fResult 👈
Перекладено з: .also() and .let() in Kotlin, a counterpart of Optional in Java