Глубинный котер
95 subscribers
60 photos
7 videos
4 files
71 links
Download Telegram
Ну разве не так?
3🤓2
Для меня такое откровение, что тайп чекер и интерпретатор — суть одно и тоже
4🍌1
В Python 3.12 подвезли события виртуальной машины и API для работы с ними. В PEP 669 – Low Impact Monitoring for CPython описываются эти события, главная мотивация их добавить — профилирование приложения на проде без влияния на производительность

C++ and Java developers expect to be able to run a program at full speed (or very close to it) under a debugger. Python developers should expect that too.


API довольно низкоуровневый, но если поиграться, можно лучше понять исполнение python программ. Например, если прикрутить коллбеки на события PY_YIELD и STOP_ITERATION, то можно понаблюдать за работой корутин:

import sys
import random
import asyncio

from types import CodeType
from contextlib import contextmanager


@contextmanager
def log_cpython_vm_events(events_callbacks: dict):
if len(events_callbacks) > 5:
raise ValueError("Too many events callbacks")

try:
for tool_id, (event, callback) in enumerate(events_callbacks.items(), start=1):
sys.monitoring.use_tool_id(tool_id, "test")
sys.monitoring.register_callback(tool_id, event, callback)
sys.monitoring.set_events(tool_id, event)
yield
finally:
for tool_id, (event, callback) in enumerate(events_callbacks.items()):
sys.monitoring.register_callback(tool_id, event, None)
sys.monitoring.free_tool_id(tool_id)


def callback_on_yield(code: CodeType, instruction_offset: int, retval: object):
next_frame = sys._getframe(1)
print(
f'Yield from "{code.co_name}" with locals={next_frame.f_locals} witch return instance id={id(retval)} of type {type(retval).__name__}'
)


def callback_on_stop_iteration(
code: CodeType, instruction_offset: int, exception: object
):
next_frame = sys._getframe(1)
print(
f'Stop iteration from "{code.co_name}" with locals={next_frame.f_locals} witch raise exc "{type(exception).__name__}"'
)


EVENTS_CALLBACKS_MAPPING = {
sys.monitoring.events.PY_YIELD: callback_on_yield,
sys.monitoring.events.STOP_ITERATION: callback_on_stop_iteration,
}


async def run(idx: int):
await asyncio.sleep(random.random())


async def main():
await asyncio.gather(*map(run, range(2)))


with log_cpython_vm_events(EVENTS_CALLBACKS_MAPPING):
asyncio.run(main())
👍21🤷‍♂1
Краем уха слышал про акторную модель в Erlang и в Akka (фреймворк на Scalla), но не понимал, как обстоят дела с гарантиями доставки. Если сообщения между акторами хранятся в памяти, то мы их потеряем при перезапуске/падении нашего приложения.

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

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

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

Кортеж является типом-произведения, а его значения принадлежат уже к декартовому (?) произведению множеств значений. Соотвественно, мы можем всегда найти функцию, которая принимает один из множителей. Повторяя поиск такой функции для других множителей, мы сможем найти последовательность функций от одного типа эквивалентную функции от типа-произведения, то есть кортежа
31👍1
Увидел красоту реактивного программирования в докладе Ахтяма Сакаева

Главный вывод: бизнес-процессы можно моделировать через стримы, ака стрелочки на диаграмме у архитекторов
4👎1
Ознакомился со статьей Understanding Real-World Concurrency Bugs in Go о которой узнал из Блог*. Главный вывод: добавление новых семантических конструкций в языке увеличивает количество багов.

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

Из статьи можно сделать вывод, что Go не обеспечивает достаточный уровень проверок на этапе компиляции. Как в том же Rust на уровне теории типов были выявлены и устранены баги в корутинах. Но статья была написана в 2019 году, на текущий момент многие примеры из статьи поднимают ошибки во время исполнения, что уже неплохо.
2👍2
Ох уж этот escape анализ
😁2
package main

import "fmt"

type SelfReferential struct {
r *SelfReferential
field int
}

func make_self_referential() SelfReferential {
s := SelfReferential {
r: nil,
field: 1,
}
s.r = &s
return s
}

func main() {
s := make_self_referential()
s.r.field = 2
fmt.Println(s.field)
fmt.Println(s.r.field)
fmt.Println(s.r.r.field)
}


Ваши догадки, что случится при запуске этой программы:

1. (lawful good) Выведется 2 2 2 без UB (структура аллоцировалась на куче)
2. (lawful evil) Произойдет UB или паника (структура аллоцируется на стеке, в s.r окажется dangling pointer)
3. (chaotic neutral) Выведется 1 2 2 без UB (🤷‍♀️)

#dev #go
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🥰2
Когда глаз дёргается от «функционалов» и «функций» в постановке задачи от бизнеса
🥰3😁21
Книга "Структурированный дизайн" Йордона и Константина, похоже, переживает настоящее переосмысление спустя более 50 лет после первого издания. На неё ссылались Влад Хононов и Кент Бек в своих новых книгах, а теперь на конференции Joker был доклад, посвящённый применению идей из этой книги в современной промышленной разработке. И знаете, что самое интересное? Эти идеи оказались удивительно злободневными, особенно в контексте функционального программирования.

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

Тема эффектов, кстати, сейчас набирает популярность. Недавно вышла книга Effect Oriented Programming с примерами на ZIO, которая тоже поднимает вопросы управления эффектами. Видимо, придётся погрузиться в современные практики приручения эффектов, так что Scala 3, до встречи
👍1🐳1
Глубинный котер
Книга "Структурированный дизайн" Йордона и Константина, похоже, переживает настоящее переосмысление спустя более 50 лет после первого издания. На неё ссылались Влад Хононов и Кент Бек в своих новых книгах, а теперь на конференции Joker был доклад, посвящённый…
В этом докладе я наткнулся на интересный и, казалось бы, очевидный критерий, который помогает оценить, насколько архитектура приложения находится в порядке. Если иерархия вызовов компонентов образует дерево — это хороший знак. Всё работает как надо, компоненты изолированы, и система остаётся поддерживаемой. Но если эта иерархия превращается в граф с множеством связей между компонентами — это явный сигнал, что что-то пошло не так.

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

Этот критерий — отличный способ быстро оценить состояние проекта и понять, где могут скрываться проблемы. Если вы видите "граф" вместо "дерева", возможно, пришло время для рефакторинга
4
Есть API, в который необходимо передавать ключ идемпотентности. На первый взгляд, всё кажется простым: можно использовать любой идентификатор или хэш в качестве ключа. Однако есть нюансы:

1. Нужно сделать N запросов на дочерние ресурсы
2. Ключ идемпотентности должен быть в формате UUID

Возникает вопрос: использовать ли идентификатор дочернего ресурса в качестве ключа или же родительского? Но здесь есть две проблемы:

1. Дочерние ресурсы не имеют UUID-идентификаторов
2. Использование родительского идентификатора не подходит, так как это сделает некорректными смежные запросы для других дочерних ресурсов

Решение сразу приходит на ум: можно склеить идентификаторы дочернего и родительского ресурсов. Но как превратить результат в UUID? Это довольно легко сделать, пример на Go:

concatenated := []byte("<ParentID>/<DaughterID>")
result := uuid.NewSHA1(uuid.Nil, concatenated)


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

Альтернативное решение — честно генерировать UUID, сохранять его в базу данных и использовать до победного конца в ретраях запроса. Но это будет в разы сложней реализовать и тяжело поддерживать в будущем
👍31
Уже как вторую неделю продолжаю изучать Scala 3 и ZIO. Изначально я заинтересовался этим стеком после прочтения книги Effect-Oriented Programming, где примеры были приведены на этих технологиях. По завершению чтения у меня сложилось впечатление (возможно, ошибочное), что основная идея ZIO — это решение проблемы эффектов через инъекцию зависимостей и модульность системы. Каждый компонент с эффектами изолирован, что упрощает его замену и тестирование. Но к тем же выводом приходят и другие направления в промышленной разработке, тот же test first подход.

Из-за этого я не увидел особых преимуществ ZIO (не судите строго, Scala-разработчики). Похожие подходы можно найти во многих других практиках, а тесты в официальных примерах ZIO кажутся недостаточно лаконичными. Для сравнения я взял Kotlin с библиотеками Arrow и Ktor.

Рассмотрим реализацию эндпоинта для создания пользователя на основе данных из запроса. Сначала пример на ZIO:


Method.POST / "users" -> handler { (req: Request) =>
for {
u <- req.body.to[User].orElseFail(Response.badRequest)
r <- UserRepo
.register(u)
.mapBoth(
_ => Response.internalServerError(s"Failed to register the user: $u"),
user => Response(body = Body.from(user))
)
} yield r
}


А теперь аналогичный пример на Kotlin:


post<UsersResource> {
either {
val user = receiveCatching<User>().bind().user
UserRepo
.register(user)
.bind().user
}
.respond(HttpStatusCode.Created)
}


В целом, код на ZIO выглядит менее лаконичным, хотя выразительнее на подобии конструкций вроде body.to[User]. При этом UserRepo в Scala выводится из типа функции Routes[UserRepo, Response], тогда как в Kotlin репозиторий передаётся через параметры функций. Оба подхода работают с ошибками как с монадами, но в Arrow, на мой взгляд, код получился минималистичней.

Теперь посмотрим на тесты. Пример на Scala:


test("Can register user") {
for {
client <- ZIO.service[Client]
_ <- TestServer.addRoutes(UserRoutes())
port <- ZIO.serviceWith[Server](_.port)
url = URL.root.port(port)
createResponse <- client(
Request.post(url / "users", Body.from[User](testUser))
)
result <- getResponse.body.to[User]
} yield assertTrue(result == testUser)
}.provideSome[Client with Driver with UserRepo](
TestServer.layer,
Scope.default,
InmemoryUserRepo.layer
)


Аналогичный тест на Kotlin:


"Can register user" {
withServer {
val response =
post(UsersResource()) {
contentType(ContentType.Application.Json)
setBody(testUser))
}

with(response.body<User>().user) {
assert(username == testUser.Name)
assert(email == testUser.Email)
}
}
}


Субъективно, Scala с ZIO действительно использует систему типов на полную, но, к моему удивлению, конечный код получается более многословным, чем на Kotlin с Arrow и Ktor. Это особенно заметно на примере тестов, где в Kotlin вся "шелуха" скрыта за withServer. Кроме того, работа с результатом через bind() в Arrow позволяет избежать дублирования обработки ошибок (вспомним err != nil из Go). В Rust также используется bind() для работы с ошибками, и это действительно удобно.

При этом работа с эффектами в ZIO будто бы не сильно отличается от Arrow c Ktor, скорее всего просто не до конца понял фишку. Но пока вывод один: эффекты это зло от которого нет серебряной пули в виде фреймворка.
4👍2
Есть мнение, что одна из ключевых особенностей Go — это кодогенерация. В Go многие проблемы решаются генерацией нового кода, начиная от работы с Protobuf и заканчивая созданием моков. Долгое время в языке не было дженериков, потому что считалось, что кодогенерация может их заменить.

Если копнуть глубже в другие стеки, то окажется, что кодогенерация там используется так же часто, как и в Go, но она скрыта от глаз разработчиков. В языках без сборщика мусора кодогенерация часто ассоциируется с макросами. Например, в Rust макросы активно используются для генерации кода на этапе компиляции. Возьмем макрос vec!, который генерирует код для создания и инициализации вектора:

let v = vec![1, 2, 3];


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

Spark генерирует Java-код на лету для выполнения map-reduce операций. Разработчик пишет высокоуровневые трансформации, а Spark самостоятельно генерирует и оптимизирует низкоуровневый код.

SQLDelight генерирует Kotlin-классы на основе SQL-запросов, описанных в .sq файлах. Разработчик пишет SQL, а SQLDelight создает типобезопасные методы для работы с базой данных. Есть похожее решения под Java — JOOQ, а под Scala — Quill.

Отдельного внимания заслуживают плагины компилятора Kotlin, которые занимаются кодогенерацией на этапе компиляции. Например, компилятор автоматически генерирует методы equals, hashCode, toString и copy для data-классов. Через плагины компилятора также работает библиотека kotlinx.serialization, которая генерирует код для сериализации и десериализации данных.

Кодогенерация — это мощный инструмент, который используется во многих стеках, но в Go она явная, а в других скрыта от конечного пользователя.
6👍1😱1
В Go существует интересный нюанс, связанный с компиляцией функций, которые формально возвращают значение, но фактически ведут себя как функции, возвращающие Unit (т.е. ничего не возвращают). Рассмотрим пример:

func unitInFact() bool {
for {} // Бесконечный цикл
}


Несмотря на то, что функция unitInFact() объявлена как возвращающая bool, она успешно компилируется, хотя и не содержит явного return. Это поведение объясняется правилами проверки terminating statements на этапе семантического анализа.

При компиляции проверка тела функции происходит в методе funcBody, где ключевым является следующий фрагмент:

if sig.results.Len() > 0 && !check.isTerminating(body, "") {
check.error(atPos(body.Rbrace), MissingReturn, "missing return")
}


В нашем случае:
- sig.results.Len() возвращает 1 (функция объявлена с возвращаемым значением)
- check.isTerminating(body, "") возвращает true (тело функции считается завершающим)

Проверка цикла на принадлежность к terminating statements происходит в методе isTerminating:

case *ast.ForStmt:
if s.Cond == nil && !hasBreak(s.Body, label, true) {
return true
}
}


Условие выполняется, если:
- Цикл не имеет условия (for {})
- Тело цикла не содержит операторов break (включая неявные)

Это поведение не просто особенность реализации, а часть официальной спецификации Go:

Если сигнатура функции объявляет возвращаемые параметры, список операторов в теле функции должен заканчиваться terminating statements


Такой прагматичный подход к системе типов заметно отличается от более строгих проверок в языках вроде Kotlin или Haskell, где подобные случаи обычно считаются ошибками типизации
51
Глубинный котер
В Go существует интересный нюанс, связанный с компиляцией функций, которые формально возвращают значение, но фактически ведут себя как функции, возвращающие Unit (т.е. ничего не возвращают). Рассмотрим пример: func unitInFact() bool { for {} // Бесконечный…
Вообще такой ход конем вызван тем, что в Go нет типа суммы. А на практике может пригодиться функция с бесконечным циклом, которая возвращает ошибку, исключений же нет. Поэтому дизайнеры языка пошли на этот компромисс, чтобы не усложнять семантику, а значит замедлять время компиляции.

Причем это не единственный хак в компиляторе Go. Один из самых знаменитых, это запрет открытия фигурной скобки на новой строке при объявлении структуры или функции. За счет этого разработчики языка убили двух зайцев: единый формат (привет С/С++) и упростили парсер
🔥62
Всегда было интересно — откуда такая уверенность в гарантиях Rust? Как компилятор может быть абсолютно уверен, что в программе не будет:

- Гонок данных
- Use-after-free
- Висячих ссылок

Тесты проверяют лишь конечное число случаев. Математические доказательства — вот что дает стопроцентную гарантию. И здесь на сцену выходит Coq — мощный инструмент формальной верификации. Coq позволяет:

- Формально описывать поведение программ
- Доказывать их свойства математическими методами
- Верифицировать всё — от алгоритмов до целых языков программирования

Простой пример — доказательство чётности числа:


Definition is_even (n : nat) := exists k, n = 2 * k.

Lemma two_is_even : is_even 2.
Proof. exists 1. reflexivity. Qed.


Но нас интересует не арифметика, а безопасность памяти в Rust. Проект RustBelt формально верифицирует ключевые механизмы Rust:

- Систему владения (ownership)
- Заимствование (borrowing)
- Времена жизни (lifetimes)

Рассмотрим, как в Coq моделируются правила заимствования. Сначала определим типы:


Inductive permission :=
| Unique (* Эксклюзивный доступ (аналог &mut T) *)
| SharedReadWrite (* Чтение и запись *)
| SharedReadOnly (* Только чтение (аналог &T) *)
| Disabled. (* Доступ запрещен *)

Inductive access_kind :=
| AccessRead (* Запрос на чтение *)
| AccessWrite. (* Запрос на запись *)


Затем реализуем логику проверки прав:


Definition grants (perm: permission) (access: access_kind) : bool :=
match perm, access with
| Disabled, _ => false (* Все операции запрещены *)
| SharedReadOnly, AccessWrite => false (* Запись запрещена *)
| _, _ => true (* Во всех остальных случаях разрешено *)
end.


В этой модели:

- SharedReadOnly соответствует &T в Rust
- Unique соответствует &mut T
- Disabled — ситуация, когда доступ к данным невозможен

Интересно? Недавно вышел курс лекций на русском по основам Coq от блокчейн-инженеров. Да, в криптовалютных проектах Coq тоже используют — для верификации смарт-контрактов
🔥42