Глубинный котер
94 subscribers
60 photos
7 videos
4 files
70 links
Download Telegram
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
Болото унаследованных интеграций


Смешно до боли 💀
Please open Telegram to view this post
VIEW IN TELEGRAM
😁51
Представьте ситуацию: вы пишете на языке, где нельзя объявлять функции с именами (чистое лямбда-исчисление), или работаете с ограничениями вроде тех, что есть в Haskell. Как сделать рекурсивную функцию?

Обычный факториал выглядит просто:


fun factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)


Но что если мы не можем ссылаться на factorial внутри его же определения? Вот тут-то и нужен Y-комбинатор, который позволяет функции вызывать саму себя, не имея имени:

1. Мы передаём функцию в саму себя как аргумент
2. Y-комбинатор организует "бесконечную" подачу функции в саму себя
3. Рекурсия работает, хотя явного вызова по имени нет

Вот как я реализовал это в Kotlin (люблю его за выразительность):


typealias Func<A> = (A) -> A

fun <A> yCombinator(f: (Func<A>) -> Func<A>): Func<A> {
return { x -> f(yCombinator(f))(x) } // Магия!
}

val factorial: Func<Int> = yCombinator { recurse -> {
n -> if (n <= 1) 1 else n * recurse(n - 1)
} // Лямбда внутри лямбды
}

println(factorial(5)) // 120


Ключевые идеи реализации:

1. yCombinator принимает функцию, которая хочет быть рекурсивной
2. Он подсовывает ей саму себя в качестве аргумента recurse
3. Когда функция вызывает recurse — она неявно вызывает саму себя

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

Это как обнаружить, что ваш любимый конструктор Lego на самом деле состоит из ещё более фундаментальных деталей 😳
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔62👀1
На недавнем DDD Europe выступил с докладом Gregor Hohpe — автор книги Enterprise Integration Patterns (да, той самой!). Доклад посвящен инженерии платформ, и красной нитью в нём проходит мысль simple ≠ easy. Ниже приведены основные идеи доклада.

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

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

Ошибка Cadillac Cimarron: Компания попыталась выпустить новый автомобиль на базе существующего, лишь дополнив его несколькими функциями. Однако игнорировала необходимость построения полноценной платформы. Результат оказался провальным, так как машина не обладала уникальными характеристиками, выделявшими её среди конкурентов.

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

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

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

Абстрагирование может приводить к ошибочному восприятию реальной проблемы. Основная цель абстракций — выражение сложности, а не упрощение. Ориентация на специфику предметной области делает систему интуитивно понятной. Разработчики должны предлагать уникальные сервисы, основываясь на своей экспертизе в предметной области.
4
В то время как в компаниях уже на проде используют Kotlin MP, Apple недавно выложили Swift SDK под Android — можно писать нативное приложение сразу под две платформы.

Похоже, что идея KMP стрельнула и для улучшения конкурентоспособности экосистемы Swift слепили SDK под Android. Из интересного, что используется интероп Swift-Java (третий по счёту в Swift?)

Причем у Swift есть все шансы потеснить Kotlin в этой нише хотя бы за счет легкости DX и более дружелюбного синтаксиса.

С другой стороны, на Kotlin много бэкенда, да и KMP на проде уже крутится.

Наблюдаем за этой гонкой 🍿
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Because request parameters and responses are handled inside route lambdas, the plugin cannot infer detailed request/response schemas automatically. To generate a complete and useful specification, you can use annotations.


Cтыдно, очень стыдно
🌚4
Антропик посягнул на святое 🤱

Буквально сегодня выложили ролик с демонстрацией того, как можно использовать Клод для модернизации старого доброго кода на Коболе. В качестве примера они взяли репозиторий AWS Mainframe Modernization (!!!) — там есть система управления кредитными картами.

А я уже хотел купить курс по Коболу и попасть в золотую КОБОЛу 🙈
Please open Telegram to view this post
VIEW IN TELEGRAM
4🤣1
Только ленивый не позлорадствовал над вчерашним падением Cloudflare, вызванным небезопасным вызовом unwrap у монады Result. Но этот инцидент, как и многие до него, — ещё одно подтверждение тому, что первичны процессы, а технологии как таковые вторичны 🥲
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Диалоги Платона ATDD
5
На дворе почти 26 год, а агент от ЖБ отказывается работать с одним из языков ООН
😁4
Знакомая ситуация? Моделируем пользователя, добавляем поле email и флаг isEmailConfirmed. Кажется логичным, но на практике рождает хрупкий код:


class Email(val value: String)

data class User(
val email: Email,
val isEmailConfirmed: Boolean
)


У такого подхода есть ряд проблем:

- Тип Email ничего не знает о своём статусе
- Флаг живёт отдельно, можно случайно отправить письмо на неподтверждённый адрес
- Валидация размазывается по коду

Решение. Вместо флага — два разных типа, выражающие жизненный цикл:


sealed class Email

class Confirmed(val value: String) : Email()
class Unconfirmed(val value: String) : Email()

data class User(val email: Email) // email может быть в двух состояниях


Компилятор теперь заставляет обрабатывать все случаи:


fun sendNewsletter(email: Email): Result = when(email) {
is Confirmed -> sendTo(email)
is Unconfirmed -> tryConfirm(email)
// Компилятор проверит, что все ветки покрыты
}


Система типов становится нашим надёжным союзником: она позволяет явно описать бизнес-правила предметной области непосредственно в структуре данных.

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

Это ещё один приемчик для снижения когнитивной нагрузки на разработчика, что, безусловно, повышает эффективность нашей работы 👽
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥63👍2👎1