Локальні змінні, блоки та цикли
Локальні змінні
В ClojureScript немає поняття змінної, як в мовах родини ALGOL, але є так звані локальні зв'язування (для зручності будемо називати їх також змінними). Локальні змінні, зазвичай, не змінюють свої значення. Якщо спробувати їх змінити, компілятор видасть повідомлення про помилку.
Локальні змінні створюються за допомогою виразу let
. Вираз містить вектор і довільну послідовність виразів після нього. Вектор повинен мати пари значень, що описують зв'язування. Така пара складається з символу та виразу, значення якого буде зв'язано з цим символом. Локальні змінні доступні усім виразам, що містяться у тілі виразу let
.
(let [x (inc 1)
y (+ x 1)]
(println "Simple message from the body of a let")
(* x y))
;; Simple message from the body of a let
;; => 6
У цьому прикладі символ x
зв'язаний зі значенням виразу (inc 1)
, що дорівнює 2
, а y
зв'язаний з сумою x
та 1
, що дорівнює 3
. Вирази (println "Simple message from the body
of a let")
та (* x y)
обчислюються зі значеннями цих локальних змінних.
Блоки
У JavaScript фігурні дужки {
та }
виділяють окремий блок коду. У ClojureScript блоки створюються за допомогою виразу do
і часто використовуються для виконання побічних ефектів, наприклад виведення у консоль або логування даних.
Побічний ефект — це вираз, який нічого не повертає.
Вираз do
приймає довільну кількість виразів, але повертає значення лише останнього:
(do
(println "hello world")
(println "hola mundo")
(* 3 5) ;; this value will not be returned; it is thrown away
(+ 1 2))
;; hello world
;; hola mundo
;; => 3
Тіло виразу let
, про який ми говорили раніше, дуже схоже на вираз do
тим, що воно теж приймає декілька виразів. Насправді, let
містить в собі неявний do
.
Цикли
Функціональний підхід в ClojureScript не передбачає наявність стандартного механізму для створення циклів за допомогою спеціальних конструкцій, як наприклад цикл for
у JavaScript. В ClojureScript цикли створюються за допомогою рекурсії. Рекурсія іноді потребує більше часу на моделювання вирішення проблеми ніж цикли в імперативних мовах.
Багато підходів, що використовують цикл for
в інших мовах, досягаються за допомогою функцій вищого порядку — таких, що приймають інші функції.
Цикли за допомогою loop/recur
Давайте розберемось, як можна виразити цикл за допомогою форм loop
та recur
. loop
містить вектор локальних змінних (так само, як let
) та вираз, що ними оперує, а recur
— циклічно викликає loop
з новими значеннями цих змінних.
Розглянемо приклад:
(loop [x 0]
(println "Циклічний виклик з " x)
(if (= x 2)
(println "Цикл закінчився!")
(recur (inc x))))
;; Циклічний виклик з 0
;; Циклічний виклик з 1
;; Циклічний виклик з 2
;; Цикл закінчився!
;; => nil
У цьому прикладі ми зв'язуємо значення 0
з символом x
і виконуємо тіло циклу — вираз, у якому є умова. Під час першого виконання цього виразу, умова не справджується, тому recur
повторює цикл з результатом виразу обчислення (inc x)
. Так буде продовжуватись, доки умова не виконається, і тоді цикл loop
вийде з рекурсії, повернувши результат.
Зауважимо, що за допомогою recur
можна зациклювати не лише loop
; recur
в функції також створить рекурсивний виклик з новими значеннями аргументів:
(defn recursive-function
[x]
(println "Циклічний виклик з" x)
(if (= x 2)
(println "Цикл закінчився!")
(recur (inc x))))
(recursive-function 0)
;; Циклічний виклик з 0
;; Циклічний виклик з 1
;; Циклічний виклик з 2
;; Цикл закінчився!
;; => nil
Замінюємо цикл for на функції вищого порядку
В імперативних мовах програмування цикл for
часто використовується для ітерування даних і їх перетворення, зазвичай, це роблять, коли потрібно:
- перетворити кожне значення у колекції і створити нову колекцію з цими значеннями;
- відфільтрувати значення у колекції за певним критерієм;
- перетворити колекцію у значення, коли кожна наступна ітерація залежить від результату минулої;
- виконати обчислення для кожного значення в колекції.
В ClojureScript ці операції виражені за допомогою функцій вищого порядку та синтаксичних конструкцій; давайте розглянемо приклади для перших трьох випадків.
Для перетворення елементів колекції ми використовуємо функцію map
, яка приймає перетворюючу функцію та саму послідовність. Під час виконання функція викликається на кожному елементі з колекції і в результаті ми отримуємо нову колекцію:
(map inc [0 1 2])
;; => (1 2 3)
Першим параметром в map
може бути будь-яка функція, що приймає один аргумент і повертає значення. Наприклад, якщо б у вас був застосунок для створення графіків і вам потрібно було б створити графік функції y = 3x + 5
для ряду значень x
, то ви могли б обчислити значення y
наступним чином:
(defn y-value [x] (+ (* 3 x) 5))
(map y-value [1 2 3 4 5])
;; => (8 11 14 17 20)
Якщо функція невелика, то можна використати анонімну функцію, у звичайному чи скороченому вигляді:
(map (fn [x] (+ (* 3 x) 5)) [1 2 3 4 5])
;; => (8 11 14 17 20)
(map #(+ (* 3 %) 5) [1 2 3 4 5])
;; => (8 11 14 17 20)
Для фільтрування елементів послідовності ми використовуємо функцію filter
, яка приймає предикату і послідовність, та повертає нову, в якій залишились тільки ті елементи, для яких предиката повернула true
:
(filter odd? [1 2 3 4])
;; => (1 3)
Знову ж таки, першим аргументом в filter
може бути будь-яка функція, що повертає true
або false
. В цьому прикладі ми створюємо колекцію, в якій кожен елемент — рядок довжиною менше п'яти знаків. (Функція count
обчислює довжину значення.)
(filter (fn [word] (< (count word) 5)) ["ant" "baboon" "crab" "duck" "echidna" "fox"])
;; => ("ant" "crab" "duck" "fox")
Функція reduce
використовується для перетворення колекції в значення. Цей процес називається згортуванням, коли колекція згортується у якесь значення через згортування попереднього згорнутого значення з наступним значенням в колекції. Функція reduce
приймає згортуючу функцію та колекцію, а також необов'язкове початкове значення:
(reduce + 0 [1 2 3 4])
;; => 10
reduce
приймає будь-яку функцію, що приймає два аргументи та повертає значення. Першим аргументом буде згорнуте значення, другим — наступне значення в колекції. Функція повертає значення, що буде згорнутим (першим аргументом) в наступній ітерації. Наприклад, ось як можна використати reduce
, щоб знайти суму квадратів усіх чисел у колекції (це часто використовується в статистиці):
(defn sum-squares
[accumulator item]
(+ accumulator (* item item)))
(reduce sum-squares 0 [3 4 5])
;; => 50
...те ж саме за допомогою анонімної функції:
(reduce (fn [acc item] (+ acc (* item item))) 0 [3 4 5])
;; => 50
Ось приклад з reduce
, що обчислює сумарну довжину усіх рядків у колекції:
(reduce (fn [acc word] (+ acc (count word))) 0 ["ant" "bee" "crab" "duck"])
;; => 14
У цих прикладах ми не використовуємо синтаксис для скороченого запису анонімних функцій, щоб вам було легше читати та розуміти їх. Але це не означає, що скорочений запис не треба використовувати.
Будьте уважні з початковим значенням для reduce
. Наприклад, якщо вам треба знайти добуток усіх чисел в колекції, то починати треба з одиниці, а не з нуля, інакше усі числа будуть помножені на нуль!
;; неправильне початкове значення
(reduce * 0 [3 4 5])
;; => 0
;; правильне початкове значення
(reduce * 1 [3 4 5])
;; => 60
Створення послідовностей за допомогою for
В ClojureScript вираз for
використовується не для ітерування, а для створення послідовностей. Ця операція також відома як «sequence comprehension». В цьому розділі ми дізнаємось, як вона працює та як її використовувати для декларативного створення послідовностей.
for
приймає вектор локальних зв'язувань та вираз, і створює послідовність, в якій кожним елементом є результат обчислення цього виразу з поточними значеннями локальних змінних. Давайте розглянемо такий приклад:
(for [x [1 2 3]]
[x (* x x)])
;; => ([1 1] [2 4] [3 9])
У цьому прикладі символ x
зв'язаний з поточним значенням з вектора [1 2 3]
, а результатом обчислення усього виразу буде послідовність векторів з двох елементів, в яких другий елемент — квадрат поточного значення локальної змінної.
for
може мати декілька зв'язувань. Це утворить вкладені цикли, так само, як декілька вкладених циклів for
у інших мовах. Кожен вкладений цикл буде виконуватись для кожного поточного значення поточного циклу.
(for [x [1 2 3]
y [4 5]]
[x y])
;; => ([1 4] [1 5] [2 4] [2 5] [3 4] [3 5])
Після визначення локальних зв'язувань для поточних значень можна використати три модифікатори: :let
— для створення додаткових локальних зв'язувань, :while
— для виходу з процесу створення колекції, та :when
— для фільтрування значень.
Наступний приклад показує використання модифікатора :let
; зауважимо, що локальні змінні створені у модифікаторі також доступні у основному виразі циклу:
(for [x [1 2 3]
y [4 5]
:let [z (+ x y)]]
z)
;; => (5 6 6 7 7 8)
Ми можемо використати модифікатор :while
, щоб створити умову, яка призведе до виходу з процесу створення колекції, коли результатом її обчислення буде логічне false
. Ось приклад:
(for [x [1 2 3]
y [4 5]
:while (= y 4)]
[x y])
;; => ([1 4] [2 4] [3 4])
Наступний приклад показує, як використовувати :when
для фільтрування значень:
(for [x [1 2 3]
y [4 5]
:when (= (+ x y) 6)]
[x y])
;; => ([1 5] [2 4])
Усі модифікатори можна використовувати разом для вираження комплексного процесу створення послідовності:
(for [x [1 2 3]
y [4 5]
:let [z (+ x y)]
:when (= z 6)]
[x y])
;; => ([1 5] [2 4])
Коли ми розглядали найбільш поширені випадки використання циклу for
в імперативних мовах, ми зазначили, що іноді нам треба виконати якусь операцію для кожного значення в колекції, не створюючи при цьому нову. Звичайно це означає, що вам треба виконати деякі побічні дії для кожного значення в колекції.
В ClojureScript є конструкція doseq
, що схожа на for
, але після її обчислення повертається nil
.
(doseq [x [1 2 3]
y [4 5]
:let [z (+ x y)]]
(println x "+" y "=" z))
;; 1 + 4 = 5
;; 1 + 5 = 6
;; 2 + 4 = 6
;; 2 + 5 = 7
;; 3 + 4 = 7
;; 3 + 5 = 8
;; => nil
Якщо ви хочете лише проітерувати колекцію і виконати побічну дію для кожного значення (наприклад println
), ви можете використати спеціально створену для цього функцію run!
, що внутрішньо використовує швидке згортування:
(run! println [1 2 3])
;; 1
;; 2
;; 3
;; => nil
Після обчислення ця функція повертає nil
.