Хороший пример использования GraalVM (полиглот VM) для рендернига Reagent приложений на сервере (вызов Node.js из JVM) https://nextjournal.com/kommen/react-server-side-rendering-with-graalvm-for-clojure
Nextjournal
React Server Side Rendering with GraalVM for Clojure
Опубликовали все записи докладов с Curry On! https://www.youtube.com/channel/UC-WICcSW1k3HsScuXxDrp0w
Интересный доклад о файберах в JVM https://youtu.be/r6P0_FDr53Q
Интересный доклад о файберах в JVM https://youtu.be/r6P0_FDr53Q
YouTube
Curry On!
Share your videos with friends, family, and the world
Файберы в кложе 🙌 https://twitter.com/roman01la/status/1153989387211948032?s=09
Ещё перевести на них core.async, чтобы он действительно напоминал гошный CSP.
Ещё перевести на них core.async, чтобы он действительно напоминал гошный CSP.
Twitter
Roman Liutikov
Nice, Fibers and Continuations in Clojure with Project Loom
Небольшой эксперимент с добавлением async/await в ClojureScript https://twitter.com/roman01la/status/1155602189311717377?s=20
В данном случае вместо core.async, который всегда генерирует огромную стейт машину, здесь я добавил в компилятор обработку функций с мета-тегом
Дальше сгенерированный JS код отдается в Closure Compiler который в зависимости от указанной версии JS на выходе может оставить async/await как есть или трансформировать в генераторы или ту же стейт машину.
Это интересный эксперимент, но в реальном мире добавление нового, платформозависимого синтаксиса только больше навредит языку и разработчикам. async/await нету в Clojure JVM, поэтому сходу страдает портируемость кода. Ну и в целом необходимость в async/await на клиенте — большой вопрос. Часто вам приходиться разруливать нетривиальные асинхронные флоу во фронтенде? async/await именно для этого неплохо подходит, а отправить пару запросов достаточно промисов.
В данном случае вместо core.async, который всегда генерирует огромную стейт машину, здесь я добавил в компилятор обработку функций с мета-тегом
^:async, которые компилятором эмитят синтаксис асинхронных функций в JS async function ..., и специальную форму await.Дальше сгенерированный JS код отдается в Closure Compiler который в зависимости от указанной версии JS на выходе может оставить async/await как есть или трансформировать в генераторы или ту же стейт машину.
Это интересный эксперимент, но в реальном мире добавление нового, платформозависимого синтаксиса только больше навредит языку и разработчикам. async/await нету в Clojure JVM, поэтому сходу страдает портируемость кода. Ну и в целом необходимость в async/await на клиенте — большой вопрос. Часто вам приходиться разруливать нетривиальные асинхронные флоу во фронтенде? async/await именно для этого неплохо подходит, а отправить пару запросов достаточно промисов.
Twitter
Roman Liutikov
Alright, hacked the compiler. Here's async functions in cljs that emits JavaScript's async/await syntax.
На заметку: многие Webpack плагины решаются макросом в Clojure. Например require SVG иконок в инлайновые компоненты.
Через 30 минут вечерний стрим https://www.youtube.com/c/roman01la/live будем работать над патчами в cljs compiler
Интересный эксперимент с перфомансом чтения transit (https://github.com/cognitect/transit-cljs) в cljs.
Для ридера можно переопределять функции трансформации в map и vector. По умолчанию transit использует transient версии этих структур во время десериализации transit. Это уже быстрее, чем персистентые структуры, но если заменить дефолтные хендлеры на transient структуры (transient bean и transient arrayvector) из библиотеки для интеропа данных cljs-bean (https://github.com/mfikes/cljs-bean), то скорость чтения выростает на 30%.
Так выходит потому, что структуры данных в cljs-bean основаны на объектах JavaScript, а их transient версии тупо мутируют эти объекты, поэтому оверхед минимальный.
С сериализацией transit сложнее и она очень медленная :(
В целом если вам не нужно гонять в браузер расширенные типы данных, то лучше использовать JSON.
Для ридера можно переопределять функции трансформации в map и vector. По умолчанию transit использует transient версии этих структур во время десериализации transit. Это уже быстрее, чем персистентые структуры, но если заменить дефолтные хендлеры на transient структуры (transient bean и transient arrayvector) из библиотеки для интеропа данных cljs-bean (https://github.com/mfikes/cljs-bean), то скорость чтения выростает на 30%.
Так выходит потому, что структуры данных в cljs-bean основаны на объектах JavaScript, а их transient версии тупо мутируют эти объекты, поэтому оверхед минимальный.
С сериализацией transit сложнее и она очень медленная :(
В целом если вам не нужно гонять в браузер расширенные типы данных, то лучше использовать JSON.
GitHub
GitHub - cognitect/transit-cljs: Transit for ClojureScript
Transit for ClojureScript. Contribute to cognitect/transit-cljs development by creating an account on GitHub.
Недавно отправил патч в cljs compiler который убирает мертвые ветки в if когда тип значения в выражении теста известен заранее (if test then else) https://clojure.atlassian.net/browse/CLJS-2875
Патч нашел пару интересных ошибок в type inference компилятора.
Одна в loop/recur, когда изменяется тип параметра в цикле https://clojure.atlassian.net/browse/CLJS-3158
Вторая в reify, связанная с присвоением некорректного типа https://clojure.atlassian.net/browse/CLJS-3160
Патч нашел пару интересных ошибок в type inference компилятора.
Одна в loop/recur, когда изменяется тип параметра в цикле https://clojure.atlassian.net/browse/CLJS-3158
Вторая в reify, связанная с присвоением некорректного типа https://clojure.atlassian.net/browse/CLJS-3160
Отправил патч в Reagent который ускоряет интерпретацию Hiccup на ~34% https://github.com/reagent-project/reagent/pull/450
Это хороший пример, когда в "горячем коде" стоит использовать нативные методы в обход рефлексии.
Это хороший пример, когда в "горячем коде" стоит использовать нативные методы в обход рефлексии.
GitHub
Optimize dash-to-prop-name & capitalize fns by roman01la · Pull Request #450 · reagent-project/reagent
This PR optimizes string manipulation code in reagent.impl.util/dash-to-prop-name and reagent.impl.util/capitalize which results in ~34% speedup when interpreting Hiccup, according to synthetic ben...
В Reagent есть хитрый макрос with-let, который создаёт локальные биндинги в компоненте, но при этом они создаются только один раз, во время инициализации компонента.
Такую же штуку можно сделать макросом с хуком useRef. Например макрос use-let принимает биндинги и разворачивается в обычный let, но значения биндингов оборачиваются в useRef, таким образом в рефе фиксируется только начальное значение.
https://github.com/roman01la/uix/commit/3fdcc82388294812c56d6b866f4a7db276d9d567
Такую же штуку можно сделать макросом с хуком useRef. Например макрос use-let принимает биндинги и разворачивается в обычный let, но значения биндингов оборачиваются в useRef, таким образом в рефе фиксируется только начальное значение.
https://github.com/roman01la/uix/commit/3fdcc82388294812c56d6b866f4a7db276d9d567
GitHub
add use-let macro · roman01la/uix@3fdcc82
Experimental ClojureScript wrapper for modern React.js - roman01la/uix
Вышел апдейт cljs REPL Replete v2.2, теперь с веб-версией репла. Android версия для x64 получила 64bit сборку V8 https://twitter.com/mfikes/status/1173336849466765312?s=20
Twitter
Mike Fikes
Replete 2.2 has been released for iOS, macOS and Android, featuring the Fira Code font, and the newest member of the family: Replete Web! https://t.co/PNc5ZmEtZf #ClojureScript #Clojure
В ClojureScript есть хитрый метатег
Допустим вот такой пример
В первом случае вызывается функция
В данном случае, с вектором, вызов делегируется на фукцию
Однако такой вызов все равно пройдет через проверку типов, чтобы найти реализацию протокола для конкретного типа данных.
Маркер
Этот маркер часто применяеться в стандартной библиотеке cljs. В будущем проверки с
^not-native, который используется как маркер для генерирования прямого вызова имплементации протокола, в обход рефлексии. Эта оптимизация имеет смысл в «горячем» коде, который часто отрабатывает, например в цикле.Допустим вот такой пример
(def v [1 2 3])
(nth v 0)
(-nth v 0)
(-nth ^not-native v 0)
В первом случае вызывается функция
cljs.core/nth, которая работает не только с IIndexed, но и с JS массивами, строками, и неиндексированными последовательностями.В данном случае, с вектором, вызов делегируется на фукцию
cljs.core/-nth в протоколе IIndexed. То есть зная, что тип данных реализует конкретный протокол можно напрямую вызывать его реализацию (как во втором примере).Однако такой вызов все равно пройдет через проверку типов, чтобы найти реализацию протокола для конкретного типа данных.
Маркер
^not-native заставит компилятор сгенерировать прямой вызов реализации протокола. То есть вместо чего-то такого cljs.core._nth.invoke(v, 0), будет сгенерирован код типа v.cljs.core.IIndexed_nth(0).Этот маркер часто применяеться в стандартной библиотеке cljs. В будущем проверки с
implements? в коде будут автоматически маркировать проверяемые значения.(if (implements? IIndexed v)
(-nth v) ;; неявный ^not-native
(nth v))
Недавно нужно было поднять браузерные тесты на CI. В итоге оказалось проще сделать что-то свое, чем разбираться в настройке существующих решений.
Скрипт в 30 строк запускающий тесты Puppeteer https://github.com/roman01la/uix/blob/master/core/noscripts/test.js и конфиг для Circle CI https://github.com/roman01la/uix/blob/master/.circleci/config.yml
profit
Скрипт в 30 строк запускающий тесты Puppeteer https://github.com/roman01la/uix/blob/master/core/noscripts/test.js и конфиг для Circle CI https://github.com/roman01la/uix/blob/master/.circleci/config.yml
profit
GitHub
uix/core/noscripts/test.js at master · roman01la/uix
Idiomatic ClojureScript interface to modern React.js - roman01la/uix
Мы сегодня вышли в закрытую бету и ребята сделали клёвый лендинг 🙌 https://pitch.com/
Pitch
Presentation software for fast-moving teams | Pitch
Pitch is the complete pitching platform that enables any team to quickly create sleek presentations that get results. Sign up for free.
В Babel есть два плагина оптимизирующие React компоненты.
Первый инлайнит вызов React.createElement в объект, описывающий ноду в виртуальном DOM, который по сути в рантайме возвращается этим вызовом https://babeljs.io/docs/en/babel-plugin-transform-react-inline-elements
Второй плагин выносит константные React элементы за пределы компонента. Таким образом они создаются только один раз, а не на каждый вызов компонента, но висят в памяти всегда https://babeljs.io/docs/en/babel-plugin-transform-react-constant-elements
Для своей кложурной обертки с Hiccup было несложно сделать инлайнинг элементов, он по сути такой же, как и в том плагине. А с хоистингом элементов вышло интереснее.
В компиляторе ClojureScript есть оптимизация константных значений, сейчас только для keyword и symbol, когда все эти значения выносятся в отдельный неймспейс и по ссылке шарятся в местах использования в коде.
Оказалось несложно включиться в этот механизм и добавить хоистинг константных элементов.
Таким образом имея несколько идентичных кусков Hiccup в разных компонентах и из разных неймспейсов, в скомпилированном JS этот код будет заменен ссылкой на один и тот же элемент.
Первый инлайнит вызов React.createElement в объект, описывающий ноду в виртуальном DOM, который по сути в рантайме возвращается этим вызовом https://babeljs.io/docs/en/babel-plugin-transform-react-inline-elements
Второй плагин выносит константные React элементы за пределы компонента. Таким образом они создаются только один раз, а не на каждый вызов компонента, но висят в памяти всегда https://babeljs.io/docs/en/babel-plugin-transform-react-constant-elements
Для своей кложурной обертки с Hiccup было несложно сделать инлайнинг элементов, он по сути такой же, как и в том плагине. А с хоистингом элементов вышло интереснее.
В компиляторе ClojureScript есть оптимизация константных значений, сейчас только для keyword и symbol, когда все эти значения выносятся в отдельный неймспейс и по ссылке шарятся в местах использования в коде.
Оказалось несложно включиться в этот механизм и добавить хоистинг константных элементов.
Таким образом имея несколько идентичных кусков Hiccup в разных компонентах и из разных неймспейсов, в скомпилированном JS этот код будет заменен ссылкой на один и тот же элемент.
babeljs.io
@babel/plugin-transform-react-inline-elements · Babel
Note
Недавно наткнулся на минималистичную имплементацию инкрементальных вычислений Adapton https://arxiv.org/abs/1609.05337
Портировал код из пейпера в cljc https://github.com/roman01la/adapton и использую в качестве базы для альтернативы re-frame в UIx https://github.com/roman01la/uix/blob/master/core/src/xframe/core/alpha.cljc
Adapton сам по себе не имеет ничего общего с re-frame и co, но с помощью его примитивов можно строить граф зависимых вычислений (подписок), в котором вершиной будет состояние приложения.
Примитивов там всего два:
Вычисление (thunk) — это функция обернутая в инстанс Adapton ноды.
Adapton ref — мутабельная нода, в которую можно складывать состояние (например как atom).
Таким образом при изменении состояния Adapton инвалидирует зависимые ноды в графе, UIx вызывает ноды на которые подписан UI в данный момент и происходит перевычисление цепочек нод в графе. Важно, что вычисляются только зависимости активных нод в UI. Поэтому ноды которые использутся в других частях приложения, которые в данный момент не отрисованы, не будут обновлены пока не отрисуется необходимый UI.
Так же перевычисление нод не означает, что код в них будет выполняться. Adapton мемоизирует ноды. С другой стороны т.к. там используется простая мемоизация это означает, что кэш в памяти будет расти постоянно. Что не очень хорошо для UI приложений с долгой пользовательской сессией. Нужно прикручивать какой-нибудь LRU кэш.
Другие реализации этого подхода есть на OCaml https://github.com/janestreet/incremental и Rust https://github.com/salsa-rs/salsa
Salsa например используется в компиляторе Rust для инкрементальной компиляции.
Портировал код из пейпера в cljc https://github.com/roman01la/adapton и использую в качестве базы для альтернативы re-frame в UIx https://github.com/roman01la/uix/blob/master/core/src/xframe/core/alpha.cljc
Adapton сам по себе не имеет ничего общего с re-frame и co, но с помощью его примитивов можно строить граф зависимых вычислений (подписок), в котором вершиной будет состояние приложения.
Примитивов там всего два:
Вычисление (thunk) — это функция обернутая в инстанс Adapton ноды.
Adapton ref — мутабельная нода, в которую можно складывать состояние (например как atom).
Таким образом при изменении состояния Adapton инвалидирует зависимые ноды в графе, UIx вызывает ноды на которые подписан UI в данный момент и происходит перевычисление цепочек нод в графе. Важно, что вычисляются только зависимости активных нод в UI. Поэтому ноды которые использутся в других частях приложения, которые в данный момент не отрисованы, не будут обновлены пока не отрисуется необходимый UI.
Так же перевычисление нод не означает, что код в них будет выполняться. Adapton мемоизирует ноды. С другой стороны т.к. там используется простая мемоизация это означает, что кэш в памяти будет расти постоянно. Что не очень хорошо для UI приложений с долгой пользовательской сессией. Нужно прикручивать какой-нибудь LRU кэш.
Другие реализации этого подхода есть на OCaml https://github.com/janestreet/incremental и Rust https://github.com/salsa-rs/salsa
Salsa например используется в компиляторе Rust для инкрементальной компиляции.
arXiv.org
miniAdapton: A Minimal Implementation of Incremental Computation in Scheme
We describe a complete Scheme implementation of miniAdapton, which implements the core functionality of the Adapton system for incremental computation (also known as self-adjusting computation)....
Выделил минимальную реализацию async/await в компиляторе cljs в библиотеку https://github.com/roman01la/cljs-async-await
По факту — это расширение анализатора и компилятора cljs, поэтому использовать эту штуку нерекомендуется. К тому же существуют определенные ограничения описанные в секции limitations в readme.
По факту — это расширение анализатора и компилятора cljs, поэтому использовать эту штуку нерекомендуется. К тому же существуют определенные ограничения описанные в секции limitations в readme.
(require '[async-await.core :refer [async await]])
(defn http-get [url]
(async
(let [response (await (js/fetch url))
json (await (.json response))]
(.log js/console json))))GitHub
GitHub - roman01la/cljs-async-await: Experimental ClojureScript's compiler extension that enables JavaScript's async/await
Experimental ClojureScript's compiler extension that enables JavaScript's async/await - GitHub - roman01la/cljs-async-await: Experimental ClojureScript's compiler extension ...
Мы делаем ивент посвященный Clojure, 7-8 декабря, в Киеве. Регистрация уже открыта, больше информации на сайте https://clojureday.in.ua/
В ClojureScript есть type inference, хотя язык динамический, который используется для генерирования externs, вместо того чтобы писать их руками, и для генерирования оптимального кода.
Тип можно определить вручную, с помощью type hints, которые добавляют значение в поле
Благодаря type inference часто явный type hint не нужен. Компилятор сам распознает и добавит тип значения.
Помимо тегов значений есть теги возвращаемых значений из функции
Для
Типы возвращаемых значений тоже могут быть автоматически распознаны компилятором. В примере ниже функции
Благодаря тому, что возвращаемые типы пробрасываются через функции, например имея в кодовой базе одну функцию которая возвращает
Однако такая цепочка пробрасывания типов может быть прервана выражениями, возвращаемый тип которых компилятор неспособен определить.
Например известно, что
Поэтому я начал работать над патчем для return type inference в функциях высшего порядка. По факту это абстрактное выполнение кода на этапе компиляции, когда компилятор повторяет как конкретная функция отрабатывает в рантайме.
Например зная, что str всегда возвращает строку, можно утверждать, что
Интереснее становиться, когда у multiarity функций методы имеют разные типы возвращаемых значений.
В примере выше
Например для
Эту информацию можно использовать чтобы определить несовпадение количества аргументов с арностью методов. Для
Type hints можно использовать, чтобы помочь компилятору сгенерировать оптимальный код. В целом разница будет заметна только для hot path кода.
Например в проверках на правдивость механизм приведения типов в ClojureScript отличается от JS. Поэтому тестовое выражение будет обернуто в вызов функции, которая приведет тип согласно этому механизму.
Или например аргументы в
Если в первом случае x действительно
Тип можно определить вручную, с помощью type hints, которые добавляют значение в поле
:tag в метаданных этого значения(def ^number x 1)Благодаря type inference часто явный type hint не нужен. Компилятор сам распознает и добавит тип значения.
Помимо тегов значений есть теги возвращаемых значений из функции
(defn ^js/Promise http-get [url]
(js/Promise. ...))
Для
(.then (http-get url) on-ok) компилятор использует информацию о возвращаемоем типе чтобы сгенерировать extern Promise.prototype.then, который позже будет использован в Closure Compiler для генерирования корректного интероп кода в процессе оптимизации.Типы возвращаемых значений тоже могут быть автоматически распознаны компилятором. В примере ниже функции
f будет присвоен тип string.(defn f [] "123")Благодаря тому, что возвращаемые типы пробрасываются через функции, например имея в кодовой базе одну функцию которая возвращает
Promise, автоматический externs inference покроет все использования этой функции и компилятор сгенерирует корректный интероп код.Однако такая цепочка пробрасывания типов может быть прервана выражениями, возвращаемый тип которых компилятор неспособен определить.
Например известно, что
(str 1 2) вернёт string, но тип (apply str [1 2]) уже неизвестен. apply здесь — функция высшего порядка, которая вызовет str и тогда уже вернёт строку.Поэтому я начал работать над патчем для return type inference в функциях высшего порядка. По факту это абстрактное выполнение кода на этапе компиляции, когда компилятор повторяет как конкретная функция отрабатывает в рантайме.
Например зная, что str всегда возвращает строку, можно утверждать, что
(apply str xs) тоже вернёт строку.Интереснее становиться, когда у multiarity функций методы имеют разные типы возвращаемых значений.
(defn f
([a b] (+ a b))
([a b c] (str a b c))
В примере выше
f имеет тип #{number string}, но зная количество аргументов на этапе компиляции, можно определить конкретный тип.Например для
(apply f a b c xs) можно утверждать, что возвращаемый тип будет string, но никак не number.Эту информацию можно использовать чтобы определить несовпадение количества аргументов с арностью методов. Для
(apply f a b c d xs) можно точно сказать, что f не имеет метода с арностью 4 или больше.Type hints можно использовать, чтобы помочь компилятору сгенерировать оптимальный код. В целом разница будет заметна только для hot path кода.
Например в проверках на правдивость механизм приведения типов в ClojureScript отличается от JS. Поэтому тестовое выражение будет обернуто в вызов функции, которая приведет тип согласно этому механизму.
(when x true)
if (cljs.core._truth(x)) {
true
}
Или например аргументы в
str будут обернуты в вызов функции которая приводит значение к строке(str a b "s")
[str(b), str(a), "s"].join("")
Если в первом случае x действительно
Boolean и во втором случае a и b строки, то об этом можно сообщить компилятору с помощью type hints и сгенерированный код будет более оптимальным.(when ^boolean x true)
if (x) {
true
}
(str ^string a ^string b "s")
[a, b, "s"].join("")
Более интересные оптимизации, так называемые intrinsics, используют type inference для переписывания кода в более оптимальный для конкретного типа данных.
Например для выражения
Но такие оптимизация не всегда дают прирост в производительности.
Например, на первый взгляд
Однако, прогнав скомпилированный код мы увидим, что вариант с
Например для выражения
(count x), где известно, что x имеет тип string или array, компилятор сгенирирует x.length, вместо рантайм вызова протокола ICounted. Или для выражения (:x rec), где rec это Record, компилятор сгенирирует rec.x, вместо доступа к полю через вызов протокола.Но такие оптимизация не всегда дают прирост в производительности.
Например, на первый взгляд
(apply str a b xs) будет медленнее, чем аналогичная конструкция (clojure.string/join (cons a (cons b xs))), потому что apply соберёт аргументы редьюсом, а потом str соберёт строку в цикле. В то время, как str/join соберёт строку в одном цикле. Выходит, что первый вариант в 2-3 раза медленнее.Однако, прогнав скомпилированный код мы увидим, что вариант с
apply наоборот, в 2-3 раза быстрее str/join. Причина в том, что функция str/join достаточно коротка, чтобы компилятор заинлайнил ее в месте вызова (что говорит о том, что такой код будет ещё быстрее), но из-за отличий правил скоупинга в ClojureScript от JS заинлайненый код будет обернут в самовызывающуюся функцию. И тогда выходит, что на каждый вызов такого выражения будет создана новая функция, что и бьёт по производительности.Опубликовали запись докладов с Clojure Day https://www.youtube.com/playlist?list=PL7839nQBxfaQ1UgBoeZHki48DYdYF8K6w
YouTube
Clojure Day 2019 - YouTube