Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
мова програмування Clojure. Процедурна парадигм...docx
Скачиваний:
1
Добавлен:
01.07.2025
Размер:
185 Кб
Скачать
☆

Основні типи даних

Дані в Clojure розділяються на 2 групи:

  1. Базові типи даних: числа, рядки і т.д

  2. послідовності, вектори, відображення, множини

Всі типи даних мають загальний набір характеристик: дані незмінні і реалізують операцію рівність

Базові типи даних:

Логічні значення(істина або хиба)

Числа: в мові можуть використовуватися числа різних типів. За замовчуванням, для представлення цілих чисел використовується спеціальний підхід, який дозволяє представляти число найбільш ефективним способом автоматично перетворюючи числа, коли це необхідно. Для цілих чисел використовуються такі класи:

Integer

BigInteger

BigDecimal

Для чисел з плаваючою комою використовують клас Double.

Крім цих чисел, в Clojure визначений спеціальний тип Ratio, що представляє з себе число в вигляді раціональних дробів

Рядки: в Clojure є екземплярами класу java.lang.String і до них можна приміняти різні функції, що належать цьому клас

Знаки: є екземплярами класу java.lang.Character і записуються або у формі \ N, де N - відповідна літера, або як назви для невідображаємих букв, наприклад, як \ tab і \ space для символу табуляції і пробілу

Символи: використовуються для посилання на щось: параметри функцій, імена класів, глобальні змінні і т. д. Для представлення символу як окремого об'єкта, а не як значення, використовується стандартний запис 'symbol

Ключові символи: це спеціальні символи, що мають значення самих себе, аналогічно символам (symbols) у Lisp. Одною з їх важливих властивостей є дуже швидка операція перевірки на рівність. Для іменованих аргументів існує спеціальна форма запису: keyword. У мові Clojure є спеціальне значення nil, яке може використовуватися як значення будь-якого типу даних, і збігається з null в Java. nil може використовуватися в умовних конструкціях нарівні зі значенням false. Проте варто відзначити, що, на відміну від Lisp, nil і порожній список - () не є взаємозамінними

Колекції, послідовності і масиви: всі колекції в Clojure мають такі характеристики:,

  1. вся робота з колекціями проводиться через загальний інтерфейс

  2. існує можливість зв'язування метаданих з колекцією

  3. для колекцій реалізуються інтерфейси:

java.lang

Iterablе

java.util.Collection

  1. всі колекції розглядаються як послідовності даних, вне залежності від їхнього змісту всередині

Коллекції Clojure

Списки(lists): записуються точно так, як і в інших реалізаціях Lisp. У Clojure списки безпосередньо реалізують інтерфейс ISeq(нові елементи списку добавляються на початок)

Вектори(vectors): являють собою послідовності, елементи яких індексуються цілим числом (з послідовними значеннями індексу в діапазоні 0 -N, де N - розмір вектора).

Для визначення вектора необхідно укласти його елементи в квадратні дужки, наприклад, [1 2 3].

Для перетворення інших колекцій в вектор можна використовувати функції vector або vec

Відображення(maps): це спеціальний вид послідовності , який перетворює одні значення даних ( ключ) в інші ( значення) .

У Clojure існують два види відображень :

А)hash - map

Б)sorted - map

Їх створюються за допомогою відповідних функцій.

hash - map забезпечує більш швидкий доступ до даних , а sorted - map зберігає дані в відсортованому по ключу вигляді. Відображення записуються у вигляді набору значень (з парною кількістю елементів) , укладених у фігурні дужки.

Значення , що стоять на непарних позиціях розглядаються як ключі , а на парних - як значення, пов'язані з даним ключем.

Множини(sets): являють собою набір унікальних значень.Існує 2 види множин:

А) hash-set

Б) sorted-set

Визначення множини має такий вигляд:

# {elements ...}

а для створення безлічі з інших колекцій може використовуватися функція set, наприклад, для отримання безлічі унікальних значень вектора, можна використовувати наступний код:

user> (set [1 2 3 2 1 2 3])

#{1 2 3}

Відображення – структури: можуть використовуватися для емуляції записів (records), що є на інших мовах програмування.. Обчислення відображення – структури відбувається за допомогою макроса defstruct або функції create-struct.

Нові екземпляри відображень створюються за допомогою функції struct-map або struct, які отримують список елементів для отримання даного зображення.

Відображення – масиви(array maps): це спеціальний вид відображень, в якому зберігається порядок ключів. Такі значення реалізовані у вигляді звичайного масива, що має ключі та значення.

Нові відображення – масиви можуть створюватися за допомогою функції array-map.

Ліниві структури даних: це функції, що працюють з послідовностями та обробляють дані по мірі їх необхідності. Щоб виявити ліниву структуру в коді, потрібно скористатися макросом lazy-seq.

Приклад написання лінивих структур(Числа Фобіаччі)

(defn fibo

([] (concat [1 1] (fibo 1 1)))

([a b]

(let [n (+ a b)]

(lazy-seq (cons n (fibo b n))))))

Перехідні структури даних(transients): це спосіб уникнення непотрібного копіювання даних, що відбувається при роботі з незмінними даними.

Базові конструкції мови:

Синтаксис мови опиується S- виразами.

Код записується у вигляді списків., використовуючи префіксну нотацію(першим елементом стовпця є функція, макрос чи спеціальна форма.)

а інші елементи – аргументи, що будуть передані в цей вираз.

Об’ява і зв’язування символів:

Є два види зв'язування символу зі значенням:

лексичне, коли значення прив'язане до символу тільки всередині тіла однієї форми, наприклад let або fn. Якраз тут і проявляється особливість Clojure - функціональні змінні: всередині тіла let не можна встановити значення використовуючи set!. Можна вважати, що в цьому випадку x - це псевдонім для значення 2.

динамічне , коли значення прив'язується до символу на час виконання програми. Як правило , в цьому випадку символ робиться доступним глобально (хоча в Common Lisp він може бути і локальним , але оголошеним за допомогою declare special ) . Суть слова динамічність в тому , що в будь-який момент і в будь-якому місці символ може бути заново пов'язаний з іншим значенням , і значення буде « видно» при всіх зверненнях до цього символу в будь-яких формах. Такі динамічні ( в даному випадку і глобальні в рамках простору імен ) символи визначаються і зв'язуються в Clojure формою def , на основі якої потім будуються макроси defn , declare , defonce і т. д. Таким чином , наприклад , ім'я функції прив'язується до самого об'єкта функції , що буде видно на прикладі розкриття макросу defn нижче.

Для оголошення «лексичних» символів використовується форма let, яка виглядає наступним чином: (let [імя1 знач1 імя2 знач2 ....] код). Наприклад, виконання наступного коду:

(let [x 1

y 2]

(+ x y))

видасть значення 3. У цьому випадку змінні x і y видно тільки всередині форми (+ xy), і можливо маскують значення змінних, оголошених на глобальному рівні, або вище за кодом.

У деяких випадках, програміст може перевизначити значення «динамічного» символу для конкретного потоку виконання. Це робиться за допомогою макросу binding, який перевизначає значення зазначеного символу для даного потоку виконання і всіх викликаються з нього функцій. наприклад:

user> (def x 10)

user> (def y 20)

user> (defn test-fn []

(+ x y))

user> (test-fn)

30

user> (binding [x 11

y 22]

(test-fn))

33

user> (let [x 11

y 22]

(test-fn))

30

У даному коді, якщо ми виконаємо test-fn на верхньому рівні коду, то отримаємо значення 30, рівне сумі значень змінних x і y. А якщо ця функція буде викликана з binding, то ми отримаємо значення 33, рівне сумі змінних оголошених в binding.

Управління конструкціями:

Для управління потоком виконання програми в Clojure є деяка кількість спеціальних форм, на основі яких потім будуються інші примітиви мови, керують виконанням програми:

  1. Умовні оператори

  2. Цикли і рекурсивні форми

  3. Виключення

Умовні оператори

Для обробки простих умов використовується конструкція if , яка є спеціальною формою . На основі if потім будуються інші конструкції - when , when - not , cond і т. д. if виглядає стандартно для Lisp'образних мов: ( if умова t - вираз f - вираз ?) , Тобто якщо умова обчислюється в істинне значення , то виконується вираз t - вираз , інакше - вираз f - вираз ( воно може не вказуватися , що використовується в макросі when ) . Результати обчислення одного з цих виразів повертаються в якості результату if .Макрос cond дозволяє перевірити відразу декілька умов. За своїм синтаксису він відрізняється від cond в Common Lisp і Scheme , і в загальному вигляді записується як ( cond условіе1 вираження1 условіе2 вираженіе2 ... : else вираз - по- замовчуванням). Зауважте , що додаткових дужок навколо пари условіеN вираженіеN не потрібно.Наприклад:

(defn f1 [n]

(cond

(number? n) "number"

(string? n) "string"

:else "unknown"))

Цикли і рекурсивні функції

Для організації циклів Clojure має кілька спеціальних форм , функцій і макросів. Оскільки JVM має деякі обмеження , що не дозволяють реалізувати оптимізацію хвостових викликів ( Tail Call Optimization , TCO) , то це накладає обмеження на спосіб реалізації деяких алгоритмів , які зазвичай реалізуються через TCO в Scheme і інших мовах , що підтримують цю оптимізацію.Явні цикли організовуються за допомогою спеціальних форм loop і recur . Оголошення loop схоже на let , але при цьому є можливість повторного виконання виразів , шляхом виклику recur7 з тим же числом аргументів , які були оголошені в списку змінних loop - зазвичай це нові значення циклу. Ось простий приклад - реалізація функції обчислення факторіала за допомогою loop / recur (тут немає перевірки на негативний аргумент) :

(defn factorial[n]

(loop [cnt n

acc 1]

(if (zero? cnt)

acc

(recur (dec cnt) (* acc cnt)))))

Виключення:

lojure підтримує роботу з винятками ( exceptions ) , які часто використовуються в коді на Java. Спеціальна форма throw як аргумент отримує вираження , результат обчислення якого буде використаний як об'єкта - ісключенія8 .Для виконання виразів і перехоплення виключень , які можуть виникнути під час виконання коду , використовується спеціальна форма try . Форма try записується таким чином : ( try вираження * ( catch ім'я - класу аргумент вираження * ) * ( finally вираження * )?) . Блоки catch дозволяють обробляти різні виключення в одному виразі try . А форма finally може використовуватися для виразів , які повинні виконуватися, і для випадків нормального завершення коду , і якщо стався викид виключення - наприклад , для закриття файлів і подібних до цього завдань. Якщо ми введемо наступний код:

(try

(/ 1 0)

(println "not executed")

(catch ArithmeticException ex

(println (str "exception caught... " ex)))

(finally (println "finally is called")))

то на екрані побачимо наступне:

exception caught... java.lang.ArithmeticException: Divide by zero

finally is called