Русский
Русский
English
Статистика
Реклама

Generators

Из песочницы Эффектное программирование. Часть 1 итераторы и генераторы

10.10.2020 16:22:36 | Автор: admin
Javascript на данный момент является самым популярным языком программирования по версиям многих площадок (например Github). Является ли при этом он самым продвинутым или самым любимым языком? В нём отсутствуют конструкции, которые для других языков являются неотъемлемыми частями: обширная стандартная библиотека, иммутабильность, макросы. Но в нём есть одна деталь, которая не получает, на мой взгляд, достаточно внимания генераторы.

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

while (true) {    const data = yield getNextChunk(); // вызов асинхронной логики    const processed = processData(data);    try {        yield sendProcessedData(processed);        showOkResult();    } catch (err) {        showError();    }}

Это первая, пилотная часть: Итераторы и Генераторы.

Итераторы


Итак, итератор это интерфейс, предоставляющий последовательный доступ к данным.

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

Предлагаю читателю ответить на вопрос: является ли массив итератором?

Ответ
Является. Методы shift и pop отлично позволяют работать с массивом как с итератором.

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

Представим, что нам нужен итератор, который реализует последовательность натуральных чисел. Или чисел Фибоначчи. Или любую другую бесконечную последовательность. В массиве сложно разместить бесконечную последовательность, понадобится механизм постепенного наполнения массива данными, а также изъятия старых данных, чтобы не заполнить всю память процесса. Это излишнее усложнение, которое несёт с собой дополнительную сложность реализации и поддержки, при том, что решение без массива может уместиться в несколько строчек:

const getNaturalRow = () => {    let current = 0;    return () => ++current;};

Также итератором можно представить получение данных из внешнего канала, например websocket.

В javascript итератором является любой объект, у которого есть метод next(), который возвращает структуру с полями value текущее значение итератора и done флагом, указывающим на завершение последовательности (эта договорённость описана в стандарте языка ECMAScript). Такой объект реализует интерфейс Iterator. Перепишем прошлый пример в этом формате:

const getNaturalRow = () => ({    _current: 0,    next() { return {        value: ++this._current,        done: false,    }},});

В javascript также есть интерфейс Iterable это объект, который имеет метод @@iterator (данная константа доступна как Symbol.iterator), который возвращает итератор. Для объектов, реализующих такой интерфейс, доступен обход с помощью оператора for..of. Перепишем наш пример ещё раз, только в этот раз как реализацию Iterable:

const naturalRowIterator = {    [Symbol.iterator]: () => ({        _current: 0,        next() { return {            value: ++this._current,            done: this._current > 3,       }},   }),}for (num of naturalRowIterator) {    console.log(num);}// Вывод: 1, 2, 3

Как можно видеть, нам пришлось сделать так, чтобы флаг done в какой-то момент стал положительным, иначе бы цикл был бесконечным.

Генераторы


Следующим этапом эволюции итераторов стали генераторы. Они предоставляют синтаксический сахар, позволяющий возвращать значения итератора будто значение функции. Генератор это функция (объявляется со звёздочкой: function*), возвращающая итератор. При этом итератор не возвращается явно, в функции лишь возвращаются значения итератора с помощью оператора yield. Когда функция заканчивает своё выполнение, итератор считается завершённым (результаты последующих вызовов метода next будут иметь флаг done равным true)

function* naturalRowGenerator() {    let current = 1;    while (current <= 3) {        yield current;        current++;    }}for (num of naturalRowGenerator()) {    console.log(num);}// Вывод: 1, 2, 3

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

В момент вызова naturalRowGenerator создаётся итератор.

function* naturalRowGenerator() {    let current = 1;    while (current <= 3) {        yield current;        current++;    }}

Далее, когда мы первые три раза вызываем метод next или, в нашем случае, проходим итерации цикла, курсор встаёт после оператора yield.

function* naturalRowGenerator() {    let current = 1;    while (current <= 3) {        yield current;         current++;    }}

И на все последующие вызовы next и после выхода из цикла генератор завершает своё выполнение и, результатами вызова next будет { value: undefined, done: true }

Передача параметров в итератор


Представим, что в наш итератор натуральных чисел нужно добавить возможность сбрасывать текущий счётчик и начинать отчёт с начала.

naturalRowIterator.next() // 1naturalRowIterator.next() // 2naturalRowIterator.next(true) // 1naturalRowIterator.next() // 2

Понятно как обработать такой параметр в самописном итераторе, но как быть с генераторами?
Оказывается, генераторы поддерживают передачу параметров!

function* naturalRowGenerator() {    let current = 1;    while (true) {        const reset = yield current;        if (reset) {          current = 1;        } else {          current++;        }    }}

Переданный параметр становится доступен как результат оператора yield. Попробуем добавить ясности с помощью подхода с курсором. В момент создания итератора ничего не поменялось. Далее следует первый вызов метода next():

function* naturalRowGenerator() {    let current = 1;    while (true) {        const reset = yield current;        if (reset) {          current = 1;        } else {          current++;        }    }}

Курсор замер на моменте возврата из оператора yield. При следующем вызове next, переданное в функцию значение установит значение переменной reset. Куда же попадёт значение, переданное в самый первый вызов next, ведь там же ещё не было вызова yield? Никуда! Растворится в просторах garbage collector-а. Если нужно передать какое-то начальное значение в генератор, то это можно сделать с помощью аргументов самого генератора. Пример:

function* naturalRowGenerator(start = 1) {    let current = start;    while (true) {        const reset = yield current;        if (reset) {          current = start;        } else {          current++;        }    }}const iterator = naturalRowGenerator(10);iterator.next() // 10iterator.next() // 11iterator.next(true) // 10

Заключение


Мы рассмотрели концепцию итераторов и её реализацию в языке javascript. Также мы изучили генераторы синтаксическую конструкцию для удобной реализации итераторов.

Хотя в данной статье я приводил примеры с числовыми последовательностями, итераторы в javascript позволяют решить намного больше задач. С помощью них можно представить любую последовательность данных и даже многие конечные автоматы. В следующей статье я хотел бы рассказать о том, как можно использовать генераторы для построения асинхронных процессов (coroutines, goroutines, csp и т. д.).
Подробнее..

Эпическая сага про маленький custom hook для React (генераторы, sagas, rxjs) часть 2

11.12.2020 12:12:55 | Автор: admin

Часть первая

О генераторах

Генераторы - это новый вид функций, который появился в ES6. О них написано немало статей и приведено множество теоретических примеров. Что касается меня, то прояснить суть генераторов и способ их использования помогла книга You don't know JS, часть async & performance. Из всех книг по JS, которые я изучал, эта наиболее упакована полезной информацией без воды.

Представим, что генератор (функция в объявлении, которой есть *) - это некое электрическое устройство с дистанционным пультом управления. После создания и монтирования генератора (объявления функции) нужно его "крутануть" (выполнить эту функцию), чтобы он вращался на холостых оборотах и "запитал" пульт управления собой (при выполнении функции-генератора возвращает итератор). На этом пульте управления две кнопки: Пуск (вызвать первый раз метод next итератора) и Next (последующие вызовы метода next итератора). Далее с этим пультом управления можно носиться по всей электростанции (по нашему приложению) и когда понадобиться электрическая энергия (некие значения из функции-генератора) нажимать на пульте кнопку next (выполнять метод next() генератора). Генератор производит нужное количество электроэнергии (возвращает некое значение через yield) и опять переходит в холостой режим (функция-генератор ждёт следующего вызова next от итератора). Цикл продолжается, пока генератор может производить электричество (имеются операторы yield) или он не остановится (в функции-генераторе встретится return).

И во всей этой аналогии ключевой момент - это пульт управления (итератор). Его можно передавать в разные части приложения и в нужный момент "забирать" значения из генератора. Для полноты картины, на пульте управления можно добавить неограниченное количество кнопок для запуска генератора в определенных режимах (передача параметров в метод next(любые параметры) итератора), но для реализации хука достаточно и двух кнопок.

Вариант 4. Генератор без промисов

Этот вариант приводится для наглядности, т.к. в полную силу генераторы работают совместно с промисами (механизм async/await). Но этот вариант рабочий и имеет право на существование в определенных простых ситуациях.

Создаю в хуке переменную для хранения ссылки на итератор (ячейка для пульта управления генератором)

const iteratorRef = useRef(null);

Нужно изменить логику хука. При выполнении обработчика события будет запускаться генератор. В данном случае нет никакого обмена данными между генератором и кодом, выполняющим метод next() итератора (генератор просто делает один оборот при нажатии кнопки next). Выглядит это так:

const updateCounter = () => {  iteratorRef.current.next();};const checkImageLoading = (url) => {  const imageChecker = new Image();  imageChecker.addEventListener("load", updateCounter);  imageChecker.addEventListener("error", updateCounter);  imageChecker.src = url;};

При запуске генератор расставляет обработчики событий и затем запускает цикл по сборке ответов из этих обработчиков. Каждому обработчику был вручён пульт и было объяснено, что, как только придёт событие, нужно нажать кнопку next на пульте. Когда приходит событие, обработчик добросовестно жмёт на кнопку и тем самым "прокручивает генератор". В процессе оборота происходит dispatch нужного действия и генератор опять переходит в холостой режим, ожидая следующего сигнала от пульта. Ниже приведён код самого генератора:

function* main() {  for (let i = 0; i < imgArray.length; i++) {    checkImageLoading(imgArray[i].src);  }  for (let i = 0; i < imgArray.length; i++) {    yield true;    dispatch({      type: ACTIONS.SET_COUNTER,      data: stateRef.current.counter + stateRef.current.counterStep    });  }}

Конечно при монтировании хука нужно "крутануть" генератор, чтобы он запитал пульт (вернул итератор в iteratorRef. И после этого нажать кнопку Пуск (выполнить метод next итератора первый раз).

Ниже привожу полный исходный код хука с использованием генератора без промисов.

Исходный код хука Генератор без промисов
import { useReducer, useEffect, useLayoutEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const iteratorRef = useRef(null);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  const updateCounter = () => {    iteratorRef.current.next();  };  const checkImageLoading = (url) => {    const imageChecker = new Image();    imageChecker.addEventListener("load", updateCounter);    imageChecker.addEventListener("error", updateCounter);    imageChecker.src = url;  };  useEffect(() => {    const imgArray = document.querySelectorAll("img");    if (imgArray.length > 0) {      dispatch({        type: ACTIONS.SET_COUNTER_STEP,        data: Math.floor(100 / imgArray.length) + 1      });    }    function* main() {      for (let i = 0; i < imgArray.length; i++) {        checkImageLoading(imgArray[i].src);      }      for (let i = 0; i < imgArray.length; i++) {        yield true;        dispatch({          type: ACTIONS.SET_COUNTER,          data: stateRef.current.counter + stateRef.current.counterStep        });      }    }    iteratorRef.current = main();    iteratorRef.current.next();  }, []);  useLayoutEffect(() => {    stateRef.current = state;    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Но в полной мере мощь генераторов по управлению асинхронным кодом проявляется совместно с использованием промисов.

Вариант 5. Генератор с промисами

Как вы уже догадались генератор будет возвращать промис. Таким образом обработчикам событий не нужно будет вызывать метод next итератора (не нужно будет отдавать им пульт управления и давать инструкции по его использованию). Просто промисифицируем колбэк (мы сами будем знать когда отработал обработчик и без его ведома).

Код генератор изменится следующим образом:

const getImageLoading = async function* (imagesArray) {  for (const img of imagesArray) {    yield new Promise((resolve, reject) => {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => resolve(true));      imageChecker.addEventListener("error", () => resolve(true));      imageChecker.src = img.url;    });  }};

А вызывающий код будет выглядеть так:

for await (const response of getImageLoading(imgArray)) {  dispatch({    type: ACTIONS.SET_COUNTER,    data: stateRef.current.counter + stateRef.current.counterStep  });}

Основную работу по сравнению с предыдущим вариантом выполняет цикл for await ... of. Пульт управления генератором находится у него и он автоматически нажимает кнопку Пуск и Next.

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

Исходный код хука Генератор с промисами
import { useReducer, useEffect, useRef } from "react";import { reducer, initialState, ACTIONS } from "./state";const PRELOADER_SELECTOR = ".preloader__wrapper";const PRELOADER_COUNTER_SELECTOR = ".preloader__counter";const usePreloader = () => {  const [state, dispatch] = useReducer(reducer, initialState);  const stateRef = useRef(state);  const preloaderEl = document.querySelector(PRELOADER_SELECTOR);  const counterEl = document.querySelector(PRELOADER_COUNTER_SELECTOR);  useEffect(() => {    async function imageLoading() {      const imgArray = document.querySelectorAll("img");      if (imgArray.length > 0) {        dispatch({          type: ACTIONS.SET_COUNTER_STEP,          data: Math.floor(100 / imgArray.length) + 1        });          for await (const response of getImageLoading(imgArray)) {          dispatch({            type: ACTIONS.SET_COUNTER,            data: stateRef.current.counter + stateRef.current.counterStep          });        }      }    }    imageLoading();  }, []);  useEffect(() => {    stateRef.current = state;    if (counterEl) {      stateRef.current.counter < 100        ? (counterEl.innerHTML = `${stateRef.current.counter}%`)        : hidePreloader(preloaderEl);    }  }, [state]);  return;};const getImageLoading = async function* (imagesArray) {  for (const img of imagesArray) {    yield new Promise((resolve, reject) => {      const imageChecker = new Image();      imageChecker.addEventListener("load", () => resolve(true));      imageChecker.addEventListener("error", () => resolve(true));      imageChecker.src = img.url;    });  }};const hidePreloader = (preloaderEl) => {  preloaderEl.remove();};export default usePreloader;

Итого:

В этой части статьи показано:

  • как использовать useRef для хранения и использования нужных значений на протяжении всего времени жизни компонента (некий аналог глобальных переменных для компонента)

  • как управлять потоком событий с помощью генераторов, но без использования промисов (с использованием колбэков)

  • как управлять потоком событий, обработчики которых промисифицированы, с помощью генераторов и цикла for await ... of

Ссылка напесочницу

Ссылка нарепозиторий

Продолжение следует... redux-saga...

Подробнее..

Шаблонные функции в Python, которые могут выполнятся синхронно и асинхронно

02.11.2020 00:07:43 | Автор: admin
image

Сейчас практически каждый разработчик знаком с понятием асинхронность в программировании. В эру, когда информационные продукты настолько востребованы, что вынуждены обрабатывать одновременно огромное количество запросов и также параллельно взаимодействовать с большим набором других сервисов без асинхронного программирования никуда. Потребность оказалась такой большой, что был даже создан отдельный язык, главной фишкой которого (помимо минималистичности) является очень оптимизированная и удобная работа с параллельным/конкурентным кодом, а именно Golang. Несмотря на то, что статья совершенно не про него, я буду часто делать сравнения и ссылаться. Но вот в Python, про который и пойдёт речь в этой статье есть некоторые проблемы, которые я опишу и предложу решение одной из них. Если заинтересовала эта тема прошу под кат.



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

Но есть один серьёзный нюанс Python очень тяжело вписывается в современные представления о языке с возможностью реализации параллельной/конкурентной логики. Язык, идея которого зародилась ещё в 80-ых годах и являющийся ровесником Java до определённого времени не предполагал выполнение какого-либо кода конкурентно. Если JavaScript изначально требовал конкурентности для неблокирующей работы в браузере, а Golang совсем свежий язык с реальным пониманием современных потребностей, то перед Python таких задач ранее не стояло.

Это, конечно же, моё личное мнение, но мне кажется, что Python очень сильно опоздал с реализацией асинхронности, так как появление встроенной библиотеки asyncio было, скорее, реакцией на появление других реализаций конкуретного выполнения кода для Python. По сути, asyncio создан для поддержки уже существующих реализаций и содержит не только собственную реализацию событийного цикла, но также и обёртку для других асинхронных библиотек, тем самым предлагая общий интерфейс для написания асинхронного кода. И Python, который изначально создавался как максимально лаконичный и читабельный язык из-за всех перечисленных выше факторов при написании асинхронного кода становится нагромождением декораторов, генераторов и функций. Ситуацию немного исправило добавление специальных директив async и await (как в JavaScript, что важно) (исправил, спасибо пользователю tmnhy), но общие проблемы остались.

Я не буду перечислять их все и остановлюсь на одной, которую я и попытался решить: это описание общей логики для асинхронного и синхронного выполнения. Например, если я захочу в Golang запустить функцию параллельно, то мне достаточно просто вызвать функцию с директивой go:

Параллельное выполнение function в Golang
package mainimport "fmt"func function(index int) {    fmt.Println("function", index)}func main() {    for i := 0; i < 10; i++ {         go function(i)    }    fmt.Println("end")}


При этом в Golang я могу запустить эту же функцию синхронно:

Последовательное выполнение function в Golang
package mainimport "fmt"func function(index int) {    fmt.Println("function", index)}func main() {    for i := 0; i < 10; i++ {         function(i)    }    fmt.Println("end")}


В Python все корутины (асинхронные функции) основаны на генераторах и переключение между ними происходит во время вызова блокирующих функций, возвращая управление событийному циклу с помощью директивы yield. Честно признаюсь, я не знаю, как работает параллельность/конкурентность в Golang, но не ошибусь, если скажу, что работает это совсем не так, как в Python. Несмотря даже на существующие различия во внутренностях реализации компилятора Golang и интерпретатора CPython и на недопустимость сравнения параллельности/конкурентности в них, всё же я это сделаю и обращу внимание не на само выполнение, а именно на синтаксис. В Python я не могу взять функцию и запустить её параллельно/конкурентно одним оператором. Чтобы моя функция смогла работать асинхронно, я должен явно прописать перед её объявлением async и после этого она уже не является просто функцией, она является уже корутиной. И я не могу без дополнительных действий смешивать их вызовы в одном коде, потому что функция и корутина в Python совсем разные вещи, несмотря на схожесть в объявлении.

def func1(a, b):    func2(a + b)    await func3(a - b)  # Ошибка, так как await может выполняться только в корутинах

Моей основной проблемой оказалась необходимость разрабатывать логику, которая может работать и синхронно, и асинхронно. Простым примером является моя библиотека по взаимодействию с Instagram, которую я давно забросил, но сейчас снова за неё взялся (что и сподвигло меня на поиск решения). Я хотел реализовать в ней возможность работать с API не только синхронно, но и асинхронно, и это было не просто желание при сборе данных в Интернет можно отправить большое количество запросов асинхронно и быстрее получить ответ на них всех, но при этом массивный сбор данных не всегда нужен. В данный момент в библиотеке реализовано следующее: для работы с Instagram есть 2 класса, один для синхронной работы, другой для асинхронной. В каждом классе одинаковый набор методов, только в первом методы синхронные, а во втором асинхронные. Каждый метод выполняет одно и то же за исключением того, как отправляются запросы в Интернет. И только из-за различий одного блокирующего действия мне пришлось практически полностью продублировать логику в каждом методе. Выглядит это примерно так:

class WebAgent:    def update(self, obj=None, settings=None):        ...        response = self.get_request(path=path, **settings)        ...class AsyncWebAgent:    async def update(self, obj=None, settings=None):        ...        response = await self.get_request(path=path, **settings)        ...

Всё остальное в методе update и в корутине update абсолютно идентичное. А как многие знают, дублирование кода добавляет очень много проблем, особенно остро это ощущается в исправлении багов и тестировании.

Для решения этой проблемы я написал собственную библиотеку pySyncAsync. Идея такова вместо обычных функций и корутин реализуется генератор, в дальнейшем я буду называть его шаблоном. Для того, чтобы выполнить шаблон его нужно сгенерировать как обычную функцию или как корутину. Шаблон при выполнении в тот момент, когда ему нужно выполнить внутри себя асинхронный или синхронный код возвращает с помощью yield специальный объект Call, который указывает, что именно вызвать и с какими аргументами. В зависимости от того, как будет сгенерирован шаблон как функция или как корутина таким образом и будут выполнятся методы, описанные в объекте Call.

Покажу небольшой пример шаблона, который предполагает возможность делать запросы в google:

Пример запросов в google с помощью pySyncAsync
import aiohttpimport requestsimport pysyncasync as psa# Регистрируем функцию для синхронного запроса в google# В скобочках указываем имя для дальнейшего указания в объекте Call@psa.register("google_request")def sync_google_request(query, start):    response = requests.get(        url="http://personeltest.ru/aways/google.com/search",        params={"q": query, "start": start},    )    return response.status_code, dict(response.headers), response.text# Регистрируем корутину для асинхронного запроса в google# В скобочках указываем имя для дальнейшенго указания в объекте Call@psa.register("google_request")async def async_google_request(query, start):    params = {"q": query, "start": start}    async with aiohttps.ClientSession() as session:        async with session.get(url="http://personeltest.ru/aways/google.com/search", params=params) as response:            return response.status, dict(response.headers), await response.text()# Шаблон для получения первых 100 результатовdef google_search(query):    start = 0    while start < 100:        # В Call аргументы передавать можно как угодно, они так же и будут переданы в google_request        call = Call("google_request", query, start=start)        yield call        status, headers, text = call.result        print(status)        start += 10if __name__ == "__main__":    # Синхронный запуск кода    sync_google_search = psa.generate(google_search, psa.SYNC)    sync_google_search("Python sync")    # Асинхронный запуск кода    async_google_search = psa.generate(google_search, psa.ASYNC)    loop = asyncio.get_event_loop()    loop.run_until_complete(async_google_search("Python async"))


Расскажу немного про внутреннее устройство библиотеки. Есть класс Manager, в котором регистрируются функции и корутины для вызова с помощью Call. Также есть возможность регистрировать шаблоны, но это необязательно. У класса Manager есть методы register, generate и template. Эти же методы в примере выше вызывались напрямую из pysyncasync, только они использовали глобальный экземпляр класса Manager, который уже создан в одном из модулей библиотеки. По факту можно создать свой экземпляр и от него вызывать методы register, generate и template, таким образом изолируя менеджеры друг от друга, если, например, возможен конфликт имён.

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

Метод template позволяет зарегистрировать генератор как шаблон в менеджере. Это нужно для того, чтобы была возможность получить шаблон по имени.

Метод generate позволяет на основе шаблона сгенерировать функцию или корутину. Принимает два аргумента: первый имя шаблона или сам шаблон, второй sync или async во что генерировать шаблон в функцию или в корутину. На выходе метод generate отдаёт готовую функцию или корутину.

Приведу пример генерации шаблона, например, в корутину:

def _async_generate(self, template):    async def wrapper(*args, **kwargs):        ...        for call in template(*args, **kwargs):            callback = self._callbacks.get(f"{call.name}:{ASYNC}")            call.result = await callback(*call.args, **call.kwargs)        ...    return wrapper

Внутри генерируется корутина, которая просто итерируется по генератору и получает объекты класса Call, потом берёт ранее зарегистрированную корутину по имени (имя берёт из call), вызывает её с аргументами (которые тоже берёт из call) и результат выполнения этой корутины также сохраняет в call.

Объекты класса Call являются просто контейнерами для сохранения информации о том, что и как вызывать и также позволяют сохранить в себе результат. wrapper также может вернуть результат выполнения шаблона, для этого шаблон оборачивается в специальный класс Generator, который здесь не показан.

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

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

И спасибо за внимание к данному вопросу и к этой статье в частности!
Подробнее..
Категории: Python , Asynchronous , Asyncio , Generators

Из песочницы Как я писал кодогенератор на PHP и что из этого получилось

18.07.2020 18:23:19 | Автор: admin

Причины и проблемы, которые нужно было решить


В этой статье я вам расскажу о том как я писал кодогенератор на php. Расскажу о пути, который он прошел от генерации простых таблиц, до довольно полноценного генератора html и css кода. Приведу примеры его использования и покажу уже сделанные проекты.


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


После бесконеного ренейма проекта Проект получил имя MelonPHP. Чтобы люди думали о еде когда произносили его имя? Но у нас тут статья не о генерации бреда, по этому давайте я вам расскажу о причине его создания.


Написать надо было много, но это не проблема. Основная проблема заключалась в выводе HTML кода через PHP. Я постараюсь объяснить проблему ниже.


Например вот вывод текста через всем знакомое echo:


$text = "out text";echo "<p>$text</p>";

Выглядит просто и понятно. Давайте возьмем отрезок кода моего друга, где он генерирует таблицу:


...$sql = "SELECT * FROM table";$result = $conn->query($sql);if($result->num_rows > 0) {    echo "<b>Table table</b><br><br>";    echo "<table border=2>";  echo "<tr><td> name </td>"."<td> name </td>"."<td> name </td></tr>";    while($row = $result->fetch_assoc()) {        echo "<tr><td>".$row["name"]."</td><td>".$row["name"]."</td><td>".$row["name"]."</td></tr>";    }    echo "</table>";} else {echo "0 results";}...

Это страшный код демонстрирует проблемы, которые я хотел решить:


  • Присутствие html в php коде, что делает его по моему мнению менее читаемым. Все-таки файл для одного яп должен содержать код только одного яп(а), по моему мнению
  • Нет разделения логики, все в каше. Хотелось более приятный "фронтенд" на PHP

Стоит отметить, что я относительно давно пишу на Flutter и мне очень нравится идея заложенная в его основе, связанная с написанием интерфейса с помощью постройки дерева из виджетов. Я решил позаимствовать оттуда идею с нодами (виджетами).


Я был уверен, что быстрее будет написать небольшой кодогенератор, чем терпеть это.


Изначально генератор занимался генерацией таблиц через функции. Но потом перерос в ней-то более масштабное.


Основными идеями были следующими:


  • UI пишется из элементов/компонентов (привет React)
  • Удобные макеты (Избавиться от div, div, div, div...)
  • Чтобы весь UI писался на PHP (без JS, без HTML, без CSS).
  • Rebuild через callback события, через AJAX + JQuery не суждено
  • Удобная система роутов не суждено
  • Поддержка CSS (и не просто строку писать, на уровне "width: 100px", а полноценная поддержка прямо в PHP коде)
  • ООП

Особенности MelonPHP


  • Почти все элементы (кроме текста, кнопки и еще нескольких) по умолчанию имеют ширину и высоту в 100%, в том числе и документ.
  • Кроме того, если элементы будут выходить за пределы страницы, то скролла по умолчанию не будет. Для этого нужно использовать ScrollView.
  • Так же по умолчанию нельзя выделать никакие элементы.

Архитектура


Очень печально, что в PHP отсутствует передачу параметров по имени, и по этому я решил использовать вместо них функции. Я уверен что это не менее удобный аналог (Какого было мое удивление, когда Microsoft показали MAUI, который использует ту же идею с функциями).


Все классы в MelonPHP наследуется от Node. Это простой класс, который имеет только 2 функции: Generate(), static Create().


  • Generate() возвращает string сгенерированный код.
  • Create() это статическая функция. Она нужна чтобы было проще создавать ноды в дереве.

abstract class Node{  abstract function Generate() : string;  static function Create() ...}

Element

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


Элемент в основном занимается генерацией чистого html кода.


Элементами в Фреймворк являются такие сущности как контейнер, кнопка, таблица и тд.


Component

Основная идея компонента в том, что этот класс управляет, и состоит из дерева элементов в нем. Компонент наследуется от элемента (бредовая идея).


Компонентами могут быть например дисплеи (аналог страниц в MelonPHP), списки, карточки, навигационные меню и тд.


abstract class Component extends Element{  function Initialize() ...  abstract function Build() : Element;  function Detach() ...}

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


Попробуем написать простой компонент. Создадим класс ListItem наследуемый от Component.


Перезапишем функции Initialize() и Build().


Initialize() вызывается при создание компонента. В ней например можно инициализировать переменные или обработать логику.

Build() вызывается при генерации элемента. В ней обязательно должен возвращаться элемент. Обязательная для перезаписи.

Detach() вызывается при удалении компонента.

В Build() возвратим контейнер, а в качестве его ребенка, элемент текста и присвоим ему текст из переменной класса $Text.


В Initialize() пропишем значение $Text по умолчанию.


Добавим функцию Text(string) в которой будет записываться значение пользователя в переменную $Text.


Обязательно надо возвращать $this в функциях, которые будут вызываться в дереве.

class ListItem extends Component{  private $Text;  function Initialize() {    $this->Text = "Name";  }  function Build() : Element {    return Container::Create()    ->Child(      Text::Create()      ->Text($this->Text)    );  }  function Text(string $string) {    $this->Text = $string;    return $this;  }}

DisplayComponent

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


Попробуем написать пример простого дисплея.


В функции Build() возвратим Document и присваиваем ему Title(string).


В DisplayComponent, в функции Build() всегда должен возвращаться Document. Document это класс, который генерирует стандартную разметку HTML5.

Создадим функцию BuildList(), в которой через цикл заполним колонку созданными выше ListItem.


В качестве ребенка документа вызовем BuildList() функцию. Разделение дерева из нод на функции не дает ему превратиться в макаронного монстра.


Если будет ситуация что надо выполнить какую-то логику прямо в дереве, то для того есть класс Builder. Но так лучше не делать...

После тела класса вызовем функцию Diplay(), которая при переходе на данный файл, на сайте cгенерирует его и выведит.


class ListDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    // название страницы    ->Title("test page")    ->Child($this->BuildList());  }  function BuildList() {    $column = new Column;    for($i = 0; $i < 10; $i++)      $column->Children(        ListItem::Create()        ->Text("number: $i")      );    return $column;  }} ListDisplay::Display();

Макеты


Одна из целей преследуемых при создании фреймворка удобство при создание макетов сайта.


Container


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


Может содержать только одного ребенка.


Column и Row

У большинства элементов есть дети. Если у элемента доступен метод Child то он может иметь только одного ребенка, а если Children то у него может быть больше одного ребенка.


Так же Child перезаписывает переменную ребенка в то время как Children добавляет аргументы в верх стека.


В Children если один элемент в аргументе то не обязательно его заносить в массив.

Тоесть вместо Children([Text::Create()]) можно написать Children(Text::Create())

Column это макет который выравнивает его детей вертикально.


Обращаясь к функциям CrossAlign и MainAlign можно выравнивать детей внутри колонки.



Row идентичен Column, но выравнивает детей горизонтально.



Stack

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



ScrollView, HorizontalScrollView, VerticalScrollView


Эти контейнеры который является областью для скороллинга.


HorizontalScrollView в этом контейнере можно скроллить только по горизонтальной оси.


VerticalScrollView в этом контейнере можно скроллить только по вертикальной оси.


ScrollView в этом контейнере можно скроллить по всем осям.


Стилизация


Долга думая как лучше встроить css в фреймворк я пришел к идее с константами.


Например у нас в css есть background-color. Я строку записываю в константу и в php коде можно будет использовать без "". Это намного удобнее.


...const BackgroundBlendMode = "background-blend-mode";const BackgroundAttachment = "background-attachment";const Border = "border";const BorderSpacing = "border-spacing";const BorderRadius = "border-radius";const BorderImage = "border-image";...

Что качается например такой конструкции "34".Px. Тут идея с константами выглядит не читабельно. По этому я решил в таких ситуациях использовать функции для css например Px(34). Выглядит понятно и вписывается в пхп код.


Простая стилизация

Для простой стилизации в элементе функция ThemeParameter(...). Первый аргумент это название параметра, а второй аргумент это или массив из значений/значение.


Рассмотрим пример.


В первом параметре мы изменим цвет фона на #f0f0f0.


В втором параметре мы добавим отступы. Сверху и снизу 20px, справа и слева 15px.


Значения в массиве генерируются через пробел. Если вы хотите чтобы генерация происходила через запятую, то для етого есть функция CommaLine().

...Container::Create()->ThemeParameter(BackgroundColor, Hex("f0f0f0"))->ThemeParameter(Padding, [Px(20), Px(15)]);...

Как видно все очень просто и удобно, но если нам понадобятся модификаторы (hover например)? Для этого сделаны темы.


Темы

Темы в этом Фреймворк это более продвинутый css, с media, keyframes, и модификаторами.


Напишем тему для контейнера с модификатором hover и active.


Для того надо создать класс темы и добавить в него ThemeBlock через метод ThemeBlocks.


Блоку темы нужно присвоить ключ / ключи. Я назову ключ my_container.


Дальше в блок темы можно добавить модификаторы. Я добавил: StandartModifier, HoverModifier, ActiveModifier. И задал для них параметры тебя через метод Parameter(...). Parameter работает так же как ThemeParameter.


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ])  ]);}

Дальше контейнеру я присвоил ключ (имя класса в css) темы через метод ThemeKeys. Но для того чтобы тему можно было использовать ее надо добавить в документ через метод Themes.


class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")    );  }} TestThemeDisplay::Display();

После можно запустить дисплей и увидеть что тема по ключу применилась.


Продвинутые анимации

Для продвинутой анимации есть keyframes.


Для того чтобы добавить в тему keyframe, используйте метод FrameBlocks.


Добавим уже в существующую тему FrameBlock.


В FrameBlock есть метод Frames. Вызовем его и добавим несколько фреймов, так же для каждого фрейма надо указывать Value. Оно может быть в процентах (используйте функцию Pr(value)) или может быть константа From, To.


Каждому кадру можно добавлять параметры которые будут уникальны и плавно изменятся между другими кадрами.


Так же добавим блок темы в котором будет применяться эта анимация и назовем его shake_text.


Создадим текст в предыдущем дисплее, в контейнере и применим ему ключ темы.


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


function GetMyTheme() : Theme {  return Theme::Create()  ->ThemeBlocks([    ThemeBlock::Create()    ->Keys("my_container")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Padding, [Px(10), Px(12)]),      HoverModifier::Create()      ->Parameter(BackgroundColor, Green),      ActiveModifier::Create()      ->Parameter(BackgroundColor, Blue)    ]),    ThemeBlock::Create()    ->Keys("shake_text")    ->Modifiers([      StandartModifier::Create()      ->Parameter(Color, Red)      ->Parameter(Animation, ["shake_text_anim", ".2s", "ease-in-out", "5", "alternate-reverse"])    ])  ])  ->FrameBlocks(    FrameBlock::Create()    ->Key("shake_text_anim")    ->Frames([      Frame::Create()      ->Value(Pr(0))      ->Parameter(Transform, Translate(0, 0)),      Frame::Create()      ->Value(Pr(25))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(50))      ->Parameter(Filter, Blur(Px(1.2))),      Frame::Create()      ->Value(Pr(75))      ->Parameter(Color, Hex("ff4040"))      ->Parameter(Filter, Blur(Px(0.5))),      Frame::Create()      ->Value(Pr(100))      ->Parameter(Transform, Translate(Px(10), 0)),    ])  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes(GetMyTheme())    ->Child(      Container::Create()      ->ThemeKeys("my_container")      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Адаптивность

Создадим еще 2 темы. Одна будет для мобильных девайсов, а вторая для пк. У темы есть функции: MinWidth, MaxWidth, MinHeight, MaxHeight, объявив которые вы можете указать на каком размере будет работать тема.


Теме для телефонов зададим MinWidth 800px.


Теме для пк зададим MaxWidth 800px.


Создадим блок темы где в стандартном модификаторе для мобильной версии будет присваиваться цвет фона зелёный, а на пк версии желтый. Назовем блок adaptive_color.


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


Добавим ключи темы к контейнеру.


function GetMobileTheme() : Theme {  return Theme::Create()  ->MinWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Green)    )  );}

function GetDesktopTheme() : Theme {  return Theme::Create()  ->MaxWidth(Px(800))  ->ThemeBlocks(    ThemeBlock::Create()    ->Keys("adaptive_color")    ->Modifiers(      StandartModifier::Create()      ->Parameter(BackgroundColor, Red)    )  );}

class TestThemeDisplay extends DisplayComponent{  function Build() : Document {    return Document::Create()    ->Themes([      GetMyTheme(),       GetDesktopTheme(),       GetMobileTheme()    ])    ->Child(      Container::Create()      ->ThemeKeys(["my_container", "adaptive_color"])      ->Child(        Text::Create()        ->ThemeKeys("shake_text")        ->Text("Error text")      )    );  }} TestThemeDisplay::Display();


Логика


Попробуем написать простой кликер.


Для начала нам надо создать класс и наследовать его от DisplayComponent.


Создадим функцию Build() и возвратим в ней Document.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker");   }} ClickerDisplay::Display();

Добавим колонку в качестве ребенка документа.


Так же в качестве детей колонки добавим текст и кнопку.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->Children([        Text::Create()        ->Text("Pressed 0 times"),        Button::Create()        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Результат будет следующим.



Далее добавим простые ThemeParameter, чтобы сделать наш пример красивее.


class ClickerDisplay extends DisplayComponent{  function Build() : Element {    return Document::Create()    ->Title("Clicker")    ->Child(      Column::Create()      ->ThemeParameter(Padding, Px(15))      ->Children([        Text::Create()        ->ThemeParameter(PaddingBottom, Px(15))        ->Text("Pressed 0 times"),        Button::Create()        ->ThemeParameter(Width, Auto)        ->ThemeParameter(Padding, [Px(4), Px(10)])        ->ThemeParameter(BackgroundColor, Blue)        ->ThemeParameter(Color, White)        ->ThemeParameter(BorderRadius, Px(4))        ->Text("Press")      ])    );   }} ClickerDisplay::Display();

Выглядит куда лучше в несколько простых строчек.



Теперь можно добавить логику.


Для начала нужно инициализировать функцию Initialize() и создать приватную переменную TapCount.


Аналог form в фреймворке это Action.

Добавим Action в наше дерево элементов. Action тип пусть будет Post. В качестве детей укажем нашу колонку где находится наша кнопка.


Далее добавим click_count переменную в Action. А в качестве ее значение присвоим TapCount.


В Initialize() через Action::GetValue(name, standart_value, action_type) получим наше переменную. В качестве значения по умолчанию укажем 0, а в качестве типа укажем Post.


Добавим инкремент для нашей переменной.


В тексте выведим "Press $this->TapCount times".


Все, простой клинкер готов.


class ClickerDisplay extends DisplayComponent{  private $TapCount;  function Initialize() {    $this->TapCount = Action::GetValue("click_count", 0 /* standart value */, ActionTypes::Post);    $this->TapCount++;  }  function Build() : Document {    return Document::Create()    ->Title("Test page")    ->Child(      Action::Create()      ->Type(ActionTypes::Post)      ->Variable("click_count", $this->TapCount)      ->Child(        Column::Create()        ->ThemeParameter(Padding, Px(15))        ->Children([          Text::Create()          ->ThemeParameter(PaddingBottom, Px(15))          ->Text("Press $this->TapCount times"),          Button::Create()          ->ThemeParameter(Width, Auto)          ->ThemeParameter(Padding, [Px(4), Px(10)])          ->ThemeParameter(BackgroundColor, Blue)          ->ThemeParameter(Color, White)          ->ThemeParameter(BorderRadius, Px(4))          ->Text("Press")        ])      )    );  }} ClickerDisplay::Display();


Итог


Мне удалось написать простой, но достаточно мощный кодогенератор.


Он прошол путь от генерации простых таблиц до полноценного генератора html и css, на котором можно удобно верстать проекты и совмещять верстку с логикой.


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


Скриншоты курсового проекта сделанного на MelonPHP




Источники


GitHub MelonPHP


Flutter


MAUI

Подробнее..
Категории: Php , Ui , Framework , Php7 , Generators , Codegeneration

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru