openlang

Result сообщает о проваленной проверке, opaque доказывает, что она была

Все хвалят Gleam за ошибки как значения, и справедливо, Result прекрасен. Но в моделировании данных я сильнее опираюсь на opaque.

Запись pub opaque type Email { Email(String) } означает, что другие модули видят тип, но не могут вызвать конструктор. Получить Email можно только через то, что экспортирует модуль, например fn parse(raw: String) -> Result(Email, EmailError). Так что Result появляется один раз, на входе, а весь остальной код принимает Email и больше ничего не проверяет.

Без opaque тот же Result на входе остаётся, а гарантии дальше нет, потому что кто угодно напишет Email("lol") и обойдёт проверку. Значение ошибки говорит, что проверка не прошла. Opaque-тип говорит, что проверка была.

Elm приходит к тому же, просто не экспортируя конструктор из модуля, та же идея с меньшими церемониями. Мне нравится, что в Gleam это написано прямо на типе, там, где читаешь определение.

Разговор продолжается 4

Следуя доводам
  1. Ссылка на ответ

    Гарантия уже, чем кажется. Haskell получает тот же эффект, не включая конструктор в список экспорта, module Email (Email, parse) where, и предел у него тот же. Инвариант выполняется вне модуля при условии, что его соблюдает каждая функция внутри. Если позже кто-то добавит в этот модуль вспомогательную функцию, которая собирает Email прямо из сырого текста, тип по-прежнему скажет Email, а проверки не было. Скрытый конструктор ограничивает объём кода, который нужно прочитать, модулем, а не всей программой. Это всё равно заметное сокращение.

  2. Ссылка на ответ

    «Прочитать модуль» звучит скромнее, чем есть на деле. Модуль в Gleam это один файл, так что код, способный сломать инвариант, буквально один файл, а такое правило на ревью люди действительно соблюдают.

  3. Ссылка на ответ

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

    Temporal их разделяет. Temporal.Instant для точки во времени, Temporal.PlainDate для календарной даты без пояса, Temporal.ZonedDateTime, когда действительно нужно и то и другое. День рождения это PlainDate. Сохраните его как Date в местную полночь, и при чтении из пояса западнее день рождения съедет на день раньше.

    Моделирование делает тип. Тот же урок, что и с Email у liesl, только способов ошибиться больше.

  4. Ссылка на ответ

    Честно говоря, приватный конструктор плюс именованный с проверкой это самый дешёвый выигрыш от типов в коде, набитом шаблонами. В PHP делаете конструктор private, добавляете public static function fromString(string $raw): self с проверкой и помечаете свойство readonly (это уже с PHP 8.1). Теперь параметр с типом Email $to действительно что-то значит.

    Подвох в том, что старые шаблоны везде гоняют массивы, так что объект-значение помогает только с того места, где вы преобразовали данные. Начинайте с краёв, куда приходят данные запроса, а шаблоны не трогайте. Когда-нибудь типизируются, а может, и нет.

    И да, ReflectionClass::newInstanceWithoutConstructor() может всё это обойти. Случайно так никто не делает.

Другие стороны того же вопроса

  1. Нужен ли пустой палитре отдельный вариант?

    В редакторе образцов пустой список цветов означает две разные вещи, и Save должен их различать. Корректность цветов здесь ни…