Лямбда-функція - Про Haskell по-людськи
Настав час познайомитись із важливою концепцією — лямбда-функцією. Саме з неї все почалося. Приготуйтеся: у цьому розділі на нас чекають нові відкриття.
У далеких 1930-х молодий американський математик Алонзо Черч запитав у тому, що означає «обчислити» щось. Плодом його роздумів стала система для формалізації поняття «обчислення», і він назвав цю систему «лямбда-обчисленням» (англ. lambda calculus, на ім'я грецької букви λ ). В основі цієї системи лежить лямбда-функція, яку в певному сенсі можна вважати "матір'ю функціонального програмування" загалом і Haskell зокрема. Далі називатиму її ЛФ.
Щодо ЛФ можна сміливо сказати: "Все геніальне просто". Ідея ЛФ така корисна саме тому, що вона гранично проста. ЛФ – це анонімна функція. Ось як вона виглядає в Haskell:
Зворотний слеш на початку ознака ЛФ. Порівняйте з математичною формою запису:
Пам'ятаєте функцію square? Ось це її лямбда-аналог:
Лямбда-абстракція (англ. lambda abstraction) - це особливий вираз, що породжує функцію, яку ми відразу ж застосовуємо до аргументу 5 . ЛФ з одним аргументом, як і просту функцію називають ще «ЛФ від одного аргументу» або «ЛФ одного аргументу». Також можна сказати і про «лямбду-абстракцію від одного аргументу».
Будова лямбда-абстракції гранично проста:
Відповідно, якщо ЛФ застосовується до двох аргументів - пишемо так:
І коли ми застосовуємо таку функцію:
то просто підставляємо 10 на місце x, а 4 - на місце y, і отримуємо вираз 10 * 4:
Загалом, все як зі звичайною функцією, навіть простіше.
Ми можемо ввести проміжне значення для абстракції лямбди:
Тепер ми можемо застосовувати mul так само, як коли б це була сама лямбда-абстракція:
І тут ми наблизилися до одного важливого відкриття.
Тип функції
Ми знаємо, що у всіх даних у Haskell-програмі обов'язково є якийсь тип, який уважно перевіряється на етапі компіляції. Питання: який тип виразу mul з попереднього прикладу?
Відповідь проста: тип mul такий самий, як і у цієї лямбда-абстракції. З цього робимо важливий висновок: ЛФ має тип, як і звичайні дані. Але оскільки ЛФ є окремим випадком функції - значить і у звичайної функції теж є тип!
У нефункціональних мовах між функціями та даними проведено чітку межу: ось це функції, а ото — дані. Однак у Haskell між даними та функціями різниці немає, адже й те й інше спочиває на одній і тій же Черепасі. Ось тип функції mul:
Так само ми можемо вказати тип будь-яких інших даних:
Хоча ми знаємо, що у Haskell типи виводяться автоматично, іноді ми хочемо взяти цю турботу на себе. В даному випадку ми явно говоримо: "Нехай вираз coeff буде дорівнює 12, але тип його нехай буде Double, а не Int". Так само і з функцією: коли ми оголошуємо її, ми тим самим вказуємо її тип.
Але ви запитаєте, чи ми можемо не вказувати тип функції явно? Можемо:
Це наша стара знайома функція square. Коли вона буде застосована до значення типу Int, тип аргументу буде виведено автоматично як Int.
І якщо функція характеризується типом так само, як і інші дані, ми робимо ще одне важливе відкриття: функціями можна оперувати як даними. Наприклад, можна створити список функцій:
Вираз функцій - це список з двох функцій. Два лямбда-вираження породжують ці дві функції, але до моменту застосування вони нічого не роблять, вони неживі і марні. Але коли ми застосовуємо функцію head до цього списку, ми отримуємоперший елемент списку, тобто першу функцію. І отримавши, відразу застосовуємо цю функцію до рядка "Hi" :
Це рівносильно коду:
Під час запуску програми ми отримаємо:
До речі, а який тип списку функцій? Його тип такий: [String -> String]. Тобто список функцій з одним аргументом типу String, що повертають значення типу String.
Локальні функції
Якщо між ЛФ та простими функціями фактично немає відмінностей, а функції є окремий випадок даних, ми можемо створювати функції локально для інших функцій:
Це дві функції, які ми визначили прямо в where-секції, тому вони існують тільки для основного вираження функції validComEmail. З простими функціями так роблять дуже часто: де вона потрібна, там її визначають. Ми могли б написати і більш очевидно:
Втім, вказувати тип таких простих функцій, зазвичай, необов'язково.
Ось як цей код виглядає з лямбда-абстракціями:
Тепер вирази вмістуAtSign і endsWithCom прирівняні до ЛФ від одного аргументу. І тут ми не вказуємо тип цих выражений. Втім, якщо дуже хочеться, можна й зазначити:
Лямбда-абстракція взята в дужки, щоб зазначення типу належало до функції в цілому, а не лише до аргументу e:
Для типу функції також можна ввести псевдонім:
Втім, на практиці вказівка типу для лямбда-абстракцій зустрічається винятково рідко, бо нема чого.
Відтепер, познайомившись із ЛФ, ми будемо використовувати їх періодично.
І насамкінець, питання. Пам'ятаєте тип функції mul?
Що це за буква a? По-перше, ми не зустрічали такий тип раніше, а по-друге, хіба ім'я типу в Haskell не повинно починатися з великої літери? Повинно. А вся річ у тому, що буква a в даному випадку – це не зовсім ім'я типу. А ось що цетаке, ми дізнаємося в одному з найближчих розділів.
Для цікавих
А чому, власне, лямбда? Чому Черч обрав саме цю грецьку букву? За однією з версій, це сталося випадково.
Йшли 30-ті роки минулого століття, комп'ютерів не було, і всі наукові роботи набиралися на машинах. У первісному варіанті, щоб виділяти ім'я аргументу ЛФ, Черч ставив над ім'ям аргументу символ, схожий на ^ . Але коли він здавав роботу набирачеві, то згадав, що друкарська машинка не зможе відтворити такий символ над літерою. Тоді він виніс цей «дах» перед ім'ям аргументу, і вийшло щось на кшталт:
А наборщик, побачивши такий символ, використав велику грецьку букву Λ :