Erlang у Рисоваську, частина 1

Віртуальна машина та ноди

Для початку хочеться пояснити, що програми, написані на Ерланзі, виконуються тільки всередині віртуальної машини, яка в термінах Ерланга називається нода (node). Існують версії віртуальної машини (або Erlang/OTP) під більшість операційних систем (Windows, Linux, Mac OS, FreeBSD, читав, що Ерланг запустили навіть на iPhone). Так як її вихідники на C відкриті, то компіляція під будь-яку операційну систему не становить проблем. На одному комп'ютері може бути запущено кілька нід, хоча це потрібно рідко. Кожна нода повинна мати своє унікальне ім'я для спілкування з іншими нодами на інших комп'ютерах мережі. Якщо взаємодія коїться з іншими нодами в мережі не передбачається, то нода може мати імені. Ім'я ноди має такий формат: ім'я @ IP або назва комп'ютера. Наприклад, приклад для локальної мережі: [email protected]. Існує також і свій компілятор ерлангівських програм у рідний код процесора: HiPE (high-performance native code compiler). Він входить до складу Erlang/OTP. Найбільше прискорення HiPE дає при роботі з бінарними даними (майже десятикратне) і плаваючою арифметикою (foating point arithmetic), в інших випадках приріст швидкості незначний.

Змінні, значення яким можна присвоїти лише один раз

Що ж такого прикольного є в Ерланзі, чого немає в інших мовах і що так ламає мозок програмістам традиційними мовами програмування (С, Delphi, Basic тощо)?

В Ерланзі є змінні, але після присвоєння їм значення змінювати його не можна. Що це за змінні, скажете ви? Так, це все ж таки змінні, так у них є два стани: не пов'язані (значення ще не присвоєно) і пов'язані (значення присвоєно). І це зробленоу мові невипадково. По-перше, це спрощує складання сміття віртуальній машині (не потрібно стежити за покажчиками). По-друге, дозволяє легко писати алгоритми, що розбиваються на безліч окремих процесів, не боячись, що один процес зашкодить дані іншого, оскільки вони нічого не поділяють між собою.

А як мені реалізовувати, наприклад цикл, якщо значення змінної міняти не можна? Рекурсією! Ось найпростіший приклад на Ерланзі зведення у ступінь N числа X:

raising(1, _X, Result) -> Result; raising(N, X, Result) -> raising (N-1, X, Result * X).

Компактно та красиво. Наприклад, raising(3, 10, 10) поверне 1000. До речі, можете написати і raising(2000, 2, 2). Побачите дуже велику кількість, але обчислення виконуються без проблем. Тут прихована ще одна цікава властивість Ерланга: він не має суворої типізації змінних, але про це пізніше.

Рекурсія – це погано, скажете ви! Але не в Ерланзі, якщо рекурсія написана, як хвостова рекурсія (tail recursion), тобто коли після виклику функцією самої себе більше нічого не виконується. Приклад вище якраз приклад хвостової рекурсії: після raising(N-1, X, Amount*X) нічого немає. І тоді віртуальній машині не потрібно запам'ятовувати виклики функції у стеку. Вона їх відразу забуває, тому це працює дуже швидко і немає обмеження на кількість вкладень.

Все, що виконується на ноді, є або окремим процесом, або частиною іншого процесу. Процеси можуть породжувати інші процеси та стежити один за одним. Кожен процес має свій унікальний номер PID. Причому дуже важливо, PID унікальний не тільки на одній ноді, але і на всіх нодах в мережі! Свій власний процес PID може дізнатися викликавши функцію self(). Ось приклад запуску нашої функції як окремий процес:

Будь-якому процесу, крім PID, можна дати унікальне ім'я:

Після цього до такого процесу зручно звертатися на ім'я з будь-якої ноди в мережі, не знаючи його PID.

Знову ж таки може виникнути питання, що працює все це повільно. Ну вже немає! По-перше, процеси в Ерланзі не мають жодного відношення до процесів операційної системи (у Ерланга свій планувальник процесів). На звичайній середній машині прості процеси запускаються приблизно зі швидкістю 350 000 в секунду. Можливо процеси від'їдають багато пам'яті? Зовсім небагато: від 4кб на найпростіший процес. До того ж, процеси можна присипляти (hibernating) відразу після запуску, тоді можна скоротити розмір пам'яті взагалі до 1кб на один процес.

Щоб взаємодіяти між собою, процеси можуть надсилати повідомлення один одному:

Причому, синтаксис не змінюється від того, чи посилаєш повідомлення процесу на тій самій ноді або на іншому комп'ютері мережі!

А ось так повідомлення приймаються процесом:

receive -> Pid! hello; OtherMessage -> Io:format(“I received some strange message:

n”, [OtherMessage]) end.

Погодьтеся, код надсилання та отримання повідомлень дуже компактний. У прикладі вище процес буде чекати вічно, поки не отримає повідомлення. Можна цього уникнути, додавши в конструкцію receive блок "after N", де N-кількість мілісекунд очікування отримання повідомлення.

Головні особливості

Будь-якій змінній в Ерланзі можна надати значення будь-якого типу. Але як і можна перевірити якого типу значення у цій змінної, використовуючи вбудовані функції (is_integer, is_binary тощо.). Зазвичай відсутність суворої типізації в мові вважається недоліком, але на практиці я переконався, що найчастіше ця перевага і сильно підвищує гнучкість програми. До того ж, щоб уникнутипотенційних помилок з типами, до складу Ерланга входить статичний аналізатор Dialyzer, що виявляє такі помилки.

Супервізори

А тепер перейдемо до справді цікавих властивостей середовища розробки Erlang/OTP. Програми, написані в Ерланзі, вважаються зазвичай не просто надійними, а наднадійними. Як це виходить? Все просто. Вся програма написана на Ерланзі, спрощено кажучи, ділиться на супервізори (supervisor - спеціальний процес, що спостерігає за дочірніми процесами) і робочі процеси, що виконують основну роботу. Докладніше ви можете прочитати про це в OTP Design Principles. Є навіть таке смішне поняття в Ерланзі: дай процесу померти. І це справді так. Якщо процес перебувати під супервізором, то у разі свого падіння він буде перезапущений супервізором. Взагалі, у супервізора можна гнучко налаштовувати його поведінку у разі падіння дочірнього процесу. Наприклад, він може в такій ситуації зупинити всі дочірні процеси і запустити їх знову. Це необхідно у ситуації, коли дочірні процеси, як-небудь залежить друг від друга і падіння одного, може призвести до непрацездатності інших.

Таким чином, програма побудована на принципах OTP буде виглядати у вигляді дерева процесів:Малюнок взятий з OTP Design Principles

Враховуючи, що супервізори не виконують жодних обчислень, а лише спостерігають, то ймовірність їхнього падіння прагнутиме до нуля. Звичайно ж може впасти вся нода цілком, наприклад, через брак пам'яті, але й тут є рішення: можна запустити спеціальний процес операційної системи, що спостерігає за нодою і перезапускає її через певний час. Є й інший варіант: розподілені додатки (Distributed Applications) – це такі додатки, які можуть працювати на кількохнодах. Причому в один момент часу, програма працює тільки на одній ноді. У разі падіння ноди, на якій працює така програма, вона автоматично перезапускається на наступній ноді у списку. Список нод, де може працювати розподілений додаток, можна змінювати динамічно в процесі роботи.

Робота з бінарними даними

Ерланг дійсно має швидку роботу з бінарними даними (особливо у зв'язці з HiPE), тому на ньому дуже природно писати обробку вхідних бінарних даних, аж до роботи з окремими бітами. Тут у порівнянні з Java 6 Ерланг виграє у кілька разів за швидкістю роботи.

Недоліки

Стаття була б не повною, якщо не сказати і про основні недоліки Ерланга:

  • відсутність підтримки Unicode рядків (усі побитий недолік мови, правда вже цієї весни обіцяють додати їх підтримку в Erlang R13B; чекаємо),
  • повільна математика, писати на ньому якісь серйозні математичні розрахунки неефективно,
  • невелика кількість додаткових бібліотек (хоча все необхідне є і число бібліотек постійно зростає все ж таки ще далеко до такого розмаїття, як у .NET або C),
  • відсутність налагодженої та швидкої графічної бібліотеки, тому писати клієнтські програми на Ерланзі я б не став (звичайно є, наприклад, wxErlang, але бібліотека ще далека до завершення).
Але всі ці недоліки, крім мабуть відсутності підтримки Unicode рядків, або можна обійти, або фактично не є недоліками, тому що не є тим, для чого створювалася ця мова. А створювався він для високонавантажувальних, масштабованих, наднадійних систем. На початку мова була створена спеціально для застосування в телекомунікаціях, але як виявилося пізніше чудово підходить для створенняWeb-серверів та розподілених систем.

Далі буде