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. 고유 링크

    타입은 하나, 뜻은 둘, 어느 getter를 부르느냐로 갈린다. 수십 년째 JavaScript의 Date가 그렇다. 담는 건 순간 하나, 에포크 이후 밀리초 수다. 그런데 메서드 절반은 그게 기계가 마침 있는 시간대의 벽시계 시각인 척한다.

    Temporal은 이걸 쪼갠다. 시점은 Temporal.Instant, 시간대 없는 달력 날짜는 Temporal.PlainDate, 정말 둘 다 필요할 때는 Temporal.ZonedDateTime. 생일은 PlainDate다. 생일을 현지 자정의 Date로 저장하면, 더 서쪽 시간대에서 읽는 순간 생일이 하루 전날이 된다.

    모델링은 타입이 한다. liesl의 Email과 같은 교훈인데, 틀릴 방법이 더 많을 뿐이다.

  3. 고유 링크

    솔직히 private 생성자에 검증하는 이름 붙은 생성자를 더하는 게, 템플릿 많은 코드에서 타입으로 얻을 수 있는 제일 싼 이득이에요. PHP라면 생성자를 private으로 만들고, 검증하는 public static function fromString(string $raw): self를 추가하고, 속성에 readonly를 붙여요(이건 PHP 8.1부터예요). 그러면 Email $to라고 타입을 단 매개변수가 진짜 의미를 갖게 돼요.

    문제는 레거시 템플릿이 어디서나 배열을 주고받는다는 거라서, 값 객체는 변환한 지점부터만 도움이 돼요. 요청 데이터가 들어오는 입구부터 시작하고 템플릿은 그냥 두세요. 언젠가 타입이 붙든지 말든지요.

    아, 그리고 ReflectionClass::newInstanceWithoutConstructor()로 이걸 전부 건너뛸 수는 있어요. 실수로 그러는 사람은 없지만요.

같은 질문의 다른 갈래

  1. 빈 팔레트도 이미 하나의 결정이라면

    스와치 편집기에서 빈 색 목록은 서로 다른 두 가지 뜻일 수 있어서, Save할 때 둘을 구분할 수 있으면 좋겠어요. 색이 유효한지는 여기서 다루지 않아요. 편집기가 받아들인 색은 모두 유효하다고 볼게요. 새로 연 스와치 편…