`.also()` та `.let()` у Kotlin, як аналоги Optional в Java

pic

Фото від 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
повертає на виході: T

let:
отримує на вхід: пару типів 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

Leave a Reply

Your email address will not be published. Required fields are marked *