Разработчики из Bellroy нашли способ ускорить компиляцию одного из своих внутренних пакетов на 10%. Они заменили тысячи вызовов TemplateHaskell одним type-level трюком и получили проверяемый на этапе типов конструктор непустых строк NonEmptyText.
Раньше код выглядел так: $$(NonEmptyText.makeTH "hello"). Если передать пустую строку, ошибка вылезала только во время выполнения сплайса. Новая версия — просто NonEmptyText.make "hello". А если написать "", GHC выдаст ошибку типа прямо при компиляции с сообщением "Expected a non-empty string".
Всё работает благодаря расширению GHC 9.10 — RequiredTypeArguments. Оно позволяет передать строковый литерал на уровне типов прямо как аргумент функции. Дальше в игру вступает класс IsNonEmptySymbol с overlapping инстансами: для пустой строки — кастомная ошибка через Unsatisfiable, для любой другой — успех. Надо только не забыть включить UndecidableInstances.
Этот подход срезал около 10% времени сборки пакета bellroy-data, в котором хранятся данные про перевозчиков, налоги, товары и т.д. И таких TH-сплайсов там были тысячи.
Тот же приём можно переиспользовать для проверки, что Natural положительный, или вообще для любого типа, для которого можно придумать type-level предикат. Например, так сделали конструктор валидных имён таблиц DynamoDB — TableName. Имя должно соответствовать regex /^[a-zA-Z_.-]{3,255}$/. Вся проверка — на уровне типов: длина от 3 до 255, каждый символ проходит через IsValidTableChar. Если имя не подходит, GHC ругается кастомной ошибкой.
Правда, есть нюанс: GHC по умолчанию ограничивает 20 шагов редукции type family, так что длинные строки так просто не распарсить. И выражать сложные алгоритмы в терминах type family всё ещё неудобно — нет нормальных let и case. Но общий тренд к Dependent Haskell делает такие трюки всё более доступными с каждым релизом GHC.