Що таке лямбда-вираз?
Це довга історія, в якій мені хочеться описати багато власних думок, тому я собі дозволю писати якомога вільніше і у стилі посереднього белетриста.
Я думаю, якщо ви все таки тицьнули на цей пост, то принаймні вухом чули, що таке лямбда-вирази. А якщо не чули, то я з вами швидко поділюсь.
Лямбда-вираз (також відомий як анонімна функція, лямбда-функція або анонімний вираз) - це спосіб створення функцій в багатьох програмувальних мовах програмування. Він отримав назву "лямбда" через лямбда-числення, математичну теорію функцій і їх визначень.
Тобто це спосіб запису функції без її імені. Ось приклад синтаксису в деяких мовах:
Java
(a) -> {a + 10};
Python
lambda a : a + 10
Haskell
\a -> a + 10
Ідея лямбда-виразів походить з математики, напевно як і всіх концепцій у програмуванні, але про математику поговоримо в іншій частині. Цього разу тільки про Джаву.
Реалізація лямбда-виразів в Джаві
В Джаві лямбда-вирази з’явились тільки у восьмій версії. До того часу все писалось на анонімних класах.
Анонімний клас (Anonymous class) - це концепція в Java, яка дозволяє створювати класи без вказання їх імені. Анонімні класи зазвичай використовуються для реалізації інтерфейсів або класів-абстракцій на льоту, без необхідності створювати окремий файл для цього класу.
Наприклад:
public class Main {
public static void main(String[] args) {
Runnable myRunnable = new Runnable() {
@Override
public void run() {
}
};
myRunnable.run();
}
}
У цьому прикладі ми викликаємо конструктор інтерфейсу Runnable і відразу пишемо реалізацію для цього інтерфейсу.
Під капотом JVM створює новий клас, який імплементує інтерфейс і вже з нього створює реалізований об’єкт. Лямбда-вирази це майже синтаксичний цукор, як і ананомний клас.
Майже, бо JVM для лямбда-виразів використовує байт-код інструкцію invokedynamic, яку не використовує для анонімних класів, що не дозволяє зв’язати лямбда-вирази і анонімні класи, як одну сутність.
Java компілятор генерує байткод, яким користується invokedynamic під капотом для реалізації функціонального програмування та лямбда-виразів. invokedynamic допомагає оптимізувати виклик функцій і підтримує зв'язування функцій на етапі виконання, що дозволяє підтримувати динамічні мови та функціональні конструкції.
invokedynamic дозволяє генерувати код під час виконання.
Ноги анонімних класів лізуть з бажання писати реалізацію для інтерфейсу швидше, ніж це робить звичайний клас, а ноги лямбда-виразів лізуть з бажання писати реалізацію інтерфейсу швидше, ніж це робить анонімний клас.
public class Main {
public static void main(String[] args) {
Runnable myRunnable = () -> {};
myRunnable.run();
}
}
ООП vs Функційне програмування
Лямбда-вираз, навідмінно від анонімних класів, дозволяє перевизначати тільки інтерфейси в яких один метод. Чому тільки для інтерфейсів з одним методом? Бо лямбда визначає тільки одну функцію по своїй природі і в неї немає імені, щоб вона могла ідентифікувати, яку саме абстрактну функцію в інтерфейсі вона перевизначає.
По своїй природі лямбда повинна представляти одну функцію, в Джаві не можна описати функцію окремо від стану. Тому використовують функційні інтерфейси.
Інтерфейс, в концепції, служить абстрактним станом для функції, щоб задовільняти правило Джави щодо станів. Таким чинов лямбда-вираз завжди можна приписати до якогось інтерфейсу. А “функційним“ його робить анотація @FunctionalInterface та наявність однієї функції з якою ніколи не сплутаєш.
Ця прив’язка до стану має свій відчутний наслідок.
Лямбда-вирази відчуваються в джаві дивно, бо це ООП дуже хоче налізти на наш гарний функційний світ.
Ідентифікатором для лямбди мали би бути вхідні і вихідні дані, але в нас тут сильний запах ООП, який під капотом ще прив’язує кожну лямбду до типу який вона визначає, тобто, якщо метод приймає Runnable, то джава сприймає вхідний вираз, як реалізацію Runnable, а не як анонімну функцію. Більш детально:
@FunctionalInterface
public interface UnaryInterface {
void doSomething();
}
public class Main {
public static void main(String[] args) {
UnaryInterface unaryInterface = () -> {};
Runnable runnable = () -> {};
// Буде працювати
acceptFunction(() -> {});
}
}
public void acceptFunction(Runnable runnable) {}
Але якщо ми передаємо вже визначенні інтерфейси, то звісно програма не працюватиме.
public class Main {
public static void main(String[] args) {
UnaryInterface unaryInterface = () -> {};
Runnable runnable = () -> {};
// Не буде працювати
acceptFunction(unaryInterface);
}
}
public void acceptFunction(Runnable runnable) {}
Якби анонімну функцію дійсно визначали вхідні і вихідні дані, то UnaryInterface та Runnable по своїй суті і використанню мали би бути однаковими функціями, але у нас тут ООП, яке чітко каже, що UnaryInterface і Runnable є різними типами. Зрештою, важко з ним сперечатись.
Імплементація лямбд в Джаві повністю підкоряється філософії мови, що я цілком приймаю, як Джава розробник, але вважаю, що також необхідно бачити, яким чином нові концепції приносяться в мову та як ці концепції виглядають в первинній формі.
Додаткові матеріали:
https://wiki.jvmlangsummit.com/images/1/1e/2011_Goetz_Lambda.pdf
https://stackoverflow.com/questions/30002380/why-are-java-8-lambdas-invoked-using-invokedynamic