Неочевидное но вероятное. Часть 2: Julia
Продолжаем изучать необычные и малоизвестные языки программирования. В этот раз расскажу о Julia — очень молодом по меркам отрасли языке, который появился на свет в 2015 м году (моложе моего кота), но подающим большие надежды.
Язык и фреймворк
Не буду перечислять все возможности языка, опишу лишь то что показалось интересным лично мне.
Julia подается как «язык будущего», поэтому:
Julia is dynamically typed, feels like a scripting language, and has good support for interactive use.
Да, как в вашем любимом петоне или руби — тоже есть интерактивная консоль, через которую и происходят «основные события», поскольку управление пакетами также реализовали через нее.
Сразу расскажу про особенности тестовой среды, для всего описанного в статье я использовал FreeBSD 13.2, для которой также есть готовая бинарная сборка (какой сюрприз).
Самая популярная IDE для Julia называется Juno, которая уже успела устареть и перековаться в плагин для VSCode, который я и использовал, установив на версию VSCode для FreeBSD.
Начнем с того что понравилось прям сразу: «As with variables, Unicode can also be used for function names»:
julia> ∑(x,y) = x + y ∑ (generic function with 1 method) julia> ∑(2, 3) 5
Можно сказать самый первый признак чего-то претендующего на футуристичность в ИТ — изначальная и глубокая поддержка юникода.
Но конечно это мелочи по сравнению с вот таким:
Function composition and piping
Function composition is when you combine functions together and apply the resulting composition to arguments. You use the function composition operator (∘
) to compose the functions, so (f ∘ g)(args...)
is the same as f(g(args...))
.
For example, the sqrt
and +
functions can be composed like this:
julia> (sqrt ∘ +)(3, 6) 3.0
Не знаю появится ли подобное в моей любимой Java, но это отличный вариант избавиться наконец от гор ((( и ))) при подобных вложенных вызовах.
Promotion
Promotion refers to converting values of mixed types to a single common type.
julia> promote(1, 2.5, 3, 3//4) (1.0, 2.5, 3.0, 0.75)
Казалось бы мелочь, полная ерунда да? А теперь вспомните весь гемморой с конвертацией в числа с плавающей точкой и количество вопросов по теме на каком-нибудь Stackloverflow.
Generator Expressions
Comprehensions can also be written without the enclosing square brackets, producing an object known as a generator.
For example, the following expression sums a series without allocating memory:
julia> sum(1/n^2 for n=1:1000) 1.6439345666815615
Ключевое тут "without allocating memory" да.
Подставьте вместо 1000 скажем пару миллиардов чтобы понять всю важность этой штуки для расчетной логики.
The @threads
Macro
Не буду полностью пересказывать всю часть про это, можете прочитать по ссылке выше, расскажу про важность.
Для начала пример с решением многопоточного суммирования чисел от 1 до миллиона на Julia, с использованием этого макроса.
Buffers that are specific to the task may be used to segment the sum into chunks that are race-free. Heresum_single
is reused, with its own internal buffers
, and vectora
is split intonthreads()
chunks for parallel work vianthreads()
@spawn
-ed tasks.
julia> function sum_multi_good(a) chunks = Iterators.partition(a, length(a) ÷ Threads.nthreads()) tasks = map(chunks) do chunk Threads.@spawn sum_single(chunk) end chunk_sums = fetch.(tasks) return sum_single(chunk_sums) end sum_multi_good (generic function with 1 method) julia> sum_multi_good(1:1_000_000) 500000500000
Тоже самое на Java/C++/C# займет пару экранов кода, который еще далеко не факт что заработает правильно.
Multicast
Julia supports multicast over IPv4 and IPv6 using the User Datagram Protocol (UDP) as transport.
Что честно говоря было достаточно неожиданно для 21 века.
Вот так например выглядит отправка, причем на IPv6 (!):
To transmit data over UDP multicast, simply send
to the socket. Notice that it is not necessary for a sender to join the multicast group.
using Sockets group = Sockets.IPv6("ff05::5:6:7") socket = Sockets.UDPSocket() send(socket, group, 6789, "Hello over IPv6") close(socket)
Поздравляю, теперь вы тоже увидели как выглядит мультикаст на IPv6, благо что не каждый современный сисадмин знает что это такое вообще.
Примеры кода
Вот достаточно простой пример, рисующий фрактал символами в консоли:
function mandelbrot(a) z = 0 for i=1:50 z = z^2 + a end return z end for y=1.0:-0.05:-1.0 for x=-2.0:0.0315:0.5 abs(mandelbrot(complex(x, y))) < 2 ? print("*") : print(" ") end println() end
Подсветки синтаксиса в Телетайпе для столь редкого языка разумеется нет, поэтому в код придется вчитываться или копировать в редактор, где такая поддержка есть. Либо запустить онлайн.
Результат работы выглядит вот так:
Это был простой пример, теперь показываю кусочек сложного:
using LinearAlgebra: norm function collision(p::Particle, e::Ellipse) dotp = dot(p.vel, normalvec(e, p.pos)) dotp ≥ 0.0 && return nocollision() a = e.a; b = e.b pc = p.pos - e.c μ = p.vel[2]/p.vel[1] ψ = pc[2] - μ*pc[1] denomin = a*a*μ*μ + b*b Δ² = denomin - ψ*ψ Δ² ≤ 0 && return nocollision() Δ = sqrt(Δ²); f1 = -a*a*μ*ψ; f2 = b*b*ψ # just factors I1 = SV(f1 + a*b*Δ, f2 + a*b*μ*Δ)/denomin I2 = SV(f1 - a*b*Δ, f2 - a*b*μ*Δ)/denomin d1 = norm(pc - I1); d2 = norm(pc - I2) return d1 < d2 ? (d1, I1 + e.c) : (d2, I2 + e.c) end
Обратите внимание на синтаксис, думаю тем кто близок к матану сразу зайдет. Полностью демо выложено вот тут, описывается автором как:
Example that highlights the extendability and intuition Julia brings on the table.
Я после определенных изысканий все-таки смог его собрать и запустить на FreeBSD, выглядит вот так:
Как вы уже наверное догадались, Julia ориентирована в первую очередь на научную работу и ученых.
Поэтому есть мощные специализированные библиотеки, реализованные ради научных изысканий:
DynamicalBilliards
is an easy-to-use, modular and extendable Julia package for dynamical billiards in two dimensions. It is part of JuliaDynamics, an organization dedicated to creating high quality scientific software.
Но поскольку я все же больше инженер, то и задачи решаю куда более обыденные: веб, подключение к СУБД, REST да JSON.
Все такое пролетарское вообщем.
Поэтому «теста ради и практики для» сваял простой проект «Гостевой книги» со всеми стандартными для типичного разработчика вещами.
Так сказать для оценки и сопоставления с более обыденными языками.
Проект «Упоротая гостевая»
Я решил все же не делать совсем все вручную а использовать что-то готовое. Код как обычно выложен на гитхаб.
Веб-фреймворк
На удивление даже для столь редкого и «научного» языка нашелся вполне себе бодрый фреймворк, реализующий примерно тоже самое что и его более старшие собратья:
Oxygen is a micro-framework built on top of the HTTP.jl library. Breathe easy knowing you can quickly spin up a web server with abstractions you're already familiar with.
- Straightforward routing
- Auto-generated swagger documentation
- Out-of-the-box JSON serialization & deserialization (customizable)
- Type definition support for path parameters
- Built-in multithreading support
- Built-in Cron Scheduling (on endpoints & functions)
- Middleware chaining (at the application, router, and route levels)
- Static & Dynamic file hosting
- Route tagging
- Repeat tasks
Что еще нужно для счастья старого офицера спрашивается?
На самом деле надо конечно еще много чего, поэтому данный проект стоит рассматривать исключительно как эксперимент и тест, но не как повод бежать и немедленно переводить ваш высоконагруженный биллинг на эту технологию.
Хотя если вы читали мои предыдущие статьи — сие предупреждение новостью не будет.
База данных
Для примера я взял обычную MariaDb (в девичестве MySQL), которую для FreeBSD еще надо установить. Хотя врядли с этим возникнут проблемы, если уж вы дошли до этого места.
Разумеется что для тестового проекта я ограничусь минимумом, поэтому будет лишь одна таблица да пара полей:
CREATE DATABASE julia_test CHARACTER SET utf8 COLLATE utf8_general_ci; USE julia_test; CREATE TABLE posts ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(512) NOT NULL, author VARCHAR(255) NOT NULL, message VARCHAR(1024) NOT NULL, created_dt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id));
Запускаете MariaDb локально и цепляетесь:
mysql -u root -h 127.0.0.1 -p
Если все настроено правильно — появится консоль базы данных, в которую и вбиваете SQL-скрипт выше.
Проект
Забираем проект из github:
git clone https://github.com/alex0x08/julia-guestbook.git
Заходим в папку проекта и запускаем julia:
julia --project=.
Естественно что сама Julia при этом должна быть в переменной PATH.
Теперь нужно выполнить прекомпиляцию, которая запустит скачивание зависимостей.
Нажмите "]" , откроется режим работы с пакетами:
Введите «precompile», запустится скачивание зависимостей:
А затем и компиляция зависимостей:
Сам тестовый проект запускается двумя способами, первый из REPL:
julia --threads 4 --project=. src/МояУпоротаяГостевая.jl
Настройка подключения к базе находится в файле LocalPreferences.toml:
# Типа настойки. [MyGbSettings] dbserver = "127.0.0.1" dbuser = "root" dbpassword = "qwerty" dbdb = "julia_test"
Даже в тестовом проекте есть дополнительное отладочное логирование, включается через параметр окружения:
JULIA_DEBUG=МояУпоротаяГостевая julia --threads 4 --project=. src/МояУпоротаяГостевая.jl
После запуска открываете http://127.0.0.1:8080 в вашем любимом браузере. Выглядит это вот так:
Интерфейс Swagger будет доступен по адресу http://127.0.0.1/docs, выглядит вот так:
Теперь самое интересное, код проекта:
# # Тестовый проект "Упоротая гостевая" # Написан ради статьи в блоге https://teletype.in/@alex0x08 # Для большего фана, все что можно было локализовано. # module МояУпоротаяГостевая # используемые библиотеки using Oxygen,SwaggerMarkdown,HTTP,StructTypes,MySQL,Dates,Preferences,Pkg using Base: UUID # эта военная хитрость необходима чтобы обойти ограничение TOML на ANSII символы в ключах. # Фейковый UUID пакета, определяющийся через [extras] секцию Preferences.main_uuid[] = UUID("16e4e860-d6b8-5056-a518-93e88b6392ae") # DTO с настройками подключения к базе struct НастройкаПодключения хост::String база::String юзер::String пароль::String function НастройкаПодключения() new(@load_preference("dbserver"), @load_preference("dbdb"), @load_preference("dbuser"), @load_preference("dbpassword")) end end # # Типа DTO/Entity для записи в гостевой struct Пост # ID записи, автогенерируется id::Int32 # заголовок title::String # автор author::String # текст сообщения message::String # дата создания createdDt::Dates.DateTime # свой конструктор, для обработки пустого ID, когда это DTO используется для добавления нового поста function Пост(id, title, author,message,createdDt) new(id != nothing ? id : 0, title, author, message,createdDt != nothing ? createdDt : Dates.DateTime(0)) end end # Регистрация нашего DTO для автоматической (де)сериализации через библиотеку JSON3 StructTypes.StructType(::Type{Пост}) = StructTypes.Struct() # подключение к базе подключение = nothing # инициализация модуля (как в петоне) function __init__() setup() end # инициализация Упоротой Гостевой function setup() @info "инициализация.." @debug "упоротая отладка включена, поздравляю!" # Да, тут тоже есть Swagger @swagger """ /api/records: get: description: Отдает все посты в гостевой responses: '200': description: Типа все ОК. """ @get "/api/records" function(req::HTTP.Request) @debug "вызов API получения всех постов" # выходной массив с постами посты = Пост[] # получить выборку курсор = DBInterface.execute(МояУпоротаяГостевая.подключение, "select p.* from posts p order by p.created_dt desc limit 500") # формируем запись и пихаем в массив for запись in курсор push!(посты, Пост(запись[1], запись[2], запись[3],запись[4],запись[5])) end # отдаем массив DTO, который будет автоматически сериализован в JSON return посты end @swagger """ /api/delete: get: description: Удаляет запись гостевой responses: '200': description: Типа все ОК. """ @post "/api/delete" function(req::HTTP.Request) @debug "вызов API удаления поста" params=queryparams(req) # проверка "notin" - ключ id не в Dict if ("id" ∉ keys(params)) return HTTP.Response(400, "Параметр ID обязателен") end recordId = params["id"] if (length(recordId)<1 || length(recordId)>500) return HTTP.Response(400, "Параметр ID какой-то кривой") end DBInterface.execute(МояУпоротаяГостевая.подключение, "delete from posts where id = $(recordId)") HTTP.Response(200, "Запись удалена") end @swagger """ /api/add: get: description: Добавляет или обновляет запись гостевой responses: '200': description: Типа все ОК. """ @post "/api/add" function(req::HTTP.Request) @debug "вызов API добаления/обновления поста" пост = nothing # десериализуем из строки JSON в теле запроса в struct try пост = json(req, Пост) catch error @error "Ошибка разбора JSON: " exception=(error, catch_backtrace()) return HTTP.Response(500, "Упоротый сервер совершил ошибку") end if (length(пост.title)<3 || length(пост.author)<3 || length(пост.message)<3) return HTTP.Response(400, "Недостаточно данных для создания поста") end # если не был указан id то создаем запись if (пост.id>0) курсор = DBInterface.execute(МояУпоротаяГостевая.подключение, "UPDATE posts SET title='$(пост.title)', author='$(пост.author)', message='$(пост.message)', createdDt = now() WHERE id=$(пост.id)") # если указан - обновляем else курсор = DBInterface.execute(МояУпоротаяГостевая.подключение, "INSERT INTO posts (title, author, message) VALUES ('$(пост.title)','$(пост.author)','$(пост.message)')") end # ID созданной/обновленной записи идЗаписи = DBInterface.lastrowid(курсор) # вытаскиваем обновленную запись курсор2 = DBInterface.execute(МояУпоротаяГостевая.подключение, "select p.* from posts p where p.id = $(идЗаписи)") # получаем сами данные из курсора запись = first(курсор2) # возвращаем DTO, которое будет автоматически превращено в JSON return Пост(запись[1], запись[2], запись[3],запись[4],запись[5]) end # отдача страницы по-умолчанию get("/") do return file("content/gb.html") end # иконка get("/favicon.ico") do return file("content/favicon.ico") end # метаданные для сваггера info = Dict("title" => "API для упоротой гостевой", "version" => "1.0.0") openApi = OpenAPI("3.0", info) swagger_document = build(openApi) # генерация документации mergeschema(swagger_document) end function isREPL() abspath(PROGRAM_FILE) != @__FILE__ end # отдельная функция для запуска сервера, чтобы вызывать через REPL function runserver() @info "запуск упоротой гостевой" # отдача статики из папки "content", будет отдаваться по пути "/static" staticfiles("content", "static") # загрузка настроек подключения настройка = НастройкаПодключения() # подключение к СУБД МояУпоротаяГостевая.подключение = DBInterface.connect(MySQL.Connection, настройка.хост, настройка.юзер, настройка.пароль, db=настройка.база) @info "Подключение к СУБД установлено" # запуск HTTP сервера if isREPL() serve() else serveparallel() end end # если запуск не через REPL - считаем себя программой и запускаемся if !isREPL() # отдельно вызов настройки setup() # запуск runserver() end end # конец модуля
Еще есть небольшая часть на Javascript, HTML шаблон и минифреймворк на CSS, но они настолько банальны что не нуждаются в описании.