Локальні змінні, блоки та цикли

Локальні змінні

В 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.

results matching ""

    No results matching ""