Фрактальная шизофрения


    Нет, я не болен. По крайней мере так говорит голос в моей голове. Я наркоман. Вот уже более 15 лет я сижу на игле. Употребляю много, жёстко, до обморочного состояния. Докатился до того, что в последнее время не стесняюсь ни друзей, ни жены, ни детей… Двоих детей! Не люблю бадяженый, люблю чистый, без примесей. За годы перепробовал многое, но в последнее время остановился в поисках. Забавно осознавать, что от одного и того же получаешь одновременно и боль, и радость. Мне бы в лечебку, я даже хочу, я даже знаю в какую. Знаете такие, где продолжаешь употреблять, но под присмотром?


    Я жёстко сижу на typescript. Именно программирование мой наркотик — его я люблю и ненавижу. Оно никогда не было моей основной работой, это моё хобби, и только в этом амплуа я от него кайфую. Тем не менее, писать код из необходимости подзаработать порой приходится, и это боль, выгорание ещё на старте, и ощущение себя в теле бурлака на Волге. Я фанатик-перфекционист, тратящий кучу времени на поиск идеального решения, затирающий код до дыр, стремящийся всё переделать под себя, из-за чего в команде чаще всего токсичен для окружающих. Я мечтатель, движимый желанием сделать мир лучше, привнести в него что-то полезное, не славы ради, но чтоб себя найти и душу успокоить...


    Помню тот день, когда узнал о новой es-фишке — генераторах и итераторах. Я только пересел с ruby на javascript, втянулся в него, как в голове что-то щёлкнуло — а как же yield? Восторгу не было предела! Он есть, да ещё и работает по другому — это что ж получается: можно код останавливать? можно возобновлять? а ещё и параметры по ходу выполнения вводить/выводить? Oh My God!


    Теперь я пихал их везде. Извращался над ними, как мог, чувствовал в них что-то магическое, не понимал, что именно, но банальная генерация бесконечных коллекций казалась лишь вершиной айсберга их функционала. Посему упорно раз за разом я пытался вывернуть их наизнанку, ставил опыты, как, например, yield первого подать на ввод второго и обратно yield второго подать на ввод первого, зациклить, да ещё и какой-то КПД с этого получить. В общем изголялся настолько, насколько фантазии хватало. В какой-то момент идеи закончились, все подопытные были замучены насмерть, новый гибрид вывести не удалось...


    Шёл третий год моей удаленной работы фуллтайм в роли фронтенд-разработчика. Основа стека — react + mobx. Проект растёт как на дрожжах, решения по добавлению/удалению функционала принимаются на лету, дизайны на лету, согласования на лету, сроки горят, что неделю делали сегодня откатили, время потеряно, времени нет, деньги инвестора кончаются, проект замораживается… К этому моменту я изрядно вымотан, выжат и даже немного рад, что появилась такая бессрочная пауза на отдых от боли и удовольствие от хобби — как ни крути, но ценного опыта за плечами поднакопилось, как и идей, вызывающих зуд в черепной коробке.


    Взгляды на фронтенд-разработку довольно сильно разнились с текущими трендами. Принципы MV* казались, не то что притянутыми — вытянутыми за уши с сервера в браузер. Чёрт возьми — да мы до сих пор с третьей буквой * определиться не можем! А стейт? — Мы слепо верим, что скелет M должен висеть в шкафу, а плоть V лежать на полке и чем дальше друг от друга тем лучше, при этом оба должны обмениваться приветами и хлопать в ладоши, потому что нам лучше знать, как вам лучше жить!


    С этими мыслями я вернулся в лабораторию генераторов, а заодно перешёл на typescript. Человек я разносторонний, попав в википедию, переходами по ссылкам могу погружаться в глубь стека до заветного Maximum call stack size exceeded с последующей потерей контекста и вопросом типа — а чё меня сюда вообще понесло? Собственно эта разностороность сначала добавила в список моих интересов астрономию, квантовую механику и теорию эфира, а затем труды Бенуа Мандельброта и, соответственно, фракталы.


    Я помешался. Сознание будто трансформировалось. Отныне во всём я искал и находил фрактальную составляющую, в программном коде, в грамматиках парсеров. Html-код — это же фрактал, разве нет? Почему бы не сделать всё приложение фракталом? А результат? Мы разрабатываем программу, для того чтобы получить от нее ожидаемый результат, а то и серию результатов — последовательность кадров отрисовки на экране, сообщений в логе консоли — а ведь это по сути контрольные точки во времени её жизни — поток отражений её состояния в ходе выполнения, дающий возможность представить происходящее внутри...


    В конце концов дошло до того, что я переехал в отдельную комнату, кодил ночами и в итоге потерял сон. Лекарство было найдено довольно быстро — я стал включать на телевизоре длинные ролики-погружения во фракталы и смотреть их до отключки. Эффект, как от транквилизаторов. Вот этот пожалуй мой любимый.


    Безумные идеи а-ля "наш мир один большой фрактал" то и дело теребили разум и не только мой — я задолбал этими разговорами жену и друзей, дети, к их счастью, не попадали под эту раздачу отборного бреда. Однажды я заявил супруге: "Заведу себе пса, назову — Фрактал" — долго смеялись — "Фрактал! Ко мне!". Звучит! Не правда ли?)


    Как-то февральским вечером я стоял на балконе и втыкал в звёзды. Небо на удивление было необычайно чистым. Сириус, самая яркая, всегда в это время висящая над крышей дома напротив, казалось, была близка, как никогда. А что если рвануть навстречу звезде? И, чисто гипотетически — что если, во время приближения, с определенного расстояния мы обнаружим, что это не звезда, а целое созвездие или система звёзд? Ведь с Сириусом так и было — до середины 19 века считалось, что это самостоятельная звезда, пока в 1844 немецкий астроном Фридрих Бессель не предположил, а в 1862 американский астроном Альван Кларк не подтвердил, что Сириус это система из двух звёзд, вращающихся вокруг общего центра масс. Впоследствии они получили названия Сириус А и Сириус В. Две близко расположенных звезды, свет которых ввиду огромного расстояния воспринимается нами, как излучаемый одним источником. А что если выбрать одну из этих звёзд и лететь уже навстречу ей, а с какого-то расстояния опять обнаружить созвездие? А что если так будет повторяться бесконечно? Мы бесконечно будем видеть поток света, проецируемый на сетчатку нашего глаза, но при всём желании не сможем достичь его источника, а именно так и получается при постоянном погружении во множество Мандельброта, Жулиа… Мы постоянно видим… Поток? Проекций? Фрактала?


    Поток проекций фрактала! Вот оно! Мгновение, когда решение всплывает перед глазами, осознается и в миг разбивается на осколки где-то в подкорке сознания. На момент истина предстаёт как есть, во всей красе и тут же скрывается за поворотом. Ты пытаешься схватить её за руку, но все усилия тщетны. Всё что тебе остается — собирать разбитый паззл её образа, стараясь не растерять детали.


    Должен признаться, что на сбор этого паззла ушло несколько месяцев, 3 репозитория, ~800 git-веток и тысячи строк экспериментального кода. Теперь мне не давал покоя yield*. Да-да, со звёздочкой. Вы часто используете return внутри генератора? С вашего позволения я разбавлю этот эпос небольшим куском кода, в котором, используя генераторы, мы опишем формирование проекции Сириуса в созвездии Большого Пса.


    async function* SiriusA() {
        return '*A' // Проекция звезды Сириус А
    }
    
    async function* SiriusB() {
        return '*B' // Проекция звезды Сириус В
    }
    
    async function* Sirius() {
        // ['*A', '*B'] Проекция звёздной системы Сириус
        return [yield* SiriusA(), yield* SiriusB()]
    }
    
    async function* CanisMajor() {
        const sirius = yield* Sirius() // ['*A', '*B']
        // а где-то тут создается проекция созвездия Большого Пса
    }

    И так до бесконечности можно собирать всё более и более сложные структуры-проекции. Вы скажете: "Что тут особенного? Все тоже самое можно описать обычными функциями" — и будете правы, но одна особенность тут всё же есть. Вся фишка в yield*. Дело в том, что на пути следования к return-значению данное выражение попутно будет "выкидывать наверх" встречающиеся yield-значения, что можно задействовать в служебных целях — скрытно, за кулисами, определить контекст выполнения, режим работы и прочие внутренние системные параметры. Не буду утомлять вас подробностями реализации — на самом деле, как сказал Йозеф Геббельс: "Всё гениальное просто", а все мои танцы с бубном в итоге вылились в ~300 строк элементарного кода. Нет я не гений, это самокритика, из-за которой какое-то время я чувствовал себя идиотом, ведь как оказалось решение лежало на поверхности, нужно было лишь взглянуть под нужным углом. Видать угол долго искал.


    Всё есть фрактал


    Именно так звучит моя идея, попытка переосмыслить существующие паттерны проектирования приложений. Способ организации кода, в котором каждый элемент огромного приложения, как и само приложение — простая самоподобная сущность. Этой идеей я хочу поделиться с вами, подкрепить её рабочим прототипом, привести примеры и в самом идеальном раскладе — найти единомышленников.


    Суть идеи заключается в том, чтобы делить приложение не горизонтально на модели, виды, контроллеры и т.д., а вглубь на фрактально-древовидную структуру, где каждый узел является самостоятельным законченным приложением. Результат работы каждого такого приложения — это поток информации, отражающей его внутреннее состояние.



    @fract/core — это реализация — небольшая библиотека, предоставляющая два простых строительных блока: fractal и fraction. С их помощью можно описать сколь угодно сложную я надеюсь структуру, которая будет являться фрактальным приложением. Последнее можно запустить в браузере или на сервере, а можно упаковать в библиотеку, поделиться ею в npm и подключить через yield*.


    Простой фрактальный Hello world может выглядеть, например, так


    import { fractal, fraction } from '@fract/core'
    
    const Title = fraction('Hello world')
    
    const HelloWorld = fractal(async function* () {
        while (true) yield `App: ${yield* Title}`
    })

    Всё, что нужно для создания фрактала — это асинхронный генератор, определяющий порядок работы. Фракция — это тоже фрактал, которому из вне можно указывать какие данные использовать в качестве своей проекции, для этого она имеет единственный метод .use(data).


    Жизненный цикл


    Внутри генератора ключевое слово yield определяет, а выражение yield* извлекает текущую проекцию фрактала, другими словами — если представить всё в виде трубопровода, то yield* — это тянуть снизу, а yield толкать наверх. Этакий pull & push.



    Ни каких хуков или методов жизненного цикла не существует, а то как фрактал обращается со своим генератором можно описать в трёх шагах:


    1. с помощью генератора создается новый итератор
    2. итератор выполняется до ближайшего yield или return — полученное значение будет принято за текущую проекцию, во время этой работы все вызовы yield* автоматически устанавливают наблюдаемые зависимости; как только новая проекция сгенерирована, фрактал переходит в режим ожидания обновлений, а вышестоящий фрактал узнаёт об изменениях
    3. узнав об изменениях в нижестоящих фракталах, список зависимостей очищается и, если на предыдущем шаге проекция была получена с помощью yield, фрактал продолжает свою работу со второго шага; если же был return работа продолжается с первого шага

    Таким образом return делает то же самое, что и yield — определяет текущую проекцию, но при этом происходит "перезагрузка" и всё начинается с начала.


    Проекции и потоки


    Вся работа фрактала заключается в генерации проекций, отражающих его внутреннее состояние. Проекции упаковываются в так называемые фреймы, которые бывают двух видов.


    Обычный фрейм соответствует следующему интерфейсу и содержит в себе текущую проекцию фрактала


    interface Frame<T> {
        data: T;
    }

    Живой фрейм расширяет интерфейс обычного и помимо текущей проекции, содержит промис следующего фрейма. Данный промис зарезолвится, как только произойдут какие-либо изменения во фракталах, принимавших участие в генерации текущей проекции.


    interface LiveFrame<T> extends Frame<T> {
        next: Promise<LiveFrame<T>>;
    }

    Живой фрейм — это и есть тот самый поток проекций фрактала, иными словами: последовательность снимков его состояния.


    Методы запуска


    Для запуска фрактального приложения существует два простых метода exec и live. Оба принимают на вход фрактал или асинхронный генератор (такой же как при создании фрактала) и возвращают промис, который зарезолвится фреймом.


    import { exec, live } from '@fract/core'

    exec<T>(target: Fractal<T> | AsyncGenerator<T>): Promise<Frame<T>>


    Простой запуск, позволяющий получить обычный фрейм с текущей проекцией.


    const frame = await exec(HelloWorld)
    
    frame.data // 'App: Hello world'

    live<T>(target: Fractal<T> | AsyncGenerator<T>): Promise<LiveFrame<T>>


    Живой запуск, позволяющий получить последовательность живых фреймов.


    const frame = await live(HelloWorld)
    
    frame.data // 'App: Hello world'
    
    Title.use('Fractal Demo')
    
    const nextFrame = await frame.next
    
    nextFrame.data // 'App: Fractal Demo'

    Фрактальное приложение


    Опишем фрактал некоторого пользователя, имеющего имя, возраст и банковскую карту


    const Name = fraction('John')
    const Age = fraction(33)
    const Balance = fraction(100)
    
    const Card = fractal(async function* () {
        while (true) {
            yield {
                balance: yield* Balance,
            }
        }
    })
    
    const User = fractal(async function* () {
        while (true) {
            yield {
                name: yield* Name,
                age: yield* Age,
                card: yield* Card,
            }
        }
    })

    Каждый описанный выше фрактал является полноценным приложением и может входить в состав более сложных фракталов


    const frame = await exec(Balance)
    frame.data //> 100

    const frame = await exec(Card)
    frame.data //> {balance: 100}

    const frame = await exec(User)
    frame.data
    /*
    > {
        name: 'John',
        age: 33,
        wallet: {
            balance: 100
        }
    }
    */

    С помощью exec получаются разовые снимки текущего состояния, а с помощью live — живые


    const frame = await live(User)
    
    console.log(frame.data)
    /*
    > {
        name: 'John',
        age: 33,
        card: {
            balance: 100
        }
    }
    */
    
    Name.use('Barry')
    Balance.use(200)
    
    const nextFrame = await frame.next
    
    console.log(nextFrame.data)
    /*
    > {
        name: 'Barry',
        age: 33,
        card: {
            balance: 200
        }
    }
    */

    Чтобы получать живые проекции и не заниматься перебором цепочки фреймов, можно написать фрактал, который будет передавать проекцию от User потребителю (выводить в консоль например), а в качестве своей отдавать undefined (выше всё равно уже никого нет)


    const App = fractal(async function* () {
        while (true) {
            console.log(yield* User)
            yield
        }
    })
    
    live(App) // запускаем приложение

    Реактивность


    Система реактивности фрактала сама по себе — фрактал, по своей структуре напоминающий фрактал Кантора



    Построена она на обещаниях и их гонках, да да — то самое ненавистное порой состояние гонки тут работает на нас.


    В каждом живом фрейме промис следующего фрейма является по сути, промисом актуальности текущей проекции, пока он не зарезолвился — проекция жива. Список зависимостей, собираемый на втором шаге жизненного цикла, — это массив таких промисов Promise<LiveFrame<T>>[], именуемый racers, когда он составлен, создается гонка Promise.race(racers) — эта гонка тоже промис — racer текущей проекции, и в вышестоящем фрактале он опять попадает в массив racers — петля замыкается. Визуально это можно выразить так



    То же самое в коде будет выглядеть следующим образом



    Promise.race([
        // level 1 
        Promise.race([/* ... */]),
        Promise.race([/* ... */]),
        Promise.race([
            // level 2
            Promise.race([/* ... */]),
            Promise.race([/* ... */]),
            Promise.race([/* ... */]),
            Promise.race([/* ... */]),
            Promise.race([/* ... */]), 
            Promise.race([ 
                // level 3
                Promise.race([/* ... */]),
                Promise.race([/* ... */]) 
            ])
        ])
    ])

    Как только резолвится одна из гонок — весь "гоночный фрактал" начинает резолвиться в направлении корня, во время этого каждый фрактальный уровнень производит пересборку своей проекции. Таким образом обновление "фрактального дерева" идет от листьев к корню, как бы поднимаясь из глубины. Это дает одно мощное преимущество, которое хорошо видно на следующем примере


    const Name = fraction('John')
    
    const User = fractal(async function* () {
        while (true) {
            yield `User ${yield* Name}`
        }
    })
    
    const Title = fraction('Hello')
    
    const Post = fractal(async function* () {
        while (true) {
            delay(5000) // что-то долго делаем
            yield `Post ${yield* Title}`
        }
    })
    
    const App = fractal(async function* () {
        while (true) {
            console.log(`App | ${yield* User} | ${yield* Post}`)
            yield
        }
    })
    
    live(App)
    
    //> 'App | User John | Post Hello'
    
    Name.use('Barry')
    Title.use('Bye')
    
    //> 'App | User Barry | Post Hello'
    // через 5 секунд
    //> 'App | User Barry | Post Bye'

    Здесь мы одновременно внесли изменения во фракции Name и Title, после чего фракталы User и Post начинают обновлять свои проекции, User сделает это первым, затем App обновится не дожидаясь обновления Post — на самом деле App вообще не знает, что Post сейчас обновляется. App обновится ещё раз после того, как Post завершит работу над своей новой проекцией. Ключевой момент тут в том, что один медленный фрактал не "вешает" работу всего приложения.


    Временные проекции


    Довольно простой в употреблении и очень полезный механизм. Он позволяет организовать фоновое выполнение работы в то время, как вышестоящий фрактал довольствуется временным результатом. Создаются временные проекции с помощью функции tmp(data), а отдаются как обычные с помощью yield.


    Один из вариантов использования — организация "лоадеров". Работая во фронтенде, я всегда люто ненавидел лепить эти крутилки-вертелки, хотя понимал их необходимость.


    import { fractal, tmp } from '@fract/core'
    
    const User = fractal(async function* () {
        yield tmp('Loading...')
    
        delay(5000) // что-то долго делаем
    
        while (true) {
            yield `User John`
        }
    })
    
    const App = fractal(async function* () {
        while (true) {
            console.log(yield* User)
            yield
        }
    })
    
    live(App)
    
    //> 'Loading...'
    // через 5 секунд
    //> 'User John'

    Здесь фрактал User является "медленным", прежде чем отдать свою проекцию ему надо сходить на сервер, по пути зайти в магазин и т.д. А кто-то сверху в это время ждёт его проекцию. Так вот, чтобы не заставлять себя ждать User отдаёт временную проекцию 'Loading...' и продолжает генерировать основную, которую отдаст по мере готовности, т.е. код генератора после yield tmp(...) продолжает выполняться, но уже в фоне.


    Это ещё не всё — вот так, например, можно сделать фрактал-таймер


    import { fractal, tmp } from '@fract/core'
    
    const Timer = fractal(async function* () {
        let i = 0
    
        while (true) {
            yield tmp(i++)
            await new Promise((r) => setTimeout(r, 1000))
        }
    })
    
    const App = fractal(async function* () {
        while (true) {
            console.log(yield* Timer)
            yield
        }
    })
    
    live(App)
    
    //> 0
    //> 1
    //> 2
    //> ...

    Здесь фрактал Timer отдаёт текущее значение переменной i в качестве своей временной проекции и продолжает вычисление следующей, в процессе чего инкрементит i, дожидается окончания задержки в 1 секунду и цикл повторяется. Кстати говоря фракция именно так и устроена — она отдаёт временную проекцию с текущим значением, и ждёт завершения промиса, который зарезолвится новым значением переданным в метод .use(data), после чего цикл повторится.


    Делегирование


    Еще один полезный механизм, благодаря которому фрактал может поручить работу над своей проекцией другому фракталу. Всё, что для этого нужно — это вернуть исполнителя в качестве своей проекции.


    Допустим у нас есть фабрика newEditor, которая создает фрактал, отвечающий за редактирование профиля пользователя. Также у нас есть фрактал Manager, который в зависимости от фракции ProfileId переключает редактируемый профиль.



    function newEditor(id) {
        return fractal(async function* () {
            const { name } = await loadUserInfo(id)
            const Name = fraction(name)
    
            while (true) {
                // где-то в глубине этого фрактала генерируется
                // интерфейс редактирования имени пользователя
                yield <input 
                    placeholder="Input name" 
                    value={yield* Name} 
                    onChange={(e) => Name.use(e.target.value)} 
                />
            }
        })
    }
    
    const ProfileId = fraction(1)
    
    const Manager = fractal(async function* () {
        while (true) {
            const id = yield* ProfileId
            const Editor = newEditor(id)
            yield Editor // <-- делегируем работу фракталу Editor
        }
    })
    
    const App = fractal(async function* () {
        while (true) {
            yield yield* Manager
        }
    })

    Фрактальное дерево будет производить пересборку проекций изнутри-наружу каждый раз, когда где-то в его глубине при редактировании будут происходить изменения, в данном примере во фракции Name. Пересборка неизбежно будет перезапускать циклы while(true) на всех уровнях до самого корня App, за исключением фрактала Manager. Последний делегирует работу над своей проекцией фракталу Editor, и как бы выталкивается из цепочки регенерации.



    Повлиять на Manager может только фракция ProfileId. Как только она изменится Manager запустит цикл пересборки, в котором создаст новый фрактал Editor и снова делегирует ему дальнейшую работу.


    Без механизма делегирования нам приходилось бы вручную определять, что изменилось — фракция ProfileId или что-то другое в глубине фрактала, ведь нам не нужно создавать новый Editor, если id редактируемого профиля не изменился. Подобный код выглядел бы довольно многословно и не особо красиво на мой взгляд.


    const ProfileId = fraction(1)
    
    const Manager = fractal(async function* () {
        let lastProfileId
        let Editor
    
        while (true) {
            const id = yield* ProfileId
    
            if (id !== lastProfileId) {
                lastProfileId = id
                Editor = newEditor(id)
            }
    
            yield yield* Editor
        }
    })

    В следующем примере вы можете увидеть, что произойдет если фракции в качестве новой проекции передать фрактал.


    const BarryName = fractal(async function* () {
        while (true) yield 'Barry'
    })
    
    const Name = fraction('John')
    
    const App = fractal(async function* () {
        while (true) {
            console.log(yield* Name)
            yield
        }
    })
    
    live(App)
    
    //> 'John'
    Name.use(BarryName)
    //> 'Barry'

    Произойдет опять же — делегирование, поскольку фракция это обычный фрактал и внутри её генератора происходит yield BarryName.


    Факторы


    Генератор фрактала не принимает аргументов, на этот счет у меня есть некоторые соображения и даже реализация, но я решил отказаться от этого в первой версии. Взамен аргументов существует механизм факторов, который внутри фрактала позволяет определять условия для работы нижележащих фракталов. Это сравнимо со спуском параметров вниз по контексту.



    import { factor } from '@fract/core'
    
    const API_VERSION = factor('v2') // 'v2' | 'v3'
    // необязательное значение ^^^^ по умолчанию
    
    /* далее код из тела генератора */
    
    yield* API_VERSION('v3')    // устанавливаем значение фактора
    yield* API_VERSION          // 'v3' - получаем
    yield* API_VERSION.is('v3') // boolean - сравниваем
    
    // установка без аргументов эквивалентна
    // сброcу до значения по умолчанию
    yield* API_VERSION()
    yield* API_VERSION          // 'v2' 

    Через факторы можно передавать абсолютно любые данные, никаких ограничений нет. Рассмотрим пример, в котором мы плавно мигрируем со старого api на новое и с помощью фактора API_VERSION определяем фракталам какую версию api использовать в своей работе.


    const Page = fractal(async function* () {
        const apiVersion = yield* API_VERSION
    
        while (true) {
            yield `Work on api "${apiVersion}"`
        }
    })
    
    const Modern = fractal(async function* () {
        yield* API_VERSION('v3')
        // всем нижележащим фракталам изпользовать api v3
    
        while (true) {
            yield yield* Page
        }
    })
    
    const Legacy = fractal(async function* () {
        yield* API_VERSION('v2')
        // всем нижележащим фракталам изпользовать api v2
    
        while (true) {
            yield yield* Page
        }
    })
    
    const App = fractal(async function* () {
        while (true) {
            console.log(`
                Modern: ${yield* Modern}
                Legacy: ${yield* Legacy}
            `)
            yield
        }
    })
    
    live(App)
    
    /*
    > `
        Modern: Work on api "v3"
        Legacy: Work on api "v2"
    `
    */

    Скрытый текст

    Важно! Внутри генератора получение значения фактора происходит именно того, который определён на верхних уровнях, а то значение, которое мы определяем на своём уровне будет доступно только на нижележащих



    const Top = fractal(async function* () {
        yield* API_VERSION('v3')
    
        while (true) {
            yield yield* Middle
        }
    })
    
    const Middle = fractal(async function* () {
        yield* API_VERSION       // 'v3' - определено во фрактале Top
        yield* API_VERSION('v2') // переопределяем, но для нижних уровней
        yield* API_VERSION       // на своем уровне у нас остается 'v3'
    
        while (true) {
            yield yield* Bottom
        }
    })
    
    const Bottom = fractal(async function* () {
        yield* API_VERSION       // 'v2' - переопределено в Middle
    
        while (true) {
            yield /*...*/
        }
    })

    И кость и плоть


    Один из вариантов использования факторов — это указание режима работы, в зависимости от которого фрактал генерирует проекцию определенного типа. Допустим нам нужно построить приложение, которое умеет выводить информацию на экран, параллельно сохранять свой стейт в локальное хранилище, а при обновлении страницы восстанавливаться из последнего сохраненного состояния.



    const APP_STORE = 'APP'
    
    function newApp({ name = 'Hello world' } /* AppState {name: string} */) {
        const Name = fraction(name)
    
        return fractal(async function* App() {
            while (true) {
                switch (yield* MODE) {
                    case 'asString':
                        yield `App ${yield* Name}`
                        continue
                    case 'asData':
                        yield { name: yield* Name } // as AppState {name: string}
                        continue
                }
            }
        })
    }
    
    const Dispatcher = fractal(async function* () {
        // берем сохраненное состояние из локального хранилища
        const data = JSON.parse(localStorage.getItem(APP_STORE) || '{}')
    
        // создаем фрактал нашего приложения
        const App = newApp(data)
    
        // создаем фрактал с предопределенным режимом работы 'asString'
        const AsString = fractal(async function* () {
            yield* MODE('asString')
            while (true) yield yield* App
        })
    
        // создаем фрактал с предопределенным режимом работы 'asData'
        const AsData = fractal(async function* () {
            yield* MODE('asData')
            while (true) yield yield* App
        })
    
        while (true) {
            const asString = yield* AsString // это мы выведем на экран
            const asData = yield* AsData     // а это сохраним в хранилище
            // выводим
            console.log(asString)
            // сохранияем
            localStorage.setItem(APP_STORE, JSON.stringify(asData))
            yield
        }
    })

    Что тут происходит: один и тот же фрактал App по разному генерирует свои проекции в зависимости от фактора MODE, зная это мы подключаем его к фракталам AsString и AsData, которые в свою очередь подключаем к Dispatcher. В результате мы получаем две разных проекции, принадлежащих одному и тому же фракталу — одна в текстовом виде, вторая в виде данных.



    Именно этот вариант использования факторов позволяет нам не отделять плоть от костей в масштабе всего приложения (не делить на модели и виды). Фрактал — это более абстрактная сущность, для которой и модель, и вид — всего лишь проекции, слепки текущего внутреннего состояния фрактала. Эти слепки мы можем получать одновременно и использовать по разному — один для вывода на экран, второй для сохранения. Их может быть сколько угодно, так же просто можно реализовать генерирование слепка, предоставляющего централизованное api управления фракталом и т.д.


    MV* MVVM MVP


    Вот мы и подошли к сравнению фрактального подхода в архитектуре приложений с традиционными, привычными нам MV* MVVM MVP и т.д. Последние я попытался изобразить в левой части. Не претендую на точность и правильность, чисто для освежения вашей памяти, честно говоря до сих пор путаюсь какая стрелка куда должна идти



    В правой части — схема фрактальной архитектуры, на которой вид и модель изображены как проекции фрактала, а синие стрелки — экшены взаимодействия с его внутренним состоянием.


    Фрактальный модуль


    Это обычный javascript модуль, который экспортирует непосредственно сам фрактал и факторы, необходимые для его работы.


    import { fractal, factor } from '@fract/core'
    
    // app.js
    export const API_URI = factor()
    export const THEME = factor('light')
    
    export const App = fractal(async function* () {
        const apiUri = yield* API_URI
        const theme = yield* THEME
    
        if (!apiUri) {
            // обязательный фактор
            throw new Error('Factor API_URI is not defined')
        }
    
        while (true) {
            /*...*/
        }
    })

    Разбивая приложение на такие модули мы получаем однообразную на всех уровнях архитектуру, которую просто обслуживать и поддерживать.


    Асинхронность и code splitting


    Под капотом вся работа фрактала происходит асинхронно, тем не менее внутри генератора её довольно просто контролировать с помощью привычного await. Фракталы, находящиеся в разных ветвях "фрактального дерева" работают не зависимо друг от друга и теоретически могут делать свою работу одновременно. Теоретически — потому что всем нам извесно, что js однопоточен, а асинхронные операции всё равно происходят в порядке очереди. Сюда бы воркеры задействовать, но это уже отдельная тема для размышлений...


    Говоря о разделении кода на мелкие куски, стоит наверное сделать небольшое отступление и поговорить о правильной загрузке приложения. Идеальную, в моём понимании, загрузку я попытался изобразить на следующей картинке



    Представьте, что у нас на сервере, как на столе, лежит граф зависимостей (модулей). При этом момент загрузки приложения сравним с тем, что мы с вами подошли и начали тянуть его вверх за точку входа в приложение, отрывая узлы от стола, что поднялось — загрузилось, что осталось лежать — пускай лежит, пока не потребуется.


    Добиться этого можно импортируя зависимости по мере надобности прямо в теле генератора, что позволяет сборщикам, типа webpack, безболезненно разбивать код на мелкие части, загрузка которых будет происходить исключительно по требованию


    // ./user.js
    export const User = fractal(async function* () {
        while (true) yield `User John`
    })
    
    // ./app.js
    export const App = fractal(async function* () {
        // импортируем зависимость, когда она нам действительно нужна
        const { User } = await import('./user')
    
        while (true) yield `User ${yield* User}`
    })

    Никакой скрытой магии, специальных загрузчиков и прочего, всё решается нативными средствами, с сохранением IntelliSense в редакторе.


    Смотри вглубь


    Когда разработка первой версии была завершена, настало время писать демки, первым делом я конечно же хотел реализовать знакомый всем пример с TodoMVC. Я привычным образом создал репозиторий, настроил сборку, подтянул зависимости и… впал в ступор — я только, что создал инструмент, в котором знаю каждую шестеренку, но не умею им пользоваться. Да-да — я споткнулся на пороге у входа в собственный дом. Возился пол дня, но ничего годного так и не получилось — по инерции я пытался создать стор и натянуть виды на модели. В какой-то момент меня стала охватывать паника, появилось ощущение, что всё это время я был в забвении, кодил свои мечты, которые вмиг разбились о простейшее практическое задание. Я не мог в это поверить...


    Просто я опять смотрел не под тем углом. После небольшого перерыва, я понял, что все паттерны проектирования, которыми я владею, тут никак не вписываются, и попытки их применить только усугубляют ситуацию, а главный принцип, помогающий правильно настроить сознание на фрактальную разработку — смотри вглубь. В прямом смысле этого слова, также как смотришь на макет страницы и видишь вложенность блоков, также и здесь нужно настроиться на распознавание фрактальной вложенности.


    После усвоения этого правила, демка была написана довольно быстро. В азарте от гордости за проделанную работу я написал ещё парочку, чтоб придти сюда не с пустыми руками. Все они написаны с использованием react и styled-components — это привычные для меня элементы стека разработки и надеюсь, что их присутствие не повлияет на ваше восприятие, поскольку использованы они исключительно в целях визуализации.


    • Todos — думаю этот пример не нуждается в представлении
    • Loadable — пример, показывающий работу временных проекций, в исходниках можно увидеть, как с помощью yield tmp(...) организуется показ лоадеров в то время, как в фоне производится загрузка, я специально добавил там небольшие задержки для того, чтоб немного замедлить процессы
    • Factors — работа в разных условиях. Один и тот же фрактал в зависимости от установленного в контексте фактора отдаёт три разные проекции, а также поддерживает их актуальность. Попробуйте поредактировать имя и возраст.
    • Antistress — просто игрушка, щёлкаем шарики, красим их в разные цвета и получаем прикольные картинки. По факту это фрактал, который показывает внутри себя кружок, либо три таких же фрактала вписанных в периметр круга. Клик — покрасить, долгий клик — раздавить, долгий клик в центре раздавленного кружка — возврат в исходное состояние. Если раздавить кружки до достаточно глубокого уровня, можно разглядеть треугольник Серпинского

    Планы на будущее


    • попробовать внедрить возможность использования синхронных генераторов для повышения производительности в тех местах приложения, где асинхронность не требуется
    • написать рендер jsx -> html заточенный именно под фрактальную структуру, react годится только для демок, ибо по факту вся его работа заключается только в том, чтобы вычислить diff и применить изменения, остальной код простаивает
    • а может даже рассмотреть вариант создания собственной фрактальной системы компонентов и их стилизации — амбициозно не правда ли? ещё амбициознее то, что браузер также может быть фракталом, как и другие приложения операционной системы, как и сама операционная система в целом
    • поэкпериментировать с grahpql, где-то на горизонте мне мерещится элегантное решение с организацией подписок на события сервера а-ля yield* gql'...'
    • связать свою жизнь с open source — это и есть та самая лечебка, о которой я говорил в самом начале
    • выучить английский :) Кстати, поскольку у меня с ним сейчас трудности — буду рад любой помощи по переводу и дополнению readme

    Попробовать


    По себе знаю, что вникать в чей-то велосипед от очередного noname-автора не особо интересно, поэтому, чтобы сгладить порог вхождения я старался разработать максимально функциональную и в то же время простую для понимания архитектуру с минимумом всяческих методов, хелперов и прочего, что требовало бы времени на чтение документации.


    Разрабатывать сейчас коммерческий продукт с нуля на фрактале я бы не рекомендовал, всё таки пока что это в первую очередь идея и концепт, требующий ухода и заботы для правильного роста во что-то зрелое, а вот попробовать где-то на кусочке своего react-приложения можно, для этого я создал простой компонент и оформил его ввиде библиотеки @fract/react-alive


    import { fractal } from '@fract/core'
    import { Alive } from '@fract/react-alive'
    
    const App = fractal(async function* () {
        while (true) {
            yield <div>Hello world</div>
        }
    })
    
    function Render() {
        return <Alive target={App} />
    }

    Насчёт библиотек — я говорил, что можно создать фрактальное приложение, упаковать его в библиотеку и подключить через yield*? В качестве примера я сделал библиотеку @fract/browser-pathname. Она экспортирует фрактал, проекцией которого является параметр window.location.pathname, и метод redirect(p: string) позволяющий его менять. Её исходники находятся тут, а то, как с её помощью можно организовать простейший роутер, можно увидеть в исходниках главной демо-страницы.


    Напоследок


    Фрактал — это не убийца всего и вся. Это новая мутация, которая либо будет отброшена на задворки эволюции, либо станет её новым витком и займет своё достойное место в этом мире. Повторюсь о том, что react использован в демках только, как средство визуализации, никакой физической связи у фрактала с ним нет, а заточка под фронтенд, указанная в планах на будущее — это одно из направлений развития фрактальной экосистемы.



    В заключение хотелось бы выразить благодарность всем читателям и надежду на то, что не зря потратил ваше время, а может даже сделал ваш день чуточку позитивнее. Буду рад вашим комментариям, конструктивной критике и помощи в развитии проекта.


    С уважением, Денис Ч.


    "Большая часть моих трудов — это муки рождения новой научной дисциплины" © Бенуа Мандельброт

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 63

      +9

      Ух, знатная норкомания. Мне по нраву :-)

        +2
        Мне тоже! Хоть я и «не настоящий сварщик».
        +3

        Всю статью не осилил, но дошёл до определений фрактальный и фракций. Кажется до этого момента стоит ответить на два вопроса. Чем это отличается от реактивного программирования? Чем фрактал отличается от «потока» в терминах SICP?

          0
          Возможно мой ответ не удовлетворит ваши ожидания — я неуч, аббревиатура SICP меня ошарашила, как студента на экзамене, и её смысл, честно говоря, я узнал только с вашей подачи, погуглив естественно :)
          Но тем не менее, начиная с последнего вопроса, постараюсь ответить так, как я это понимаю:
          Поток — это некоторая последовательность порций информации.
          Фрактал — это сущность, результатом работы которой является поток — последовательность порций информации, согласованной с его внутренним состоянием.
          Каждая новая порция информации появляется в потоке в результате изменений, произошедших внутри фрактала — это реактивность.
            +1

            Тогда вам стоит вот это почитать — http://conal.net/fran/tutorial.htm На практике реактивное программирование на потоках вылилось в штуки вроде reactive LINQ.

              0

              Спасибо за ссылку, я обязательно изучу этот материал.

            +2
            Ну вот для того, чтобы не изобретать велосипеды, человеку необходим широкий кругозор. Хотя с другой стороны, без велосиподостроителей мир был бы скучный)
              +4

              Я изобретаю велосипеды не от желания компенсировать узкость своего кругозора (а он на мой взгляд действительно узок). Я занимаюсь этим, потому что это часть моей жизни, мне без этого скучно, ну не могу я ни дня, чтоб чего нить не слепить :)

            0

            Я тоже остановился примерно там же, но вопрос сформулировал ещё проще: что это, если не "автор открыл для себя декомпозицию". Мы просто делим сложный объект на более простые. Не? Или я что-то не уловил?

              +1

              Да, делим, и дальше в статье описывается как именно и что с этого получается ;)

            +4

            А вот неплохо, радует, что опыт познания мира начинает проникать в программирование. Тоже потихоньку пишу статью со своим симбиозом, но в основе не "всплытие" данных по дереву (как если смотреть на эту фрактальную картину сбоку), а единое глобальное состояние с доступом в любую точку. В материальном мире мы работаем с неэффективными передатчиками — свет, волны — которые обладают ограниченной скоростью и не позволяют получать доступ к краю Вселенной мгновенно, но в программировании можно обойти это ограничение и каждый элемент связывать "порталом" с глобальным стейтом:


            @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

              –2

              Идея такая под грибами тоже проскакивает что все связано со всем. Но к сожалению фрактал первичен. Для изменения вам все таки нужно учитывать везде предельную скорость взаимодействия. А вот для связанных состояний да такого ограничения нет. Ваша идея могла бы наверно подойти для реализации сборщика мусора. А вот строить интерфейсы без структуры нельзя.

                0

                Разговор о том, что первично, тем более если вы сравниваете с концепцией "начало начал не имеет начала, безначальность бесконечна" (фрактал), был бы не особо уместен. В моем понимании это лишь одна из составляющих — движение есть следствие взаимодействия других сил, и для меня оно не первично.


                Не знаю, как вы сделали вывод, что я отрицаю структуру в интерфейсах — но это другой слой, который не относится к данным, и представлен в виде иерархичного DOM-дерева, создаваемого опять же иерархичной композицией реакт-компонентов. Прокидывать же в инпут данные через всех родителей, а сигналы от него собирать по всей цепочке — это и есть ограничение, заставляющее "учитывать везде предельную скорость взаимодействия", портальность снимает эту проблему. Но она присутствует в цепочках react props, когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться, а в предлагаемой "фрактальной" архитектуре, похоже, именно так.

                  0
                  когда данные, нужные одному лишь чайлду, заставляют всех родителей и соседние компоненты перерендериваться

                  Если не ошибаюсь, в реакте эта проблема решается контекстами и порталами. Во vue3 также вводят порталы, назвав их телепортом. Нужен ли этот концепт в отрыве от фреймворка — немного сомнительно.

                    0

                    В отрыве от фреймворка это называется "двухсторонний биндинг" (https://seemple.js.org/), только с глобальным состоянием, а не локальным.

                      0

                      Двусторонний биндинг бесполезен без реактивного фреймворка/либы. К сожалению, для реакта — это не react-way, а в других фреймворках/либах он есть из коробки. Разве нет?

                        +1

                        Вы, похоже, говорите о приложениях, в которых не используется js-хранилище с данными, а все они содержатся в атрибутах дом-дерева? И для операции с элементами вызываются getElementById, а затем в него записываются значения и дата-атрибуты? Да, подобная схема используется в крошечных простых приложениях, и все, о чем мы тут говорим, там не нужно.

                +3
                Да, не очень, конечно, говорить про свой подход, когда вы ждете комментариев про свою идею

                О нет нет! Своим комментарием вы прям всколыхнули волну воспоминаний о том, что я это всё прошёл. Не бахвальства ради, что впереди на шаг, но быть может мне удастся повернуть вас с этого пути в мою сторону, тем более — сами напросились :) Смотрите.

                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, они не обмениваются данными.
                  +1

                  Ну вот не знаю про "шаг впереди". В ваших примерах есть как раз "обмен данными между компонентами" в виде


                  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 }
                          }
                      })

                  Тоже много лет назад писал подобные системы, полные функциональщины и скрытых слоев, с привкусом Алохоморы при выполнении. Но потом дорос до понимания, что лучший код — максимально простой и понятный, так как работаю в корпоративных продуктах, где это — ключ к успеху. Бывает, приходят ребята с очень нестандартными подходами, но их код не понимают и приходится переписывать после их ухода. В общем, не вижу пользы от вашего подхода, кроме как для саморазвития)

                    +2
                    Ну вот не знаю про "шаг впереди".

                    Ни в коем случае не хотел вас как-то "задеть" ) не зашла шутка :)


                    В ваших примерах есть как раз "обмен данными между компонентами" в виде

                    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 данных. исходники


                    С уважением )

                      0

                      Спасибо за пояснение, действительно я сознательно сравниваю все паттерны, которые когда-либо использовал, с чем-то новым, чтобы понять выгоду. Для меня цели архитектуры — комфорт в разработке бизнес-функционала, минимизация возможных ошибок, простота масштабирования и возможность легкой интеграции сторонних решений. С этого угла зрения, к сожалению, полностью фрактальная архитектура не подходит.


                      А как идея — интересно. Если абстрагироваться от всплытия по 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-перехватчиков на разных уровнях не получится, все должно быть в этой парадигме. А это быстро приведет к устареванию подхода и через какое-то время к полному переписыванию на более гибкие рельсы и на новые технологии (во фронтенде значимые изменения часты).


                      Вот такой вот прагматичный подход, тоже не в обиду, но я так вижу)

                        0

                        тем не менее — время покажет)
                        P.S. спасибо за effector

                +5

                Советую порыть в сторону функциональных ЯП и ленивых вычислений. Не силен в функциональщине, но мне кажется Ваши фракталы имеет много общего с монадами. Может подчерпнете интересных идей.

                  +1

                  Тема про return в асинк-генераторах не раскрыта. И ее связь с async-await, кстати.

                    0

                    В быту чаще всего мы используем генераторы для перебора некоторой последовательности значений, последовательность генерируется на лету, значения перебираются как правило в цикле либо 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, кстати.

                    Вот эта часть вашего вопроса мне не совсем понятна, поясните пожалуйста, что вы имели в виду.

                      +1

                      Ну просто у меня есть догадка (возможно, неверная), что когда хочется написать const res = yield something(), то обычно это на самом деле желание сделать res = await something() и иметь something() как async function. Yield предназначен для генерации последовательностей, его использование для чего-то еще — пережитки старины глубокой, когда люди еще не догадались до async-await.

                    +5
                    Суть идеи заключается в том, чтобы делить приложение не горизонтально на модели, виды, контроллеры и т.д., а вглубь на фрактально-древовидную структуру, где каждый узел является самостоятельным законченным приложением. Результат работы каждого такого приложения — это поток информации, отражающей его внутреннее состояние.

                    Походу вы функциональное программирование изобрели :)

                      +2
                      Что только люди не придумают чтобы не было скучно писать e-shop.
                        +2
                        Совсем не про программирование, но вдруг в силу вашего упорства и фанатизма фракталами вам понравится эта программка: www.mandelbulb.com/2014/mandelbulb-3d-mb3d-fractal-rendering-software

                        В ней вот такие шедевры выполняют: www.youtube.com/watch?v=OFFrdW2_7Lo
                        Рекомендую просмотр либо в анаглиф очках, либо на смартфоне через youtube app + cardboard для просмотра в стереоскопическом режиме.

                        А вообще жаль что вы не художник, с вашим складом ума, и любви к ковырянием в деталях у вас бы могли получится отличные серии картин, на подобие того как Клод Моне Руанский собор рисовал в разных вариациях…
                          0

                          Спасибо! Да, на mandelbulb и ролики с её шедеврами я натыкался — действительно потрясающие виды, но попробовать самому поэкспериментировать с ней пока не доводилось. Тем не менее у меня это в заметках и я обязательно доберусь до этой программы.

                          0
                          Мда… Вот интересно, до каких пор «программисты» будут ацки извращаться, пока не поймут банальности? А ведь всё что есть — это отображение множества в другое множество, и ничего другого для программирования и не требуется вовсе. :)))
                            +1

                            Боюсь, помимо отображения вам еще понадобится само множество

                              0
                              Ну в идеале вообще-то как минимум два. И два множества упомянуты выше.
                                0
                                А ведь всё что есть — это отображение множества в другое множество

                                По моему тут всё же речь об одном множестве, второе это результат преобразования первого. Да их два, но исходно — одно


                                Вот интересно, до каких пор «программисты» будут ацки извращаться, пока не поймут банальности?

                                Фрактал это и есть та самая банальность, абстрактное множество, главная задача которого, как раз преобразование в разные виды множеств


                                Вы по сути, сами не заметив, четко и емко сформулировали суть :) Спасибо

                                  0
                                  Отлично. Теперь развейте математическую мысль. У вас пара множеств [скорее всего]. Пусть А, и П. А — исходное, П — мн-во преобразования. И элементы. Пусть а1, а2, а3, а4. И элементы преобразования — п1, п2, п3. Что делать с этим? :)
                                    0

                                    Если под множеством вы подразумеваете именно коллекцию, то я пожалуй изначально не правильно расценил этот термин в данном контексте.


                                    Тем не менее позвольте всё таки продолжить мысль о преобразовании.


                                    Небольшое отступление — представьте театр теней, мы устанавливаем фонарик напротив стены, помещаем в поток света руку и видим на стене её тень, путем различных манипуляций мы получаем зайца, волка, птицу и т.д. Затем мы добавляем второй фонарик, свет которого создает тень от этой же руки на полу. Таким образом мы одновременно получаем две тени одной руки, да, они выглядят скорее всего по разному, но это не важно, важно то, что они полностью синхронизированы с рукой и незамедлительно реагируют на её движения.


                                    Так вот: если рука — это исходное "множество" некоторых данных, то тень — это новое "множество", полученное путём преобразования исходного, а именно проецированием трёхмерной сущности (руки) на плоскость в двухмерную сущность (тень) с учетом различных факторов, как то: угол падения света, его яркость и т.д.


                                    Рука — это и есть фрактал, а тень — это проекция фрактала, созданная с учетом заданных факторов. Помещая один и тот же фрактал в разные условия (добавляя фонарики), мы можем получать несколько "теней" одновременно — 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 и потом используются для восстановления приложения в исходное состояние. Первичное дерево я по дефолту добавил, чтоб не пусто было, справа на панелях можно поредактировать его состояние.
                                    Вот тут можно посмотреть "механизм сворачивания узла". Не самый чистый код, делал для себя на пробу, но основные моменты можно проследить.

                            +3
                            Классика никогда не устареет.
                            А если серьезно, это просто реактивное программирование, целиком завёрнутое в генераторы, из-за чего «вход рубль, а выход — два»:
                            1) Весь код async, из-за чего браузер будет устраивать стейт-машину по каждому вызову, JIT-компилятор не сможет ничего заинлайнить, и каждое вычисление «фрейма» будет неоправданно дорогим в сравнении с вычислением такой же структуры данных традиционными способами. См. redux-saga, у которого те же грабли. Заворачивание кода в async имеет весьма заметную цену, а заворачивание туда всего кода (а не только того, который реально должен быть async) — это слив производительности в трубу только потому, что программист такой весь из себя фиялка и ему так проще думать.
                            2) Глубина связей данных не может превышать глубину стека вызовов. Да, для современных браузеров это не то, чтоб прямо очень страшно, поскольку счёт идёт минимум на десятки тысяч. Но, например, в хроме некоторой степени лежалости глубина стека вызовов всего лишь в районе 20К. Наивная попытка запилить на этом объемный граф может легко обрушиться в maximum call stack size exceeded.
                            3) Что там с циклическими зависимостями?
                            4) А что с быстродействием? Как она будет шевелиться, если кто-нибудь будет динамически переставлять зависимости десятками тысяч в какой-нибудь сложной структуре?
                              +1

                              Архитектурный астронавт — да, пожалуй это в точности про меня)
                              Я не презентую готовое решение всех проблем, я презентую идею и концепт, показывающий, как проблемы могли бы решаться.
                              Да, async это заметное падение производительности и быстродействие не конёк текущей реализации. Для того чтобы этот код шустро отрабатывал ему возможно необходима своя собственная среда выполнения, но поскольку это уже из области фантастики — я естественно рассматриваю варианты оптимизации, как использование синхронных генераторов там, где асинхронность не нужна и т.д.
                              Циклические зависимости — отдельная тема, архитектурно я не представляю кейса, когда это действительно нужно и дабы пресекать возможность случайного создания таких ситуаций с последующим долгим дебагом, я обдумываю механизм их отслеживания.

                                +1
                                Классика никогда не устареет.


                                Я тоже вначале об этом подумал, но тут все-таки не совсем так – автор же и сам попробовал создавать приложения, и написал интеграцию с Реактом.

                                Меня тоже смущают генераторы, у меня сложилось ощущение, что они очень медленные. Но тут все-таки «мясо» идеи не в генераторах, а в том, как смотреть на потоки данных.

                                Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта. Разве что тут задается не только структура интерфейса, но и любых других данных
                                  0
                                  Генераторы не «медленные» как-то по особенному, они просто медленные настолько, насколько медленные вызовы async-кода там, где можно и без него. Что не особо заметно на простых случаях, но становится очень заметным, когда счёт вызовов идёт на миллионы (а если вся обработка ваших данных вся происходит таким образом и данные достаточно сложные — там счёт идет на миллионы).

                                  Но тут все-таки «мясо» идеи не в генераторах

                                  Нет. Но мясо идеи тут — это обычное pull-based FRP. А генераторы — да, конкретная (но очень проблемная для использования «в жизни») форма выражения.

                                  Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта.

                                  Ну, сложно говорить всерьез про достоинства «реактивности из реакта», потому что она не выделена в standalone-абстракцию. Рассуждать о преобразовании данных в терминах компонентов реакта конечно можно (более того, я видел людей, которые так делают), но… зачем? Вырожденные компоненты с рендером-заглушкой, зоопарк HOC, и вот это всё, когда тебе нужно просто без всякой визуализации данные погонять? MVC не просто так придумали, хотя да, очень много программистов до сих пор не понимает, что им делать с C и в каком виде его представлять (см. статью собссно). Да и насчет M тоже порой встречаются очень своеобразные толкования. Ладно хоть V не вызывает вопросов обычно, кроме того, что в V пытаются впихнуть весь MVC и жить с этим.
                                    +1
                                    Но тут все-таки «мясо» идеи не в генераторах, а в том, как смотреть на потоки данных.

                                    Именно так. Генераторы с их возможностью ввода/вывода параметров по ходу выполнения просто оказались идеальными кандидатами для организации локаничного решения.


                                    Я, честно говоря, все равно плохо понял, в чем преимущество перед реактивностью из Реакта.

                                    Ни в чем, я не ставлю фрактал в противовес реакту, реактивность последнего во фрактальных приложениях попросту не используется, используется только рендеринг 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, в то время как фрактал может отдавать что угодно и использоваться в составе чего угодно.


                                    Надеюсь, что пример получился удачным :)

                                      0
                                      Не правда ли эти два компонента даже визуально на уровне кода выглядят очень похожими?

                                      Да, я понимаю! Как раз в этом и был вопрос – я вижу, что это уж слишком похоже на Реакт.

                                      Может быть, есть примеры, как это использовать не для построения интерфейса? Если это новая абстракция, которая отличается от реактивных вложенных компонент Реакта, но в чем ее преимущества? Где она засияет, там где Реакт спотыкается? Или где другие абстракции не смогли понятным образом описать задачу?
                                  +3
                                  Это великолепно, люблю когда код это искусство!

                                  П.С. Кто сказал, что программисты психи? Вам просто этого не понять!
                                    0

                                    Почти переизобрели монады, неплохо.

                                    • UFO just landed and posted this here
                                        0
                                        TypeScript — это уже бодяженый. Пишите на ES7 сразу без транспилятора. Быстрее в стадию JIT компиляции попадет.
                                          0

                                          Под примесями я подразумевал html и css. Люблю просто писать код, а вот верстка, хоть и делаю хорошо и добросовестно, но это для меня то ещё мучение — минимум фантазии, просто стук по клавишам :)
                                          Упустил, не раскрыл этот момент в статье.
                                          Typescript потому, что без типов после одного большого проекта мне стало как-то тяжеловато.

                                          0

                                          Замечательная статья.
                                          Давным-давно смотрел на визуализацию Winamp'а и не понимал как это "видео" может занимать килобайты…
                                          Но когда узнал про соревнования 64 kB и меньше, когда "влезает" даже "вселенная"…
                                          https://habr.com/ru/company/mailru/blog/406969/

                                            +1
                                            У меня одного КДПВ вызывает чувство «зловещей долины» из-за имеющихся местами нерегулярностей?
                                              +1
                                              Прошёл по ссылке на видео в ютубе и пропал на 2 часа. )
                                                0

                                                я там несколько суток точно оставил )

                                                –3
                                                1. Фрактал — аналитическая функция. А приложение регулярно.
                                                2. Были попытки делать что-то с йелдами, но не взлетели: они слишком сложные для рядового разработчика.
                                                3. 80% всей работы по внедрению технологии: обучение и практика применения. Это самое сложное.
                                                4. Не знаю. На самом деле есть большой и стабильный HTML, который как бы baselin веб-разработки и все начинают учить с него. Если ваш подход взлетит, вы сможете привлечь людей, а послезавтра у него найдется фатальный недостаток, то люди останутся без работы и перед необходимостью учить все тот же HTML.
                                                  +4
                                                  спасибо за шизу
                                                    0

                                                    Пожалуйста ))

                                                    0
                                                    Существует такая вещь как continuation passing style. Есть соответствующая монада (способ писать линейный код), и комонада. На хабре была статья, и картинки в ней наталкивают на определённые мысли.

                                                    Я не буду делать вид, что до конца понял идею в статье, но издалека это всё здорово напоминает CPS с несколькими разными типами chaining'a продолжений.
                                                    Надеюсь, разбор чужих достижений (монады, комонады, их связь с async/await и (ко)монадой продолжения) поможет выразить это всё на более общепринятом языке. Именование переменных всё-таки важно. Я так же верю, что это сильно упростило бы код.

                                                    Вопросы производительности, кстати, тоже интересны, раз уж тут осторожно упоминается возможность использования сабжа в проектировании ОС:)
                                                      +1
                                                      Пока читал статью в голове мелькала эта прога
                                                      www.youtube.com/watch?v=G6yPQKt3mBA
                                                        +2
                                                        Вставило знатно. Итак, всё есть дерево, иерархия. Окей. Профитов от этого при правильном подходе и желании можно получить массу. Лишь бы мозги были расположены к деревьям.

                                                        Но, не вдаваясь в философию, здесь на практике приходится писать while(true)… yield. То есть, уже работая с фракталами по определению, постоянно натыкаться на «фрактальность», писать её и читать, замусоривая своё сознание, вместо того, чтобы держать в голове одну идею и не видеть её постоянно, растворённую до уровня философии.

                                                        Абстрагируя while… true, вы придёте к функциональному ЯП и да, к монадам. А статья классная, спасибо. Сама идея в правильных местах взлетит обязательно.
                                                          –3

                                                          Я бы не стал делать ставку на генераторы. Всё же это довольно медленные штуки.



                                                          К тому же я не заметил в статье упоминания каких-либо механизмов кеширования вычислений и автоматической ревалидации. Без этого будет вычисление всего мира на каждый чих, что приводит к диким тормозам уже не среднего размера приложениях.


                                                          Далее, я бы рекомендовал полностью инкапсулировать лоадеры. Как в $mol, где лоадеры вообще не пишешь, но они формируются автоматически для тех частей приложения, которые не могут сейчас показаться из-за ожидания чего-либо. Вообще, у нас там тоже фрактальная структура компонент, только на полностью синхронных функциях и suspense-api — это даёт простоту написания кода и эффективность его исполнения.

                                                            0
                                                            К тому же я не заметил в статье упоминания каких-либо механизмов кеширования вычислений

                                                            конечно же оно есть)


                                                            как только новая проекция сгенерирована, фрактал переходит в режим ожидания обновлений

                                                            вот тут имелось ввиду, что данные кешируются, пока не появятся изменения в зависимостях, принимавший участие в калькуляции. Да, не совсем явно получилось. Также стоит добавить, что после генерации новые данные поверхностно сравниваются с предыдущими и, если они эквивалентны, то они отбрасываются


                                                            Далее, я бы рекомендовал полностью инкапсулировать лоадеры.

                                                            я специально по максимуму вырезал всё кроме ядра, дабы показать показать нутро как есть, не скрывая ничего за абстракциями

                                                            –2

                                                            .

                                                              +1

                                                              Я очень далек от фронтенда и просто мимопроходил, но вот эта библиотека: https://crank.js.org/guides/getting-started очень напоминает ваши фракталы :)

                                                                0

                                                                спасибо! на первый взгляд действительно есть что-то общее, изучу её для общего развития)

                                                              Only users with full accounts can post comments. Log in, please.