능력을 묶기, 그리고 그것은 어디서 오는가

명시적 능력과 세밀한 대여 모드는 시그니처를 길게 한다 —— 두 줄짜리 함수에 아홉 개의 인자. 수정은, 이 Part 전체가 싸워 얻은 명시성을 되사지 않으면서, 표기를 압축해야 한다. 그리고 밑에는 두 번째 물음이 기다린다: 능력은 어디서 구축되고 파기되는가? 어느 답도, 이미 손에 있는 기구를 재사용하게 된다 —— 하나는 순수한 타입 레벨의 전개, 다른 하나는 메모리 모델 자신의 `with`와 `region`이다.

mereeffectscapabilitieslifecyclelanguage-design

Part III는 능력을 값으로 넘김으로써 이펙트를 보이게 했고, 고차 함수를 통과해도 보이게 유지했으며, 대여 모드로 결을 주었다. 어느 한 걸음이나 정직함을 더했고, 정직함이 길이를 더했다. 이 이펙트 마지막 회는, 그 셈을 치르는 이야기다 —— 시그니처는 길어졌다 —— 그리고, Part가 줄곧 암묵에 맡겨 온 물음의 이야기다: 능력은 실은 어디서 오는가?

열세 줄의 시그니처

능력 전달과 세밀한 대여를 현실적인 함수 위에서 합치면, 대가는 뚜렷하다:

fn save_order[region A](
    order:   owned Order,
    db:      &mut A Database,
    cache:   &borrowed Cache,
    logger:  &shared write Logger,
    mailer:  &borrowed Mailer,
    metrics: &shared write Metrics,
    clock:   &borrowed Clock,
    rng:     &borrowed Random,
    arena:   &A Region,
) -> Result[owned OrderId, owned DbError]
    where len(order.items) > 0
{
    logger.info("saving order")
    db.insert("orders", order)
}

아홉 개의 인자, where 절, 반환 타입 —— 두 줄짜리 본체에 대해 열세 줄의 시그니처. 이런 함수가 하나라면 괜찮다. 코드베이스 통째가 이러면, 실제로 무언가를 하는 코드를 삼키는 리뷰 부하가 된다. 이것이, 첫 이펙트 회가 깃발을 세운 인간공학의 부채이고, 이제 상환 기한이 왔다.

유혹은, 능력을 숨기는 것이다 —— 그것들 전부를 슬며시 나르는 “컨텍스트” 오브젝트 —— 하지만 그것이야말로 Part III가 거부해 온 수다. 이펙트를 다시 보이지 않게 이동시키기 때문이다. 첫 회에서 말한 구별이, 여기서의 경첩의 전부다: “운반을 쉽게 하지 않는다”는 “묶지 않는다”가 아니다. 묶는 것은 허용된다. 숨기는 묶음이 허용되지 않는다. 일은, 정보를 지우지 않고 기술을 압축하는 묶음을 찾는 것이다.

시그니처 에일리어스: 정보가 아니라 표기를 압축한다

Mere의 답은 시그니처 에일리어스 —— 그 자리에서 전개되는, 이름 붙은 인자 무리다:

signature server_caps =
    db:      &mut Database,
    cache:   &borrowed Cache,
    logger:  &shared write Logger,
    mailer:  &borrowed Mailer,
    metrics: &shared write Metrics;

fn save_order[region A](order: owned Order, ...server_caps, arena: &A Region)
    -> Result[owned OrderId, owned DbError]
{
    logger.info("saving order")
    db.insert("orders", order)
}

...server_caps 스프레드가, 그 지점에서 다섯 인자를 모두 시그니처에 떨어뜨린다. 숨은 컨텍스트 오브젝트가 기각된 자리에서 이것이 허용되는 것은, 하나의 성질 덕이다: 그것이 참조 투명하다는 것이다. 스프레드는 순수한 타입 레벨의 치환이고 —— 기계적으로 열세 줄의 형태로 되전개할 수 있으며, 둘은 같은 함수다. 개개의 이름은 살아남는다: db·logger·mailer는, ctx.의 뒤에 묻히지 않고, 쓰기 위해 거기에 있다. 정보량은 동일하고, 표기만이 짧다.

그것이, Part 전체가 짊어져 온 긴장의 해소다. Part III는 장황함을 명시성의 대가로 받아들였다 —— 하지만 시그니처 에일리어스는, 그 둘이 실은 결코 같은 것이 아니었음을 보인다. 명시는 장황과 같지 않다. 모든 능력을 이름 붙여지고 검사된 채 유지하면서, 그럼에도 모든 자리에서 목록을 써 내지 않아도 된다 —— 약기가 꼭 긴 형태로 전개되고, 아무것도 숨기지 않는 한. 컨텍스트 오브젝트는 그 시험에 떨어진다 —— 정보를 제거한다 —— 그래서 그것은 기각된 채이고, 에일리어스는 채택되었다.

모서리가 없는 것은 아니고, 노트는 그것에 정직하다: 둘 다 db를 정의하는 두 에일리어스를 스프레드하면 이름 충돌이고, 그것을 컴파일 에러로 만들어 명시적 rename으로 고침으로써 해결한다; 그리고 시그니처는 공유된 계약이므로, 하나를 바꾸면 그것을 스프레드하는 모든 것에 파급된다. 검토된 대안 —— Scala 식의 모듈 레벨 암묵 인자와, 모듈 안에서 능력을 잠자코 채우는 추론 —— 은 어느 쪽도 같은 이유로 기각되었다: 그것들은, 언어가 가장 명시를 원하는 경계에서야말로 암묵성을 재도입함으로써, 간결함을 사는 것이다.

능력은 어디서 오는가 —— 그리고 Part II와의 재회

시그니처는, 함수가 Logger받는다고 말한다. 그것은, 누가 logger를 만들었는지, 언제 부서지는지를 말하지 않는다. logger는 구축되어야 하고, 데이터베이스 연결은 열리고, 이윽고 닫혀야 한다. 그것은 누구의 일이고, 어디에 사는가?

네 선택지가 책상 위에 있었다: 전부를 main에서 만들어 아래로 넘긴다(고전적 의존성 주입); 능력을 모듈 레벨의 글로벌로 구축한다(즉각 기각 —— 그것이야말로 Part 전체가 막기 위해 존재하는, 주변적이고 보이지 않는 권한이다); 능력을 region에 결속하여 region이 사라질 때 함께 죽게 한다; 혹은 명시적 스코프에서 구축하고 파기한다. Mere는 뒤의 둘을 하이브리드로 택하고, 첫째를 그 특수 사례로 접어 넣었다 —— 그리고 두드러지는 것은, 그것이 새 기구를 전혀 요하지 않았다는 것이다. 필요한 구문이, Part II에서 이미 만들어져 있었기 때문이다.

능력은 with로 구축되고 해제된다 —— Trivial과 정리의 메모리 모델 회의, 바로 그 구문이다. 이것은 우연이 아니라, 언어의 두 절반이 만나는 지점이다. 대부분의 능력은 Drop을 가진다: 데이터베이스는 연결을 닫고, 파일은 fd를 해제하고, logger는 flush한다. 그것이 바로, Trivial한 값밖에 담을 수 없는 region에 살 수 없는 이유이고 —— 그리고 바로, 모든 출구 경로에서 LIFO 순으로 정리를 관리하기 위해 존재하는 with에 사는 이유다. 이펙트 시스템의 라이프사이클 문제와, 메모리 모델의 정리 문제는, 같은 문제이고, with는 이미 그 답이었다.

이것이 능력에, 같은 두 구문을 중첩하여 표현되는, 자연스러운 수명의 계층을 준다:

  • 애플리케이션 전체 —— 프로그램의 최외 스코프의 with, root region 위에, 프로세스와 같은 만큼 사는 것을 위해: 연결 풀, logger.
  • 요청 단위 —— 자식 region 위의 중첩된 with, 요청 자신의 능력과 스크래치 메모리를 위해, 요청 종료 시 모두 함께 해제된다.
  • 함수 스코프 —— 잠깐만 필요한 능력을 위한 국소의 with.

의존성 주입 —— 전부를 main에서 만드는 것 —— 은, 이것의 최외 층일 뿐, 별개의 기구가 아니다. 그리고 테스트는, 첫 이펙트 회에서 그랬듯, 공짜로 흘러 떨어진다: 테스트는 바깥의 with에서 가짜를 구축하고, 테스트 대상 코드는 건네받은 것보다 앞으로 손을 뻗을 수 없다.

Part III, 닫히다

이펙트 모델은 이제 완성되었고, 그 형태는 메모리 모델의 그것과 운을 맞춘다. 이펙트는 값이고, 명시적으로 넘겨진다; 고차 함수를 통과해도 보이는 채다, 능력이 함수가 범용이 되기 전에 소비되기 때문에; 결을 나른다 —— 읽기인지 쓰기인지, 공유인지 배타인지 —— 타입에서 검사되고 런타임에 공짜로; 숨기지 않고 묶인다, 정직한 긴 형태로 전개되는 에일리어스로써; 그리고 with에서 태어나고 죽는다, 메모리의 정리를 관리하는 것과 같은 구문으로. 여기에는 주변적인 것이 아무것도 없고, 아무것도 보이지 않게 이동하지 않으며 —— 그리고 말해 주듯, 가장 어려운 두 인간공학의 답은, 더한 발명이 아니라, 이미 손에 있는 기구를 재사용했다.

두 Part가 이제 언어가 무엇인가를 그렸다: 메모리 모델과 이펙트 시스템, 어느 쪽도 명시적이고, 어느 쪽도 검사 가능하며, 어느 쪽도 몇 번이고 다시 등장하는 작은 개념의 집합에 기댄다. 그것들이 보이지 않은 것은 어떻게 도는가다. 언어는, 무언가가 그것을 실행하기까지는 약속이다 —— 그리고 Mere는 그것을 네 가지 다른 방식으로 실행하고, 그것들을 엄밀히 일치시키기를 택했다. 다음, Part IV가 열린다: 왜 언어는 하나 이상의 backend를 요하는가, 그리고 그것들을 동일하게 유지한다는 것은 무엇을 뜻하는가.

← Back to Mere: 언어를 만들다