Ми з вами збираємось змінити наш світогляд. Відтепер, ми приминимо казати комп'ютеру, як робити свою роботу, і натомість ми почнемо писати специфікацію до того, що ми хочемо мати в якості результату. Я переконаний, що ви з'ясуєте, що це менш стресово, ніж намагатись проконтролювати одночасно усе на найдрібніших рівнях.
Декларативність, на противагу імперативності, означає, що ми писатимемо вирази, а не покрокові інструкції.
Подумайте про SQL. Там немає "спочатку зроби це, а потім оте". Там є один вираз, який вкузує, щоб ми хотіли отримати з бази даних. Ми не вирішуємо, як зробити те, що воно робить. Коли база даних оновлена та SQL двигун оптимізовано, ми не повинні змінювати наш запит. Це тому, що існує багато шляхів інтерпретувати наші вимоги та досягти того ж самого результату.
Для декого, в тому числі і для мене, важко одразу охопити концепцію декларативного написання коду, тому, щоб більше проникнутись цим, давайте розглянемо кілька прикладів.
// імперативно
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
makes.push(cars[i].make);
}
// декларативно
const makes = cars.map(car => car.make);
Імперативний цикл має спочатку ініціалізувати масив. Інтерпретатор має оцінити це твердження, перед тим як продовжувати рух. Потім цикл напряму перебирає список автомобілів, вручну збільшуючи лічильник і демонрує нам свою шматки та шматочки у своєму вульгарному відтворенні явної ітерації.
Версія з map
- це один вираз. Він не потребує жодного порядку чи оцінки. Тут набагато більше свободи у тому, як map функція пребирає і як може складатись повертаємий масив. Це визначає що, а не як. Отже, він носить блискучий декларативний тюрбан.
До тогож, за бажанням, щоб бути чільш чіткішою і лаконічнішою, map-функція може бути оптимізована і при цьому наш дорогоцінний код не потрібно змінювати.
Тим з вас, хто думає "Так, але ж це набагато швидше написати імперативний цикл", я пропоную трохи позайматись самоосвітою і ознайомитись з тим, як JavaScript двигун оптимізує ваш код. Ось приголомшливе відео, яке може пролити трохи світла
Ось інший приклад.
// імперативно
const authenticate = (form) => {
const user = toUser(form);
return logIn(user);
};
// декларативно
const authenticate = compose(logIn, toUser);
Хоча в імперативній версії немає нічого стовідсотково невірного, в ній все ще присутня поетапна оцінка. Вираз compose
просто констатує факт: функція authenticate - це композиція toUser
та logIn
. Знову ж таки, це лишає нам простір для зміни коду підтримки та призводить до того, що код нашої програми являється специфікацією високго рівня.
У наведеному вище прикладі порядок виконання визначено (toUser
має бути викликаний перед logIn
), але існує багато сценаріїв, де порядок не має значення, і це легко визначити за допомогою декларативного написання коду (більше про це пізніше).
Оскільки ми не задаємо послідовність оцінки коду, декларативне написання коду забезпечує паралельне обчислення. Це, в поєднанні з чистими функціями, демонструє чому функціональне програмування є хорошим варіантом для паралельного майбутнього - нам не потрібно робити нічого особливого, щоб досягти паралельних систем.
А тепер ми з вами побудуємо приклад програми у декларативному та композиційному стилі. І не дивлячись на те, що ми й досі будемо трохи мухлювати, використовуючи побічні ефекти, але ми триматимемо їх у мінімальній кількості і окрема від нашої чистої кодової бази. Ми з вами збираємось побудувати додаток до браузера, який витягає з flickr зображення та відображає їх. Давайте розпочнемо побудову додатку. Ось розмітка:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flickr App</title>
</head>
<body>
<main id="js-main" class="main"></main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
<script src="main.js"></script>
</body>
</html>
А ось кістяк main.js:
const CDN = s => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN('ramda/0.21.0/ramda.min');
const jquery = CDN('jquery/3.0.0-rc1/jquery.min');
requirejs.config({ paths: { ramda, jquery } });
requirejs(['jquery', 'ramda'], ($, { compose, curry, map, prop }) => {
// app goes here
});
Ми використовуватимемо ramda замість lodash чи якоїсь іншої допоміжної бібліотеки. В ній є compose
, curry
, та багато чого іншого. Я використав requirejs, що можливо трохи занадто, але ми використовуватимемо його в усіх розділах книги, тож постійсть - наш ключ.
Тепер, коли ми з цим розібрались - перейдемо до специфікації. Наш додаток робитиме 4 речі.
- Будувати url для нашого певного пошувого слова
- Робитиме запит до flickr api
- Перетворюватиме отриманий json у html зображення
- Відображатиме їх на екрані
Тут є 2 нечисті дії. Ви їх бачите? Ті частини в яких йдеться про запити та отримання даних від API flickr та відображення їх на екрані. Тож давайте спочатку їх визначимо, щоб їх було легше ізолювати. Також я додам нашу чудову функцію trace для легкого відлагодження.
const Impure = {
getJSON: curry((callback, url) => $.getJSON(url, callback)),
setHtml: curry((sel, html) => $(sel).html(html)),
trace: curry((tag, x) => { console.log(tag, x); return x; }),
};
Тут ми просто огорнули jQuery методи так, щоб вони були карровані, а також просто змінили місцями аргументи для більшої зручності. Я виокремив їх як Impure
, щоб ми знали, що ці функції - небезпечні. У майбутньому прикладі ми зробимо ці функції чистими.
Далі нам потрібно побудувати URL, щоб передати його в нашу функцію Impure.getJSON
.
const host = 'api.flickr.com';
const path = '/services/feeds/photos_public.gne';
const query = t => `?tags=${t}&format=json&jsoncallback=?`;
const url = t => `https://${host}${path}${query(t)}`;
Існують більш гарні та занадто складні способи написання функції url
у безточечному стилі, за допомогою моноїдів(ми вивчатимемо про них трохи згодом) чи комбінаторів. Але ми поки обрали більш читабельний варіант та зібрали цю строку у нормальний точечний спосіб.
Давайте напишемо функцію app
, яка робить запит та відображає контент на екрані.
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');
Це викликає нашу url
функцію, потім передає строку до нашої функції getJSON
, яка була частково застосована з trace
. Завантаження додатку виведе у консоль відповідь від запиту до API.
Ми б хотіли побудувати зображення з цього json. Схоже, що mediaUrls
поховані у items
, а потім ще й у властивості m
об‘єкту media
.
Але, якби там не було, для того, щоб дістатися до вкладених властивостей, ми можемо скористатися універсальною getter функцією з бібліотеки ramda, яка нащивається _.prop
. Ось власноруч виготовлена версія, і ви можете побачити, що відбувається:
const prop = curry((property, object) => object[property]);
Це доволі нудно. Ми просто використовуємо синтаксис []
, щоб отримати доступ до властивості будь якого об'єкту. Давайте скористаємось цим, щоб отримати наші mediaUrls
.
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
Як тільки ми отримаємо items
, ми повинні пройтись map
-ом по них, для того щоб витягнути з когожного media url. Так ми отримаємо гарненький масив з mediaUrls
значеннями. Давайте прикрутимо це до нашої програми і виведемо їх на екран.
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);
Все, що ми зробили, це створили нову композицію, яка викличе наші mediaUrls
та встановить HTML <main>
з ними. Ми замінили виклик trace
на render
, тепер, коли у нас є щось для рендерингу, крім сирого JSON. Це грубо відобразить наші mediaUrls
у тілі.
Нашим останнім кроком буде перетворення цих mediaUrls
на справжні images
. У більшому застосунку ми б використовували бібліотеку шаблонів/DOM, таку як Handlebars або React. Однак для цього застосунку нам потрібен лише тег img
, тому давайте залишимося з jQuery.
const img = src => $('<img />', { src });
jQuery метод html
отримає масив тегів. Нам лише лишилось трансформувати наші mediaUrls
у зображення і надіслати їх до методу setHtml
.
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);
Все, у нас все готово!
Ось завершений script: додати
А тепер подивіться на код. Гарна декларативна специфікація того, чим являються речі, а не те, як вони ними стають. Тепер ми розглядаємо кожну лінію, як рівняння з властивостями. Ми можемо використовувати ці властивості, щоб оцінювати наш додаток та відлагодження.
Тут можлива оптимізація - ми проходимось по кожному елементу, щоб обернути його на медіа url, потім ми проходимось знову по всім тим mediaUrls
, щоб перетворити їх на теги image
. Але існує закон про map та композицію:
// закон композиції map
compose(map(f), map(g)) === map(compose(f, g));
Ми можемо використати цю властивість, щоб оптимізувати наш код. Давайте тотально відлагодимо наш код.
// початковий код
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);
Давайте вишикуємо в лінію наші map. Ми можемо записати в лінію виклики до mediaUrls
у images
завдячуючи рівноправним міркуванням та чистоті.
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));
Тепер, після того як ми підрівняли наші map
, ми можемо застосувати закон композиції.
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));
Тепер двигун пройдеться циклом лише раз, коли він буде перетворювати кожен елемент у зображення. Давайте лише зробимо це трохи більш зручним для прочитання, завдяки відокремленню функції.
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));
Ми побачили, як скористатись нашими новими вміннями у маленькому, проте реальному додатку. Ми скористались нашою математичною базою для обгрунтування та редагування нашого коду. Але що стосовно обробки помилок та розгалудження коду? Як ми можемо зробити всю програму чистою, замість того, щоб просто виокремлювати деструктивні функції? Як ми можемо зробити нашу програму більш безпечнішою та виразнішою? Це питання, які ми розглянемо у частині 2.