Глубинный котер
94 subscribers
60 photos
7 videos
4 files
70 links
Download Telegram
Есть 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
Маркетинг в опен-сурс, который мы заслужили

github.com/mark3labs/mcp-go
😁3
Команда Go разрабатывает новый алгоритм сборки мусора — Green Tea 🍵, который радикально меняет подход к разметке памяти. Вместо обработки отдельных объектов он работает с целыми блоками по 8 КБ, но только для маленьких объектов (≤512 байт). Уже есть экспериментальная реализация, и в Go 1.25 её можно будет протестировать.

Сейчас Go использует классический алгоритм трёхцветной разметки, который:

- Не учитывает расположение объектов в памяти, сканирует указатели вразнобой
- Тратит 85% времени на цикл сканирования, из которых 35% CPU-циклов уходят просто на чтение памяти (из-за плохой локальности)
- Плохо масштабируется на системах с десятками ядер и сложной топологией памяти (NUMA, HBM)

Вместо работы с отдельными объектами, Green Tea оперирует целыми 8 КБ (для уменьшения промахов L1/L2) блоками называемыми интервалами, но только для маленьких объектов. Такой фокус на маленьких объектах обусловлен тем, что сборка их новым алгоритмом дает наибольший выигрыш:

- Дешевле сканировать блоками, чем по одному
- Они чаще создают фрагментацию и нагрузку на GC

Крупные объекты обрабатываются старым алгоритмом, чтобы не усложнять логику. Новый алгоритм Green Tea работает следующим образом:

- Каждый 8 КБ интервал хранит битовую маску (серый/чёрный биты) для своих объектов
- Если в блоке появляется новый указатель, он помечается серым, а весь блок ставится в очередь на сканирование
- Когда блок извлекается из очереди, все помеченные объекты в нём сканируются разом

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

Бенчи в целом хорошие:

- CPU-затраты GC снизились на 10–50% в CPU-bound сценариях
- В 2 раза меньше промахов кэша (L1/L2)
- Тесты компилятора Go показывают незначительный регресс (~0.5%)

Oригинал
3🔥2
На мой взгляд, одна из самых красивых идей в программировании — соответствие Карри-Ховарда. Это концепция, которая связывает программирование с логикой, показывая, что программы можно рассматривать как доказательства логических утверждений.

Обычно мы думаем о программах как о способах обработки данных: чисел, списков, функций. Но что, если взглянуть на это с другой стороны? Что, если программа — это доказательство того, что ее тип обитаем? То есть, если программа компилируется, она доказывает, что ее тип не пуст.

Например, рассмотрим простую функцию, которая создает пару:


let pair x y = (x, y)


Тип этой функции:


val pair : 'a -> 'b -> 'a * 'b


Теперь давайте переведем это на язык логики. Тип 'a * 'b соответствует логической формуле A /\ B, где /\ — это конъюнкция (логическое "и"). Таким образом, функция pair доказывает, что если у нас есть доказательства для A и B, мы можем создать доказательство для A /\ B.

Процесс вычисления программы можно рассматривать как упрощение доказательства. Например, рассмотрим функцию, которая меняет местами элементы пары:


let swap (x, y) = (y, x)


Тип этой функции:


val swap : 'a * 'b -> 'b * 'a


В логике это соответствует доказательству того, что если у нас есть доказательство для A /\ B, мы можем получить доказательство для B /\ A. Процесс вычисления функции swap можно рассматривать как упрощение этого доказательства.

Если тема показалось интересной, то в одном из блоков курса cs3110 ждут упражнения на OCaml 🐫
3
Недавно я наткнулся на интересную поделку — Dockerfile Kotlin DSL. Да, вы не ослышались, это DSL на Kotlin для написания Dockerfile. Сомнительно, но представьте, как это удобно: вместо того чтобы писать Dockerfile в текстовом формате, вы можете использовать Kotlin для создания Dockerfile прямо в вашем Gradle-конфигурационном файле. Вот как выглядит простенький DSL для JVM-приложения:


dockerfile {
from("openjdk:21-jdk-slim")
workdir("/app")

+"Копируем JAR-файл в Docker-образ"
copy {
source = "app.jar"
destination = "/app/app.jar"
}

cmd("java", "-jar", "app.jar")
}


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

Главная мотивация автора — избежать дублирования Dockerfile для разных сред (staging/production). Вместо того чтобы создавать отдельные Dockerfile для каждой среды, вы можете использовать один DSL-файл, который будет генерировать Dockerfile в зависимости от переменных окружения. Например, под разные окружения можно загрузить разные артефакты в образ:


+"Загружаем активы"
val assetPath =
when (System.getenv("MODE")) {
"local" -> "/first_100_assets.zip"
"staging" -> "/compressed_assets.zip"
else -> "/full_assets.zip"
}


Это может быть полезно, если вы не хотите засорять ваше приложение if-условиями и фича-флагами, и уже используете Gradle для сборки. Хотя на моей практике был всегда противоположный подоход через if-условия и фича-флаги.

Для меня, Dockerfile Kotlin DSL — это еще один пример того, насколько мощным может быть Kotlin для создания DSL. Он позволяет писать Dockerfile как код, что делает его более гибким и удобным для управления. И это не единственный пример. Есть ещё Exposed — ORM для Kotlin от JetBrains. В ней также удалось минимизировать семантический разрыв между DSL и SQL. Сравните SQL-запрос:


select * from user
where vibe = 'cheel'
and status = 'active'


с его аналогом на Exposed DSL:


User
.select { (User.vibe eq "cheel")
and (User.status eq "active") }


И это потрясающе. Восхищаюсь командой разработки языка, потому что до этого мир видел подобные DSL на динамически типизированных языках, как Ruby и Groovy. Но чтобы на статически типизированном и без необходимости получать PhD по теории категорий, это впервые 🔥
🔥3👍1👎1💩1
Мимо меня чуть не прошла одна из главных новостей этого года — поддержка асинхронного ввода-вывода через io_uring в PostgreSQL 18. Напомню, что io_uring — это новая подсистема асинхронного ввода-вывода, разработанная для Linux, позволяющая асинхронно управлять операциями чтения-записи на диск. До недавнего времени операции ввода-вывода с файловой системой были синхронными и блокирующими.

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

Основные компоненты PostgreSQL, затронутые изменениями:

- Sequential Scans: Последовательные сканирования таблиц теперь выполняются быстрее, поскольку данные считываются параллельно и обработка выполняется независимо друг от друга.

- Bitmap Heap Scans: Этот метод поиска данных стал более эффективным благодаря параллельному доступу к индексированным данным.

- Vacuum Operations: Очистка таблиц от устаревших записей также получила ускорение благодаря возможности параллельного исполнения операций удаления.

Эти улучшения были достигнуты именно за счёт внедрения асинхронного механизма, основанного на io_uring. Тестирование показало впечатляющие результаты: улучшение производительности вплоть до 2–3 раз 🔥

Главная причина прироста производительности заключается в устранении задержки при операциях ввода-вывода. Когда PostgreSQL отправляет запрос на чтение файла, процесс блокируется до завершения операции. Но с использованием io_uring база данных продолжает обрабатывать другие запросы, пока идёт операция ввода-вывода. Это значит, что даже самые тяжелые нагрузки на базу данных обрабатываются эффективнее.

Release notes
🔥11
Friendly error message наше всё. Да, JetBrain?
🤡3👍2
This media is not supported in your browser
VIEW IN TELEGRAM
На днях наткнулся на апрельский маркетинговый ролик от JetBrains, где собрали максимум хайпа: MCP, AI Coding Assistant, IoT приправленный Kotlin, только RAG не хватало. В видео автор вместе со зрителями пишет наык для Алисы MCP-сервер для управления лампочкой через LLM. Понятно, что ролик рекламный, но есть пара моментов, которые меня зацепили, о которых расскажу ниже.

Похоже, JetBrains первыми среди разработчиков IDE догадались сделать интерактивную работу ассистента: LLM сам пишет код, пытается его запустить, пока тесты не станут зелёными. Насладиться можно на отрывке видео.

Похожая фича появилась недавно у Jules от Google. Хотя такая работа ассистента кажется очевидной, реализовать её начали только сейчас — раньше разработчики самостоятельно создавали такие тулзы, как тут.

MCP только набирает популярность, а в JVM-экосистеме уже всё готово:

- Официальные SDK для MCP, в отличие от модных Rust и Go, для которых ещё нет SDK.
- Spring AI — для упрощения работы со всем циклом разработки LLM-приложений.
- Официальный плагин MCP в IntelliJ для обеспечения связи между LLM и средой разработки.

В целом здорово, что развивается вся экосистема, а разработчики языка не ограничиваются одним лишь туториалом, как их коллеги. Хотя в том же Go есть родная версия LangChain, похожего проекта для других языков кроме Python не встречал.
👍42
Чем больше я занимаюсь промт-инженерией, тем сильнее чувство, что я занимаюсь каким-то НЛП 🏌️

Но пока самый главный вывод: машина напишет для машины промт качественнее чем человек. Но как это сделать?

1.1 Берём какой-нибудь DeepSeek и описываем нашу проблему как человеку. Можно до этого воспользоваться техникой якорение из НЛП, сказав LLM, что она разработчик, например, в стартапе. Главное не жалеть токенов на первый промт.

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

2.1 Валидируем план от LLM, просим внести правки. Главная задача выстроить роад-мап нашей фичи верхнеуровнево.

2.2 Пользуемся техникой мета-модели из НЛП, делая фокус на первом этапе нашего плана. Просим модель написать план реализации первого этапа, валидируем при необходимости.

3. Просим написать промт по самому первому шагу нашего плана для Cursor.

Звучит бредово? Возможно, но зато работает. В комменте к посту приложил пример такого промта от LLM 🍾
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍3
Глубинный котер
Админ би лайк
Админ спустился с дерева
6
Глубинный котер
Чем больше я занимаюсь промт-инженерией, тем сильнее чувство, что я занимаюсь каким-то НЛП 🏌️ Но пока самый главный вывод: машина напишет для машины промт качественнее чем человек. Но как это сделать? 1.1 Берём какой-нибудь DeepSeek и описываем нашу проблему…
This media is not supported in your browser
VIEW IN TELEGRAM
В предыдущем посте я рассказал о продвинутых приёмах промт-инженерии, которые применил в своём новом проекте ast2llm-go — инструменте, который делает общение с AI-ассистентами умнее и эффективнее. Какую проблему я пытаюсь решить?

Часто случалось, что при запросе к Copilot/Cursor забываешь передать контекст проекта, и в итоге получаешь:

- Несуществующие методы
- Ошибки в сигнатурах
- Бесконечные «Уточните структуру проекта...»

Это раздражало, особенно в enterprise-проектах, где кодовая база большая, и для добавления фичи приходится затрагивать кучу файлов. Решение напрашивалось само — автоматически собирать контекст из исходников.

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

1. Модель понимает, что ей не хватает информации о какой-то структуре, используемой в коде.
2. LLM запрашивает у MCP-сервера подробности об импортируемых функциях, структурах и т. д.
3. MCP-сервер парсит проект, проходит по AST и собирает все неизвестные для LLM элементы в текстовый ответ.
4. Модель получает данные и меньше галлюцинирует.

В гифке к посту показан простой пример использования этого MCP-сервера в Cursor. В итоге LLM получает ответ вроде такого и сразу пишет правильный код:


Used Items From Other Packages:
Struct: testme/dto.MyDTO
Fields:
- Foo string
- Bar int


Пока проект в пилоте — я его обкатываю и не уверен на 100% в выбранном подходе. В его защиту могу сказать, что тот же repomix работает довольно топорно: он просто вставляет весь проект в контекст LLM, что не всегда возможно, да и замусоренный контекст плохо себя показывает на практике. В Cursor есть встроенный grep, и агент может искать нужный код, но это происходит медленно, да и сам редактор по понятным причинам не всегда подходит для enterprise.

Почему Go? Потому что у него простой синтаксис, и такой инструмент было проще всего реализовать. Плюс это мой основной рабочий язык сейчас. В планах — отдельные MCP-серверы для других экосистем, так что если интересно, присоединяйтесь к ast2llm, пишите в личку. Будем вместе двигать технологии обогащения контекста для LLM 🍪
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥52
TL;DR: ФП продолжает идти в массы, а Kotlin становится все лучше.

Посмотрел доклад с Kotlin Conf 2025 о новой фиче — Rich Errors — которая сделает обработку ошибок удобной, без зоопарка комбинаторов или нелинейной логики с исключениями. Если фичу выкатят, Kotlin станет первым популярным языком после Go Rust, где ошибки как значения встроены на уровне синтаксиса.

Сейчас в Kotlin есть несколько способов работать с ошибками:

1. Исключения — но они не типобезопасны и неявны
2. Nullable-типы (T?) — простые, но не дают информации о причине ошибки
3. Result<T> / Either<L, R> — типобезопасно, но требует fold, mapLeft и прочих комбинаторов

Вот типичный пример с Result:


fun getUser(): Result<User> {
val user = fetchUser().getOrElse { return Result.failure(it) }
val parsedUser = user.parseUser().getOrElse { return Result.failure(it) }
return Result.success(parsedUser)
}

fun usage() {
getUser()
.onSuccess { user -> println(user.name) }
.onFailure { error ->
when (error) {
is NetworkException -> println("Failed to fetch user")
is ParsingException -> println("Failed to parse user")
else -> println("Unhandled error")
}
}
}


Проблема в том, что:

- Много boilerplate — постоянно приходится вызывать getOrElse, onSuccess, onFailure.
- Нет exhaustiveness checking — если добавим новый тип ошибки, компилятор не подскажет, что её нужно обработать.
- Неудобные цепочки вызовов — в отличие от nullable-типов, где есть ?., тут приходится вручную разворачивать Result.

С новыми rich error теперь можно помечать классы как error, и они автоматически становятся частью системы типов:


error class NetworkError(val code: Int)


Функция может явно указать, какие ошибки она возвращает:


fun fetchUser(): User | NetworkError | UserNotFoundError {
...
}


Теперь можно использовать when с exhaustive checking (компилятор проверит, что все случаи обработаны):


fun loadUserData() {
val result = fetchUser()
when (result) {
is User -> show(result)
is NetworkError -> showError("Network issue (${result.code}). Try again.")
is UserNotFoundError -> showError("User not found. Check your credentials.")
// Компилятор заставит обработать все варианты!
}
}


Можно писать цепочки, как с nullable:


fun getUser(): User | NetworkError | ParsingError {
return fetchUser() // User | NetworkError
?.parseUser() // User | ParsingError
}


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

Обещают KEEP по фиче этим летом и добавления в Koltin 2.4. Ждёмс 🙈
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4
В правилах для код-агента от Его Бугаенко наткнулся на интересную строчку:

Tests may not use setUp() or tearDown() idioms.


Мне, как человеку, выросшему на pytest с его удобными фикстурами, такое ограничение (отсутствие подготовки/очистки данных) показалось странным. Однако, пообщавшись с более опытными коллегами, я узнал, что существует альтернативная точка зрения.

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

Теперь отсуствие такой искуственной изоляции на текущем проекте выглядит оправдано 🙂
Please open Telegram to view this post
VIEW IN TELEGRAM
2😁1🤔1
Разбирая реализации MCP-серверов в разных экосистемах, я был впечатлён разницей в объёме кода при схожих характеристиках (статическая типизация, асинхронность, GC). Go-версия занимает 400+ строк, тогда как Kotlin-решение укладывается в 100 строк— и всё благодаря выразительности языка и богатой стандартной библиотеке Kotlin.

В Go-реализации все механизмы синхронизации прописаны явно:


type stdioSession struct {
pendingRequests map[int64]chan *samplingResponse // Хэш-таблица запросов
pendingMu sync.RWMutex // Мьютекс для доступа
requestID atomic.Int64 // Атомарный счётчик
}


Каждое изменение shared-состояния требует явной блокировки:


s.pendingMu.Lock()
s.pendingRequests[id] = responseChan
s.pendingMu.Unlock()


В Kotlin аналогичная функциональность достигается проще:


private val pendingRequests = ConcurrentHashMap<Long, Channel<SamplingResponse>>()
private val requestId = AtomicLong(0)


Доступ к ConcurrentHashMap автоматически потокобезопасен:


pendingRequests[id] = responseChannel // Не нужны явные блокировки


Запуск асинхронной задачи в Go с коммуникацией через канал:


go func() {
response := s.server.HandleMessage(ctx, rawMessage)
if err := s.writeResponse(response, writer); err != nil {
s.errLogger.Printf("Error: %v", err)
}
}()


Эквивалентная логика в Kotlin более лаконична:


scope.launch {
val response = server.handleMessage(rawMessage)
writeChannel.send(response) // Потокобезопасная отправка
}


Исходники:

- на Kotlin
- на Go
4🤔2
После coq продолжаю знакомиться с чудесами Computer Science. На этот раз познакомился со статьей Checking Polynomial Time Complexity with Types за авторством Patrick Baillot, которая посвящена верификации полиномиальной временной сложности программ с помощью систем типов.

В статье используется модифицированная система типов на основе Light Affine Logic, где модальности ограничивают дублирование данных и глубину рекурсии. Это гарантирует, что типизированные программы выполняются за полиномиальное время.

Примеры модельностей:

- ! (экспоненциал) контролирует, сколько раз значение может быть использовано.
- § (мягкий экспоненциал) ограничивает глубину рекурсивных вызовов.

Например, рекурсивные функции проверяются на соответствие линейным и стратифицированным правилам, исключая неконтролируемый рост вычислений. Тип N → §N для функции означает, что каждый рекурсивный вызов уменьшает «глубину» страта, обеспечивая полиномиальную остановку.

В коде такой подход можно проиллюстрировать так:


-- Тип с модальностью §, ограничивающей рекурсию
data § a = § a

-- Функция сложения с контролем глубины
add :: §Int -> §Int -> §Int
add (§x) (§y) = §(x + y)

-- Рекурсивная функция с ограничением через типы
factorial :: Int -> §Int
factorial 0 = §1
factorial n = §(n * un§ (factorial (n-1))) -- un§ "снимает" модальность


Здесь модальность § гарантирует, что глубина рекурсии factorial ограничена, а время выполнения остается полиномиальным.
2