Skip to content

Instantly share code, notes, and snippets.

@nerodono
Last active January 2, 2025 17:14
Show Gist options
  • Save nerodono/1e4359d2b1cf9837dd6d1f2280c4ad88 to your computer and use it in GitHub Desktop.
Save nerodono/1e4359d2b1cf9837dd6d1f2280c4ad88 to your computer and use it in GitHub Desktop.

Буду вести разработку более прозрачно, начну публиковать небольшие дневники. slonogram, в первую очередь, должен быть расширяем и предлагать нечто лучше, чем существующие библиотеки, поэтому следует начать с основ. At core, библиотека для ботов это несложно - датаклассы и запросы к API. Сложная часть это обработка событий, она должна быть продумана так, чтобы хватало всем - и среднему разработчику ботов с знаниями примерно никакими, и любителю долбится в гланды. Потому начнем с основ - с библиотеки для обработки событий.

Sena

За основу возьмем следующий подход:

C = TypeVar("C")
I = TypeVar("I")
N = TypeVar("N")
O = TypeVar("O")

class Handler(Protocol[C, I, N, O]):
    def __call__(self, ctx: C, req: I, next: N, /) -> O:
        ...

У неподготовленного читателя может случится приступ "а зачем?", поясню каждый генерик здесь:

  1. C - контекст. Контекст, в котором обработчик выполняется. Может содержать в себе какие-нибудь причуды для DI, например - динамический лексический скоупинг (уже знакомый вам dishka, по сути, является реификацией лексического скоупинга, без древовидных скоупов)
  2. I - входной тип для функции.
  3. N - функция-продолжение. Почему это генерик - будет объяснено когда приступим к типизации.
  4. O - тип результата функции.

Больше всего люди задаются вопросами, когда видят определение Handler как функции с продолжением. Вспомним где гипотетический читатель мог видеть такой паттерн - да это ж middleware!

FastAPI

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.perf_counter()     # <- Pre-process
    response = await call_next(request)  # <- Call continuation
#   -------------------------------------  <- Post-process
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

Django

# Разработчики уже все за меня расписали
def simple_middleware(get_response):
    # One-time configuration and initialization.
    def middleware(request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        response = get_response(request)
        # Code to be executed for each request/response after
        # the view is called.
        return response
    return middleware

Зачем же смешивать понятие middleware и актуальной функции-обработчика? Но, на самом деле, никто ничего не смешивает, просто в веб-разработке, так уж исторически сложилось, принято делить middleware и актуальную функцию-обработчик. Мы так не будем делать, но, если вам это важно, такая техника называется delimited continuations:

In programming languages, a delimited continuation, composable continuation or partial continuation, is a "slice" of a continuation frame that has been reified into a function. Unlike regular continuations, delimited continuations return a value, and thus may be reused and composed.

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

# При вызове возвращает просто аргумент, конец цепочки
def end(ctx, arg, next):
    return arg

# "Упасть" по цепочке вниз.
# Занятно! - then(fallthrough, fallthrough) = бесконечная рекурсия
def fallthrough(ctx, arg, next):
    return next(ctx, arg, fallthrough)

# Добавить к аргументу
def add(delta):
    def inner(ctx, arg, next):
        return next(ctx, arg + delta, fallthrough)
    return inner

# Соединить две функции
def then(lhs, rhs):
    def inner(ctx, arg, chain_rest):
        def cont(ctx, arg, rhs_next):
            return rhs(ctx, arg, then(rhs_next, chain_rest))

        return lhs(ctx, arg, cont)
    return inner

Бэм! Попользуемся этим:

ctx = {}
add2 = then(add(1), add(1))

print(add2(ctx, 0, end))

# Output: 2

Если вам показалось определение then непонятным, то не беспокойтесь - вы в этом не одиноки. Давайте поясню: По определению lhs вызовет cont, rhs вызовет then(rhs_next, chain_rest), а вот что это? Выглядит же стремно! Тут логика проста, но рекурсивна, если then соединяет две функции, то все, что нам нужно это соединить rhs_next (продолжение, переданное в rhs) с chain_rest (остальная часть цепочки). А если так?

def f(ctx, arg, next):
    return next(ctx, arg + 1, lambda ctx, arg, next: next(ctx, str(arg), fallthrough))

heh = then(f, add(1))
print(repr(heh(ctx, 0, end)))

Ответ - '2', заметим, что порядок исполнения у нас такой

f -> (add(1) -> add(1)) -> str(result)

Схема
      f
+------------+
| +--------+ |
| | add(1) | |
| +--------+ |
|     |      |
| +--------+ |
| | add(1) | |
| +--------+ |
+------------+
      |
     str

А мы еще не использовали контекст. А давайте

# di add!!
def diad(ctx, arg, next):
    return next(ctx, arg + ctx['delta'], fallthrough)

heh = then(diad, diad)
print(heh({'delta': 1}, 0, end))
# Output: 2

Типизировать это все - занятие веселое! В следующих сериях.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment