Комментарии 63
Ух, знатная норкомания. Мне по нраву :-)
Всю статью не осилил, но дошёл до определений фрактальный и фракций. Кажется до этого момента стоит ответить на два вопроса. Чем это отличается от реактивного программирования? Чем фрактал отличается от «потока» в терминах SICP?
Но тем не менее, начиная с последнего вопроса, постараюсь ответить так, как я это понимаю:
Поток — это некоторая последовательность порций информации.
Фрактал — это сущность, результатом работы которой является поток — последовательность порций информации, согласованной с его внутренним состоянием.
Каждая новая порция информации появляется в потоке в результате изменений, произошедших внутри фрактала — это реактивность.
Тогда вам стоит вот это почитать — http://conal.net/fran/tutorial.htm На практике реактивное программирование на потоках вылилось в штуки вроде reactive LINQ.
Спасибо за ссылку, я обязательно изучу этот материал.
Почитайте ещё эти статьи про реактивность: https://habr.com/ru/users/bgnx/posts/
Я тоже остановился примерно там же, но вопрос сформулировал ещё проще: что это, если не "автор открыл для себя декомпозицию". Мы просто делим сложный объект на более простые. Не? Или я что-то не уловил?
А вот неплохо, радует, что опыт познания мира начинает проникать в программирование. Тоже потихоньку пишу статью со своим симбиозом, но в основе не "всплытие" данных по дереву (как если смотреть на эту фрактальную картину сбоку), а единое глобальное состояние с доступом в любую точку. В материальном мире мы работаем с неэффективными передатчиками — свет, волны — которые обладают ограниченной скоростью и не позволяют получать доступ к краю Вселенной мгновенно, но в программировании можно обойти это ограничение и каждый элемент связывать "порталом" с глобальным стейтом:
@portal
class Input {
render() {
const { store } = this.portal;
return <input value={store.any.data.from.the.universe} />;
}
}
Этот же компонент имеет моральное и фактическое право менять глобальный стейт, как человек может шевелиться, меняя глобальное состояние Вселенной, при этом оно всегда остается цельным и согласованным.
То есть относительно ваших идей я предлагаю не пробираться сквозь фракталы для доступа к компонентам через длинную цепочку (под капотом) вызовов, а обращаться к любому объекту напрямую, будто до него рукой подать. Надо до Сириуса — пожалуйста, как будто он за окном, и за всеми двойными звездами, которые есть за ним. Если вы работали с observable-паттерном, то это должно быть знакомо.
Проверяю эту архитектуру на практике последний год, пока не могу наткнуться на ограничения, идеально масштабируется, разбивается на отдельные репозитории, состояние всегда синхронизировано с DOM и доступно из любого места.
Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею, но не вижу, чем бы была полезна такая вот фрактальность, которая добавляет лишний код и цепочки parent-child. Навскидку код будет сильно запутываться и при малейшей небрежности уходить в глубины call stack is too small for you imagination
Идея такая под грибами тоже проскакивает что все связано со всем. Но к сожалению фрактал первичен. Для изменения вам все таки нужно учитывать везде предельную скорость взаимодействия. А вот для связанных состояний да такого ограничения нет. Ваша идея могла бы наверно подойти для реализации сборщика мусора. А вот строить интерфейсы без структуры нельзя.
Разговор о том, что первично, тем более если вы сравниваете с концепцией "начало начал не имеет начала, безначальность бесконечна" (фрактал), был бы не особо уместен. В моем понимании это лишь одна из составляющих — движение есть следствие взаимодействия других сил, и для меня оно не первично.
Не знаю, как вы сделали вывод, что я отрицаю структуру в интерфейсах — но это другой слой, который не относится к данным, и представлен в виде иерархичного DOM-дерева, создаваемого опять же иерархичной композицией реакт-компонентов. Прокидывать же в инпут данные через всех родителей, а сигналы от него собирать по всей цепочке — это и есть ограничение, заставляющее "учитывать везде предельную скорость взаимодействия", портальность снимает эту проблему. Но она присутствует в цепочках react props, когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться, а в предлагаемой "фрактальной" архитектуре, похоже, именно так.
когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться
Если не ошибаюсь, в реакте эта проблема решается контекстами и порталами. Во vue3 также вводят порталы, назвав их телепортом. Нужен ли этот концепт в отрыве от фреймворка — немного сомнительно.
В отрыве от фреймворка это называется "двухсторонний биндинг" (https://seemple.js.org/), только с глобальным состоянием, а не локальным.
Двусторонний биндинг бесполезен без реактивного фреймворка/либы. К сожалению, для реакта — это не react-way, а в других фреймворках/либах он есть из коробки. Разве нет?
Вы, похоже, говорите о приложениях, в которых не используется js-хранилище с данными, а все они содержатся в атрибутах дом-дерева? И для операции с элементами вызываются getElementById, а затем в него записываются значения и дата-атрибуты? Да, подобная схема используется в крошечных простых приложениях, и все, о чем мы тут говорим, там не нужно.
Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею
О нет нет! Своим комментарием вы прям всколыхнули волну воспоминаний о том, что я это всё прошёл. Не бахвальства ради, что впереди на шаг, но быть может мне удастся повернуть вас с этого пути в мою сторону, тем более — сами напросились :) Смотрите.
let one = { name: 'John' }
let two = `<input placeholder="Name" value="John">`
По вашему пути one — это данные (элемент стора), а two — это вид, они должны быть максимально разнесены в архитектуре приложения и общаться между собой через длинные адреса а-ля `store.any.data.from.the.universe`
По моему пути и one, и two — это одни и те же данные, только представленные в разных форматах, первый — json, второй — html.
Фрактал — это более абстрактная сущность, для которой что вид, что модель — всего лишь результат работы. Общение между собой фракталам не нужно, т.е. не нужно пробираться сквозь уровни parent-child, они не обмениваются данными.
Ну вот не знаю про "шаг впереди". В ваших примерах есть как раз "обмен данными между компонентами" в виде
export const App = fractal(async function* () {
const { User } = await import('./user')
while (true) yield `User ${yield* User}`
})
В моей же схеме данные от User лежали бы в общедоступном хранилище, с ними удобно работать, нормализовывать, мутировать, создавать сложные селекторы из композиции различных параметров, сериализовывать в json-структуры, использовать для общения с другими сервисами по АПИ.
А ваш подход скорее из мира, когда данные хранились в дата-атрибутах DOM, постоянно извлекались и сериализовывались-десериализовывались, и так же работать с постоянными yield для извлечение набора актуальных данных — это затратно. Код будет погрязать в бесконечных
Promise.all(await exec(One), await exec(Two))
.then(arr => arr.map(frame => frame.data))
.then(finallyGotSomeRelevantData)
// вместо
const store = observable({
one: 1,
two: 2,
get relevantData() { return [this.one, this.two]}
})
const easyGetAllYouNeed = store.relevantData;
Не очень понял про то, как вы хотите одну сущность переводить одновременно в json и html — то есть в объектах описывать все возможные комбинации раскладки и атрибуты? Вроде нет, в ваших примерах на гитхабе используются вполне классический JSX в xml-формате, только с очень сложным извлечением данных.
Что-то почитал репозитории и, кажется, толковую дискуссию мы не создадим. Мне банально непонятен весь этот звездочный код и его смысл
const Counters = fractal(async function* _Counters() {
yield* TODO_MODE(TodoMode.Service)
while (true) {
let active = 0
let completed = 0
for (const Todo of yield* Todos) {
const { Done } = (yield* Todo) as TodoService
;(yield* Done) ? completed++ : active++
}
yield { completed, active }
}
})
Тоже много лет назад писал подобные системы, полные функциональщины и скрытых слоев, с привкусом Алохоморы при выполнении. Но потом дорос до понимания, что лучший код — максимально простой и понятный, так как работаю в корпоративных продуктах, где это — ключ к успеху. Бывает, приходят ребята с очень нестандартными подходами, но их код не понимают и приходится переписывать после их ухода. В общем, не вижу пользы от вашего подхода, кроме как для саморазвития)
Ну вот не знаю про "шаг впереди".
Ни в коем случае не хотел вас как-то "задеть" ) не зашла шутка :)
В ваших примерах есть как раз "обмен данными между компонентами" в виде
while (true) yield `User ${yield* User}`
я так понимаю, что под обменом данных вы тут подразумеваете отдачу наверх через yield
и получение от User
через yield*
, если так, то это скорее не обмен данных, а проброс вверх по дереву с предварительным маппингом.
Общение между собой фракталам не нужно, т.е. не нужно пробираться сквозь уровни parent-child, они не обмениваются данными.
под этим я подразумевал то, что в правильно построенной фрактальной архитектуре нам никогда не потребуется получать доступ к фракталам, которые являются детьми наших детей, т.е. App.User.Age и т.д.
Promise.all(await exec(One), await exec(Two))
.then(arr => arr.map(frame => frame.data))
.then(finallyGotSomeRelevantData)
так нам, тоже никогда не потребуется делать, правильный эквивалент этого кода будет выглядеть так
const Three = fractal(async function*(){
while(true) yield [yield* One, yield* Two]
})
Дальше Three
опять будет подключен к какому-то фракталу, где его данные пройдут через эквивалент finallyGotSomeRelevantData
и т.д. Вы пытаетесь извлечь выгоду из фрактала локально, для какой-то операции в контексте приложения с традиционным подходом, игнорируя главную суть — всё есть фрактал, всё приложение, каждый его кусочек. Продукт работы фрактала — это поток информации. Фракталами описывается всё дерево приложения от самого корня и до каждого листика.
В моей же схеме данные от User лежали бы в общедоступном хранилище, с ними удобно работать, нормализовывать, мутировать, создавать сложные селекторы…
Что-то почитал репозитории и, кажется, толковую дискуссию мы не создадим...
проблема в том, что я вашу схему полностью понимаю, но на мою вы похоже смотрите через призму своей, подсознательно сравнивая их, это не верно — между ними нет ничего общего — это два разных подхода, поверьте — я уважаю ваш выбор, и даже не думал вступать в спор о том у кого круче, скорее всего в статье у меня просто не получилось донести до вас ключевые моменты моей идеи
Не очень понял про то, как вы хотите одну сущность переводить одновременно в json и html — то есть в объектах описывать все возможные комбинации раскладки и атрибуты?
Вот этот пример fract.github.io/factors показывает как один и тот же фрактал в зависимости от фактора MODE
выдает поток данных разного формата, исходники
Вот этот пример fract.github.io/antistress демонстрирует это в работе. Каждый фрактал приложения умеет отдавать и jsx, и json. Создаются два потока — jsx идет на экран, json в localStorage. Если вы откроете devtools, то увидите как данные обновляются одновременно с изображением на экране. Более того при перезагрузке состояние фрактала восстанавливается из последних сохраненных в localStorage данных. исходники
С уважением )
Спасибо за пояснение, действительно я сознательно сравниваю все паттерны, которые когда-либо использовал, с чем-то новым, чтобы понять выгоду. Для меня цели архитектуры — комфорт в разработке бизнес-функционала, минимизация возможных ошибок, простота масштабирования и возможность легкой интеграции сторонних решений. С этого угла зрения, к сожалению, полностью фрактальная архитектура не подходит.
А как идея — интересно. Если абстрагироваться от всплытия по yield (это ключевая фича, как понимаю, но все же) семантически подход очень напомнил effector, когда на каждый тип данных формируется локальный стор с методами для изменения
export function newUser() {
const Name = fraction('John')
const Age = fraction(33)
const handleNameChange = (e) => Name.use(e.target.value)
const handleAgeChange = (e) => Age.use(parseInt(e.target.value))
while (true) {
yield (
<NameInput onChange={handleNameChange} defaultValue={yield* Name} />
<AgeInput onChange={handleAgeChange} defaultValue={yield* Age} />
)
}
}
// vs
const increment = createEvent('increment')
const decrement = createEvent('decrement')
const resetCounter = createEvent('reset counter')
const counter = createStore(0)
.on(increment, state => state + 1)
.on(decrement, state => state - 1)
.reset(resetCounter)
const App = () => {
const value = useStore(counter)
return (
<>
<div>{value}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={resetCounter}>reset</button>
</>
)
}
Для крупных приложений такие подходы неудобны, в реализациях, которые видел, постепенно все равно код стремится к формированию более крупных и независимых от компонентов состояний, абстрагированию от библиотеки для реализации потоков данных и минимизации кода для проброса в видимые части интерфейса. Абстрагироваться же от yield-перехватчиков на разных уровнях не получится, все должно быть в этой парадигме. А это быстро приведет к устареванию подхода и через какое-то время к полному переписыванию на более гибкие рельсы и на новые технологии (во фронтенде значимые изменения часты).
Вот такой вот прагматичный подход, тоже не в обиду, но я так вижу)
Советую порыть в сторону функциональных ЯП и ленивых вычислений. Не силен в функциональщине, но мне кажется Ваши фракталы имеет много общего с монадами. Может подчерпнете интересных идей.
Тема про return в асинк-генераторах не раскрыта. И ее связь с async-await, кстати.
В быту чаще всего мы используем генераторы для перебора некоторой последовательности значений, последовательность генерируется на лету, значения перебираются как правило в цикле либо spread-оператором
function* range(start, finish) {
do {
yield start++
} while (start <= finish)
return 'end of range'
}
for (const num of range(1, 3)) {
console.log(num)
}
/*
> 1
> 2
> 3
*/
const arr = [...range(1, 3)]
// [1, 2, 3]
В генераторе range мы указали return
явно, но в действительности ни в первом, ни во втором варианте использования это попросту не нужно, поскольку по факту используется он только для прерывания процесса перебора, а возвращаемое им значение просто отбрасывается за ненадобностью.
Выражение yield*
мы обычно используем, чтобы делегировать генерирование последовательности другому генератору, и практически никогда не используем его вторую особенность — получение return-значения. Попросту по тому, что не случается кейсов когда нам это действительно нужно. Это если рассматривать и использовать генераторы, как то для чего они предназначены — перебор коллекций.
function* range1to3() {
const returnValue = yield* range(1, 3)
// 'end of range' - но что с этим делать?
}
const arr = [...range1to3()]
// [1, 2, 3]
Но что если рассматривать и использовать их как обычные функции, не для перебора коллекций, а для обычного вычисления какого-то результата.
function* sum(one, two) {
return one + two
}
function* pifagor(one, two) {
return Math.sqrt(yield* sum(one ** 2, two ** 2))
}
тогда return
будет использоваться привычным нам образом для возврата результата вычисления, а yield*
для того чтобы этот результат получить, но при этом варианте использования за бортом остается yield
— не понятно как тут задействовать его функционал, просто не видится кейсов.
Так вот во фрактале работает как раз второй вариант использования генераторов, а один из кейсов использования yield
— обмен служебной информацией между уровнями вложенности.
const GET_PARENT = Symbol('parent query')
function wrap(gen) {
return function* wrapper() {
const parentName = yield GET_PARENT
console.log(`Calc ${gen.name} for ${parentName}`)
const iterator = gen()
let input
while (true) {
const { done, value } = iterator.next(input)
if (done) return value
if (value === GET_PARENT) {
input = gen.name
continue
}
throw 'unforeseen case'
}
}
}
const sum = wrap(function* (one, two) {
return one + two
})
const pifagor = wrap(function* (one, two) {
return Math.sqrt(yield* sum(one ** 2, two ** 2))
})
const main = wrap(function* () {
console.log(yield* pifagor(3, 4))
//> Calc pifagor for main
//> Calc sum for pifagor
//> 5
})
Очень надеюсь, что смог прояснить этот момент.
И ее связь с async-await, кстати.
Вот эта часть вашего вопроса мне не совсем понятна, поясните пожалуйста, что вы имели в виду.
Ну просто у меня есть догадка (возможно, неверная), что когда хочется написать const res = yield something(), то обычно это на самом деле желание сделать res = await something() и иметь something() как async function. Yield предназначен для генерации последовательностей, его использование для чего-то еще — пережитки старины глубокой, когда люди еще не догадались до async-await.
Суть идеи заключается в том, чтобы делить приложение не горизонтально на модели, виды, контроллеры и т.д., а вглубь на фрактально-древовидную структуру, где каждый узел является самостоятельным законченным приложением. Результат работы каждого такого приложения — это поток информации, отражающей его внутреннее состояние.
Походу вы функциональное программирование изобрели :)
В ней вот такие шедевры выполняют: www.youtube.com/watch?v=OFFrdW2_7Lo
Рекомендую просмотр либо в анаглиф очках, либо на смартфоне через youtube app + cardboard для просмотра в стереоскопическом режиме.
А вообще жаль что вы не художник, с вашим складом ума, и любви к ковырянием в деталях у вас бы могли получится отличные серии картин, на подобие того как Клод Моне Руанский собор рисовал в разных вариациях…
Боюсь, помимо отображения вам еще понадобится само множество
А ведь всё что есть — это отображение множества в другое множество
По моему тут всё же речь об одном множестве, второе это результат преобразования первого. Да их два, но исходно — одно
Вот интересно, до каких пор «программисты» будут ацки извращаться, пока не поймут банальности?
Фрактал это и есть та самая банальность, абстрактное множество, главная задача которого, как раз преобразование в разные виды множеств
Вы по сути, сами не заметив, четко и емко сформулировали суть :) Спасибо
Если под множеством вы подразумеваете именно коллекцию, то я пожалуй изначально не правильно расценил этот термин в данном контексте.
Тем не менее позвольте всё таки продолжить мысль о преобразовании.
Небольшое отступление — представьте театр теней, мы устанавливаем фонарик напротив стены, помещаем в поток света руку и видим на стене её тень, путем различных манипуляций мы получаем зайца, волка, птицу и т.д. Затем мы добавляем второй фонарик, свет которого создает тень от этой же руки на полу. Таким образом мы одновременно получаем две тени одной руки, да, они выглядят скорее всего по разному, но это не важно, важно то, что они полностью синхронизированы с рукой и незамедлительно реагируют на её движения.
Так вот: если рука — это исходное "множество" некоторых данных, то тень — это новое "множество", полученное путём преобразования исходного, а именно проецированием трёхмерной сущности (руки) на плоскость в двухмерную сущность (тень) с учетом различных факторов, как то: угол падения света, его яркость и т.д.
Рука — это и есть фрактал, а тень — это проекция фрактала, созданная с учетом заданных факторов. Помещая один и тот же фрактал в разные условия (добавляя фонарики), мы можем получать несколько "теней" одновременно — json
, html
… одно выводить на экран, другое сохранять в localStorage, при этом фрактал заботится о поддержке актуальности своих проекций в ответ на изменение своего состояния (как движения руки приводят к изменению её теней), как например: пользователь, взаимодействуя с проекцией html
, меняет внутреннее состояние фрактала, провоцируя изменение проекции json
и, как следствие, обновление данных в хранилище. Фрактал — это как бы мост между всеми своими проекциями.
О генерации проекций: что мы делаем, когда нам нужно сериализовать модель?
Подход первый: мы пишем функцию-сериализатор, которая рекурсивно обходит все ее свойства и на выходе отдает нам объект, который по сути отражает текущее внутреннее состояние модели и всех её связей. Но что, если нам нужно, чтобы в итоговый набор попали только определенные свойства модели? — тогда мы должны сообщить сериализатору "что брать, что не брать", для этого мы используем например декоратор @serializable
, которым помечаем нужные поля. В последствии мы отдаем этот объект в JSON.serialize
и получаем строку, которую куда-то там отправляем и т.д.
Подход второй: мы определяем в каждой модели метод toJSON
, в котором описываем порядок сериализации именно этой модели. Далее мы опять же отдаем модель в JSON.serialize
и получаем тот же результат.
Но, что если точно также, как во втором подходе, мы будем определять в моделях метод toHTML
, отвечающий за генерацию вида именно этой модели, а дальше мы будем отдавать модель в некий метод HTML.serialize
и получать полный вид модели, с учетом всех её зависимостей.
А что если и JSON.serialize
и HTML.serialize
будут отдавать не разовый снимок, а последовательность снимков, в которой каждый следующий генерируется, как только в модели появились изменения, а точнее в тех её свойствах, которые участвовали в сериализации? Тогда, используя html
-последовательность мы сможем обновлять изображение на экране, а данные json
-последовательности будут параллельно сохраняться в хранилище. Модель будет мостом между json
и html
последовательностями, уйдет за кулисы и станет чем-то большим чем просто "модель", это будет некий граф, каждый узел которого может "сворачиваться" в разные представления и обеспечивать поддержку их актуальности.
Фрактал — это как раз идея о том, чтобы строить приложения в виде таких графов.
В классическом MV*
подходе мы описываем граф данных M
и "граф видов" V
, далее отправляем оба этих графа в некий render
, он их "объединяет" и мы получаем результат. В итоге: два графа один результат.
Во фрактальном подходе все наоборот: мы описываем один граф, но каждую ноду "учим" отдавать два результата. В итоге: один граф, два результата (три, четыре — это уже как фантазия позволяет). Суть в том, что в разработке обслуживать один граф проще, чем два. Конечно же, по моему сугубо личному мнению :)
Вот собственно, что я подразумевал под отображением одного множества в другие, может быть с этого примера и стоило бы начать статью, но "хорошая мысля приходит опосля".
С уважением)
P.S. На днях стукнуло в голову сделать что-то типа редактора фрактального дерева, а чтобы привязать его к конкретной задаче — взял для примера html-код, ведь это фрактал, всего один блок с детьми в виде таких же блоков и т.д.
В итоге получился редактор, состоящий из одного и того же фрактала Block
, который вложен сам в себя и в зависимости от условий может сворачиваться до
View — вид предварительного просмотра
Style — редактор своих стилей
Tree — дерево навигации
Data — набор данных, которые сохраняются в localStorage и потом используются для восстановления приложения в исходное состояние. Первичное дерево я по дефолту добавил, чтоб не пусто было, справа на панелях можно поредактировать его состояние.
Вот тут можно посмотреть "механизм сворачивания узла". Не самый чистый код, делал для себя на пробу, но основные моменты можно проследить.
А если серьезно, это просто реактивное программирование, целиком завёрнутое в генераторы, из-за чего «вход рубль, а выход — два»:
1) Весь код async, из-за чего браузер будет устраивать стейт-машину по каждому вызову, JIT-компилятор не сможет ничего заинлайнить, и каждое вычисление «фрейма» будет неоправданно дорогим в сравнении с вычислением такой же структуры данных традиционными способами. См. redux-saga, у которого те же грабли. Заворачивание кода в async имеет весьма заметную цену, а заворачивание туда всего кода (а не только того, который реально должен быть async) — это слив производительности в трубу только потому, что программист такой весь из себя фиялка и ему так проще думать.
2) Глубина связей данных не может превышать глубину стека вызовов. Да, для современных браузеров это не то, чтоб прямо очень страшно, поскольку счёт идёт минимум на десятки тысяч. Но, например, в хроме некоторой степени лежалости глубина стека вызовов всего лишь в районе 20К. Наивная попытка запилить на этом объемный граф может легко обрушиться в maximum call stack size exceeded.
3) Что там с циклическими зависимостями?
4) А что с быстродействием? Как она будет шевелиться, если кто-нибудь будет динамически переставлять зависимости десятками тысяч в какой-нибудь сложной структуре?
Архитектурный астронавт — да, пожалуй это в точности про меня)
Я не презентую готовое решение всех проблем, я презентую идею и концепт, показывающий, как проблемы могли бы решаться.
Да, async это заметное падение производительности и быстродействие не конёк текущей реализации. Для того чтобы этот код шустро отрабатывал ему возможно необходима своя собственная среда выполнения, но поскольку это уже из области фантастики — я естественно рассматриваю варианты оптимизации, как использование синхронных генераторов там, где асинхронность не нужна и т.д.
Циклические зависимости — отдельная тема, архитектурно я не представляю кейса, когда это действительно нужно и дабы пресекать возможность случайного создания таких ситуаций с последующим долгим дебагом, я обдумываю механизм их отслеживания.
Классика никогда не устареет.
Я тоже вначале об этом подумал, но тут все-таки не совсем так – автор же и сам попробовал создавать приложения, и написал интеграцию с Реактом.
Меня тоже смущают генераторы, у меня сложилось ощущение, что они очень медленные. Но тут все-таки «мясо» идеи не в генераторах, а в том, как смотреть на потоки данных.
Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта. Разве что тут задается не только структура интерфейса, но и любых других данных
Но тут все-таки «мясо» идеи не в генераторах
Нет. Но мясо идеи тут — это обычное pull-based FRP. А генераторы — да, конкретная (но очень проблемная для использования «в жизни») форма выражения.
Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта.
Ну, сложно говорить всерьез про достоинства «реактивности из реакта», потому что она не выделена в standalone-абстракцию. Рассуждать о преобразовании данных в терминах компонентов реакта конечно можно (более того, я видел людей, которые так делают), но… зачем? Вырожденные компоненты с рендером-заглушкой, зоопарк HOC, и вот это всё, когда тебе нужно просто без всякой визуализации данные погонять? MVC не просто так придумали, хотя да, очень много программистов до сих пор не понимает, что им делать с C и в каком виде его представлять (см. статью собссно). Да и насчет M тоже порой встречаются очень своеобразные толкования. Ладно хоть V не вызывает вопросов обычно, кроме того, что в V пытаются впихнуть весь MVC и жить с этим.
Но тут все-таки «мясо» идеи не в генераторах, а в том, как смотреть на потоки данных.
Именно так. Генераторы с их возможностью ввода/вывода параметров по ходу выполнения просто оказались идеальными кандидатами для организации локаничного решения.
Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта.
Ни в чем, я не ставлю фрактал в противовес реакту, реактивность последнего во фрактальных приложениях попросту не используется, используется только рендеринг jsx -> html, что может быть заменено на гораздо более легковесную альтернативу.
Интеграцию именно с react я написал потому что это привычный для меня инструмент и естественно это первое, что пришло мне в голову, плюс ко всему — широкий охват желающих попробовать с минимумом усилий. Был бы я backend разработчиком — я бы в первую очередь написал интеграцию с чем-то серверным.
Разве что тут задается не только структура интерфейса, но и любых других данных
Всё правильно, фрактал задает общий стиль для описания любых данных и автоматически организует их "течение". Если рассматривать всё приложение как одно большое дерево с потоком данных, направленным к корню, то на листьях будут скорее всего примитивы, а на выходе из корня поток сложных структур, например jsx-снимков, которые отдаются реакту для рендера.
Продолжая разговор о генераторах и реакте, может не по теме, но могу привести некоторую параллель, смотрите — ниже два абсолютно идентичных компонента, первый реакт, второй фрактал
function Counter() {
const [count, setCount] = useState(0)
// !
return (
<div>
<p>Current value {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
const Counter = fractal(async function* () {
const Value = fraction(0)
while (true) { // !
const count = yield* Value
yield (
<div>
<p>Current value {count}</p>
<button onClick={() => Value.use(count + 1)}>Increment</button>
</div>
)
}
})
Реакт будет вызывать функцию Counter
каждый раз, когда будет вызываться setCount
, при этом useState
по факту отработает полностью только при первом вызове, реакт отслеживает это внутри себя с помощью "магии" и при последующих вызовах реакт пропускает её, как бы имитирует то, что код сразу же выполняется с того места где я поставил восклицательный знак.
Фрактал просто идет до первого yield
(по пути собирая зависимости, подключенные через yield*
) отдает полученное значение в поток и ждёт изменений, как только дождался, код, благодаря генератору, просто продолжает своё выполнение c момента последней остановки до следующего yield
и так по кругу. Никакой магии — всё последовательно и логично, но суть не в этом.
Не правда ли эти два компонента даже визуально на уровне кода выглядят очень похожими? Существенная разница здесь в том, что react компонент сильно ограничен тем, что должен использоваться только в составе jsx-кода и отдавать значение, соответствующее интерфейсу JSX.Element
, в то время как фрактал может отдавать что угодно и использоваться в составе чего угодно.
Надеюсь, что пример получился удачным :)
Не правда ли эти два компонента даже визуально на уровне кода выглядят очень похожими?
Да, я понимаю! Как раз в этом и был вопрос – я вижу, что это уж слишком похоже на Реакт.
Может быть, есть примеры, как это использовать не для построения интерфейса? Если это новая абстракция, которая отличается от реактивных вложенных компонент Реакта, но в чем ее преимущества? Где она засияет, там где Реакт спотыкается? Или где другие абстракции не смогли понятным образом описать задачу?
П.С. Кто сказал, что программисты психи? Вам просто этого не понять!
Почти переизобрели монады, неплохо.
Под примесями я подразумевал html и css. Люблю просто писать код, а вот верстка, хоть и делаю хорошо и добросовестно, но это для меня то ещё мучение — минимум фантазии, просто стук по клавишам :)
Упустил, не раскрыл этот момент в статье.
Typescript потому, что без типов после одного большого проекта мне стало как-то тяжеловато.
Замечательная статья.
Давным-давно смотрел на визуализацию Winamp'а и не понимал как это "видео" может занимать килобайты…
Но когда узнал про соревнования 64 kB и меньше, когда "влезает" даже "вселенная"…
https://habr.com/ru/company/mailru/blog/406969/
2. Были попытки делать что-то с йелдами, но не взлетели: они слишком сложные для рядового разработчика.
3. 80% всей работы по внедрению технологии: обучение и практика применения. Это самое сложное.
4. Не знаю. На самом деле есть большой и стабильный HTML, который как бы baselin веб-разработки и все начинают учить с него. Если ваш подход взлетит, вы сможете привлечь людей, а послезавтра у него найдется фатальный недостаток, то люди останутся без работы и перед необходимостью учить все тот же HTML.
Я не буду делать вид, что до конца понял идею в статье, но издалека это всё здорово напоминает CPS с несколькими разными типами chaining'a продолжений.
Надеюсь, разбор чужих достижений (монады, комонады, их связь с async/await и (ко)монадой продолжения) поможет выразить это всё на более общепринятом языке. Именование переменных всё-таки важно. Я так же верю, что это сильно упростило бы код.
Вопросы производительности, кстати, тоже интересны, раз уж тут осторожно упоминается возможность использования сабжа в проектировании ОС:)
www.youtube.com/watch?v=G6yPQKt3mBA
Но, не вдаваясь в философию, здесь на практике приходится писать while(true)… yield. То есть, уже работая с фракталами по определению, постоянно натыкаться на «фрактальность», писать её и читать, замусоривая своё сознание, вместо того, чтобы держать в голове одну идею и не видеть её постоянно, растворённую до уровня философии.
Абстрагируя while… true, вы придёте к функциональному ЯП и да, к монадам. А статья классная, спасибо. Сама идея в правильных местах взлетит обязательно.
Я бы не стал делать ставку на генераторы. Всё же это довольно медленные штуки.
Насколько генераторы и асинки медленнее синхронного кода:https://t.co/YSELe37bGX
— Jin (@_jin_nin_) August 3, 2020
Помимо того, что на каждый вызов функции приходится совершать кучу дополнительных действий, так ещё и JIT компилятору крайне сложно сгенерировать эффективный код. pic.twitter.com/X3aN6fI8hY
К тому же я не заметил в статье упоминания каких-либо механизмов кеширования вычислений и автоматической ревалидации. Без этого будет вычисление всего мира на каждый чих, что приводит к диким тормозам уже не среднего размера приложениях.
Далее, я бы рекомендовал полностью инкапсулировать лоадеры. Как в $mol, где лоадеры вообще не пишешь, но они формируются автоматически для тех частей приложения, которые не могут сейчас показаться из-за ожидания чего-либо. Вообще, у нас там тоже фрактальная структура компонент, только на полностью синхронных функциях и suspense-api — это даёт простоту написания кода и эффективность его исполнения.
К тому же я не заметил в статье упоминания каких-либо механизмов кеширования вычислений
конечно же оно есть)
как только новая проекция сгенерирована, фрактал переходит в режим ожидания обновлений
вот тут имелось ввиду, что данные кешируются, пока не появятся изменения в зависимостях, принимавший участие в калькуляции. Да, не совсем явно получилось. Также стоит добавить, что после генерации новые данные поверхностно сравниваются с предыдущими и, если они эквивалентны, то они отбрасываются
Далее, я бы рекомендовал полностью инкапсулировать лоадеры.
я специально по максимуму вырезал всё кроме ядра, дабы показать показать нутро как есть, не скрывая ничего за абстракциями
.
Я очень далек от фронтенда и просто мимопроходил, но вот эта библиотека: https://crank.js.org/guides/getting-started очень напоминает ваши фракталы :)
Фрактальная шизофрения