Вшивый о бане (монады всякие)

Jun 02, 2023 19:03

(Этот же текст с нормальным форматированием и хайлайтингом: https://gist.github.com/akuklev/53cf44920f1ad4b0edd7d56d7ce312da)

Вот кто о чём, а вшивый завсегда о бане. Вот начал я активно и очень много пользоваться Котлином - это язык программирования такой, если вдруг кто не знает. Его Андрей Бреслав сделал, а сейчас им кроме меня ещё больше 5 миллионов людей пользуется, хороший в общем язык. И главное синтаксически очень гармоничный, и поэтому сразу шаловливые мои ручки тянутся его развивать.

Одна из постоянных тем в программировании - corner case handling. Есть какой-то happy path, алгоритм или формула, которые работают в случае общего положения. При этом есть ситуации, которые нужно обработать отдельно. В математической формуле это скажем такие значения переменных, при которых случается деление на нуль. В пошаговом алгоритме - ситуации, когда какой-то компонент не готов/сломался или ещё какая-то ошибка. Часто логика предметной области или конкретной задачи требует особой обработки какого-то специфического случая.

И вот happy path чаще всего записывается удобно и лаконично, его описание удобно читать.
А вот corner cases имеют тенденцию к комбинаторному взрыву, т.к. могут зачастую встречаться в разных комбинациях, поэтому код обработки этих ситуаций имеет тенденцию быть уродливым, нечитаемым и подверженным ошибкам - не ошибкам кодинга, а ошибкой думинга. В смысле не «хотел чтобы программа работала так-то, но неправильно объяснил компьютеру», а «не продумал как следует, что в такой ситуации нужно делать, или вообще не подумал что такая ситуация может случиться». Это те самые ошибки, где зачастую оказываются бессильны формальные математические методы, и уж точно не поможет никакой ИИ. В лучшем случае, формальные методы помогут выявить такие ситуации, а ИИ придумать реалистичный сценарий, в котором они возникают. Но вот придумать что в этой ситуации делать - это часть постановки задачи, это работа программиста (программиста-аналитика), понимающего предметную область и идеологический смысл решаемой задачи.

Так вот, одна из главных задач дизайнеров языка программирования - сделать так, чтобы добавление обработки corner cases не превращала исходную реализацию happy path в нечитаемую стену текста по форме и клубок спагетти по содержанию.

Для этого крайне желательно иметь возможность обрабатывать corner cases как “на месте”, так и “в сноске” и иметь возможность свободно переключаться между этими подходами.

Вариант “в сноске” известен как обработка исключений, вариант “на месте” как null handling в императивных языках программирования, а в функциональных языках программирования - как работа внутри декартовой монады ResultOrFailure.

В Котлине синтаксически весьма приятно устроен null handling и предусмотрено удобное переключение между ним и обработкой исключений - `runCatching {code}` превращает исключения в null, а `expr!!` превращает null назад в исключение. Однако в той форме, как это реализовано сейчас, !! не является обратным к runCatching, т.е. преобразование исключения в null стирает всю информацию про исключение. Чтобы это исправить, необходимо и достаточно добавить в язык custom null objects: https://youtrack.jetbrains.com/issue/KT-59047

Таким образом мы под шумок расширяем удобный синтаксис для работы с nullable values на любые булевы декартовы монады. Ну раз такое дело, так давайте же уже сделаем из этого синтаксиса полноценную do-notation для таких монад, благо это предложение в пропозал строчек: https://youtrack.jetbrains.com/issue/KT-59046, причём идеально укладывающийся в логику синтаксиса языка.

С этим расширением мы можем взять формулу `arctan (a / b)`, не работающую для b = 0, и прям на месте обработать этот исключительный случай:
`arctan ?(a /? b) ?: 90°`

Тут `/?` это как деление, только в случае деления на ноль вернёт null. Вопрос перед скобкой сделает применение функции arctan пропускающим этот null насквозь, а оператор `?:` заменит этот null на 90°.

А обработка “в сноске” тут выглядит как

try {
arctan (a / b)
} catch (e: ArithmeticException) {
90°
}

А ещё в Котлине есть метки, использование которых позволило бы сделать ещё и вот так:

try {
180° - p1@{arctan (a / b)}
} catch@p1 (e: ArithmeticException) {
90°
}

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

* * *

Но если уж соприкоснулся с монадами, то на обработке corner cases остановиться невозможно. Очень руки чешутся по аналогии с синтаксисом для обработки null'ов сделать полноценную do-notation, ведь она тоже очень красиво укладывается в логику языка: https://youtrack.jetbrains.com/issue/KT-59052

Это ожидаемо в Котлин не берут, потому что он не Скала (и слава богу, наверное).

А теперь смотрите внимательно. Выше мы видели двойственность - можно обрабатывать corner cases при помощи null-object handling а можно при помощи exception handling, и между ними можно переключаться. За этим математически вообще красивая штука стоит, называется сопряженная пара монады и комонады.

Так вот, do-notation в Котлин не пускают, а сопряженная с ним штука там цветёт и здравствует, и называется type-safe builder notation, и считается жемчужиной языка. Type-safe builder устроен так что мы вдоль всего кода тащим с собой один конкретный мутабельный объект и периодически дёргаем его методы. Например, мы можем вот так сгенерировать список:

val x = listOf('b', 'c')

val y = buildList() {
add('a')
add(*x)
repeat(3) {
add('d')
}
add('e')
}

println(y) // [a, b, c, d, d, d, e]

Внутри скоупа buildList() у нас к имеющимся операторам-с-побочными-эффектами (например, throw) добавляются ещё все методы специального мутабельного объекта под названием листбилдер. Ну и мы просто выполняем какой-то код, между делом потихоньку наполняем этот самый билдер, а когда блок закончится, на выходе у нас будет обычный (в идеале, иммутабельный) список.

Этот самый “избранный мутабельный объект, методы-мутаторы которого в текущем скоупе используются как операторы-с-побочными-эффектами” на математическом языке называется комонадой.

Раз объект-контекст type-safe builder'ов в Котлине мутабельный, то Type-safe builder'ы получаются последовательные (Sequential, или Serial), двойственные к do-нотации для монадических декораторов.

А что будет двойственным к do-нотации для аппликативно-функториальных декораторов? Интуитивно кажется, что это должен быть вариант type-safe builder'ов, где объект-контекст не неограниченно-мутабельный, а аккумулятивный. Типа как в примере с `buildList()`.
Previous post Next post
Up