Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2026-07-30 업데이트.

Prism

Prism: 타입이 지정된 이펙트를 갖춘 비순수 함수형 언어

이번 글은 상당히 너드한 글이 될 것이니 양해해 주십시오. 가장 짧게 요약하면 다음과 같습니다:

#![allow(unused)]
fn main() {
fn fib(n) =
  var a := 0
  var b := 1
  var i := 0
  while i < n do
    let t = a + b
    a := b
    b := t
    i += 1
  a
}

이것은 Int -> Int 타입을 가진 가변 루프입니다. 변경은 실제로 일어나지만 지역적이고 관찰할 수 없으므로, 호출자의 관점에서 이 함수는 순수합니다. Prism은 효과 메커니즘을 통해 var와 while을 로어링하고, 비공개 상태가 빠져나갈 수 없음을 증명한 다음, 루프를 여러분이 의도한 제자리(in-place) 코드로 컴파일합니다. Python으로 작성할 법한 코드에 OCaml에서 원할 법한 시그니처를 갖추었으면서도, 함수 한가운데를 작은 종교 의식으로 만들지 않습니다.

예전에는 그 예제가 이야기의 전부였습니다. v0.15에 이른 지금은 그것을 훨씬 더 큰 설계의 작은 끝자락으로 이해하는 편이 낫습니다. Prism은 하나의 계약을 중심으로 만들어진 개념 증명용 함수형 언어입니다: 관찰 가능한 동작은 검사된 코드와 명시적으로 고정된 입력에 의해 결정되어야 합니다. 효과는 계산이 세계에 무엇을 요청할 수 있는지를 말하고, 핸들러는 그 요청이 무엇을 의미하는지 결정하며, 검사된 중간 표현은 그 결정을 보존하고, 콘텐츠 식별자는 그것에 이름을 붙이며, 하나의 정규 관찰 트레이스가 그것을 실행하는 모든 방식을 판정합니다. 이 언어는 여전히 장난감이지만, 이제는 약간 도를 넘어 버린 원래의 효과 데모보다 훨씬 더 넓은 설계를 갖추고 있습니다.

  • Prism 소스 코드 - GitHub에 있는 컴파일러, 표준 라이브러리, 도구
  • Prism 문서 - 언어 가이드, 레퍼런스, 브라우저 플레이그라운드

효과는 권한입니다

대수적 효과는 계산이 보내는 요청에 대한 인터페이스이고, 핸들러는 그 문맥이 제공하는 구현이자 권한입니다. 따라서 ! 뒤의 행(row)은 단순한 정직성 표시 이상입니다: ! {Console, FileSystem}은 함수가 관찰할 수 있는 세계의 정확한 부분이며, 빈 행은 그러한 권한이 전혀 필요하지 않음을 뜻합니다. 행은 구조적으로 합성되고, 핸들러는 자신이 소유한 연산만 처리할 수 있으며, 나머지는 모두 바깥쪽으로 통과해 나갑니다. 재개 가능한 절에서 계속(continuation)은 resume 뒤에 작성되고, never 절에는 계속이 전혀 없습니다. 그래서 생성기, 예외, 상태, 실패, 재시작, 스트림, 백트래킹이 모두 같은 작은 메커니즘을 공유합니다. 빠른 경로에서 컴파일러는 효과 증거를 직접 호출로 바꾸고 연산마다 자유 모나드 노드를 할당하는 일을 피하므로, 이 추상화가 성능에 대한 변명거리가 되지 않습니다.

#![allow(unused)]
fn main() {
effect Gen
  yield(Int) : Unit

fn produce(n) : Unit ! {Gen} =
  if n == 0 then
    ()
  else
    yield(n)
    produce(n - 1)

fn total(n) =
  handle produce(n) with
    yield(v) resume k => v + k(())
    return r => 0
}

결정성이 계약입니다

Prism은 구현상의 선택이 관찰 불가능하게 유지될 것을 요구합니다. 인터프리터, LLVM 백엔드, MLIR 백엔드, 최적화 수준, 강제된 효과 로어링 단계는 출력, 권한 이벤트, 커밋된 파일, 오류, 결과를 담은 하나의 순서 있는 관찰 트레이스를 통해 비교됩니다. 다른 스케줄러 정책이나 더 빠른 로어링은 비용을 바꿀 수 있습니다. 의미는 고정된 채로 유지됩니다. 초월 부동소수점 연산은 동일하게 벤더링된 musl 루틴을 거치므로, 호스트 라이브러리와 관계없이 결과 비트가 안정적으로 유지됩니다. 시간, 무작위성, 환경 변수, 파일 시스템 읽기는 권한 효과로 들어오며, 그 결과가 고정되고 나면 실행은 프로그램과 그 트레이스의 함수가 됩니다. 결정성은 백엔드 동등성, 재생, 캐싱, 그리고 이후의 모든 주장이 참조하는 공통 실행을 제공합니다.

하나의 검사된 의미론적 척추

컴파일러에는 모든 백엔드가 공유하는 하나의 의미 파이프라인이 있습니다. 표면 구문은 엘라보레이션에 필요한 타입, 효과, 바인딩, 패턴, 소스 정보를 담은 검사된 HIR로 해석됩니다. 엘라보레이션은 이 정보를 call-by-push-value, 관리 정규형(administrative normal form) Core로 옮겨 적습니다. 타입이 있는 Core는 독립 검증기가 모든 변환 후에 검사하는 증거(witness)를 담고 있습니다. 검사기가 결정하고, 이후 단계는 그 정보를 옮겨 적습니다. 최적화는 항을 단순화하거나 로어링할 수 있지만, 인터프리터나 네이티브 이미터가 그 항을 볼 수 있게 되기 전에 독립적으로 유효한 또 다른 Core 아티팩트를 만들어 내야 합니다. CBPV는 값과 효과를 지닌 계산 사이의 경계를 명시적으로 드러내며, 이 덕분에 효과 로어링, 소유권 삽입, 백엔드 코드 생성이 합의할 수 있는 하나의 지점이 생깁니다. 물론 컴파일러에도 여전히 버그가 있을 수 있지만, 모든 백엔드는 화요일에도 똑같이 검사된 언어를 받습니다.

콘텐츠 정체성과 계보

검사된 모든 정의에는 정규화된 Core와 그 컴파일이 읽는 정확한 의미적 의존성으로부터 도출된, 스킴 태그가 붙은 정체성이 있습니다. 파일, 정규 구문 트리, 바인더, 타입 형태, 모듈 인터페이스, 표준 라이브러리 루트, 빌드 쿼리, 네이티브 산출물은 서로 관련되어 있지만 의도적으로 구별된 정체성을 가집니다. 소스 텍스트와 검사된 의미를 혼동하는 순간 빌드 시스템은 신학이 되기 때문입니다. 같은 머클 구조가 증분 검사와 영속 빌드 캐시를 모두 구동합니다. 인터페이스가 그대로인 구현 수정은 그것을 임포트하는 모듈들을 검사된 상태로 남겨 두며, 변경되지 않은 쿼리는 이전 산출물을 재사용할 수 있습니다. 더 특이하게도 컴파일러는 이러한 결정에 대한 계보 정보를 기록하므로, prism lineage why-recompiled는 어떤 의미적 입력이 바뀌었는지 설명하여 빌드 시스템의 전통적인 어깨 으쓱을 은퇴시킬 수 있습니다. core-identity 덤프는 정의 해시에 들어가는 재료를 드러내며, v0.15에는 컴파일러의 결과를 재현하는 Prism 소비자가 포함되어 있습니다. 그 범위는 정규화된 검사된 코드와 의존성의 내포적 정체성이며, 이는 검사기, 캐시, 산출물, 실행 관문이 함께 공유합니다.

재생과 구체화된 실행

관찰이 명시적이고 실행이 결정적이라면, 기록된 능력 트레이스가 바깥 세계를 대신할 수 있습니다. 재생은 실제 연산을 수행하지 않고도 같은 결과를 제공하며, 같은 정규 관찰을 재현해야 합니다. Prism은 또한 인터프리터 컨티뉴에이션을 코드 정체성에 묶인 버전 있는 봉투로 구체화하므로, 중단된 계산은 그것이 지칭하는 의미 세계에 대해서만 검사되고, 저장되고, 재개될 수 있습니다. 두 기능 모두 언어 수준의 효과 기계에서 곧바로 나옵니다. 보장들은 분리되어 있고 정직하게 유지됩니다. 콘텐츠 정체성은 이것이 어떤 검사된 계산인지를 말하고, 재생 가능성은 어떤 관찰을 트레이스로부터 제공할 수 있는지를 말하며, 이식성은 컨티뉴에이션이 경계를 넘을 수 있는지를 말하고, 각각은 고유한 검사기나 증거를 가집니다. 실행은 프로세스 이미지나 임의의 소켓, 어제의 배포가 결코 충족할 수 없을 만큼 엄격한 조건을 지닌 데이터가 됩니다.

결정적 동시성

동시성은 여전히 Async 효과 위의 표준 라이브러리 핸들러로 남으며, 함수 타입은 하나의 일관된 효과 어휘를 유지합니다. 파이버, 채널, 구조적 조인, 취소 정리, 스케줄러 정책은 Concurrent 모듈이 해석합니다. 파이버 안에서 수행된 효과는 호출자의 행(row)에 남아 있으며, 스케줄러를 거쳐 바깥쪽 핸들러로 전달됩니다. 협력적 스케줄링은 각 양보 지점을 명시적으로 만들고, FIFO와 LIFO 정책은 교체 가능한 핸들러이며, 논리적 Clock을 스케줄러 바깥에 둘 수 있으므로 테스트는 운영체제에 묻지 않고도 결정적인 시간을 진행시킬 수 있습니다. 같은 프로그램을 재생 핸들러로 감싸면, 그 스케줄된 관찰은 일반적인 순차 실행과 같은 트레이스로 판정됩니다. 어려운 동시성 문제는 여전히 남아 있지만, 런타임, 시계, 스케줄러가 모두 프로그램 의미의 눈에 보이는 일부가 됩니다.

함수형 성능

런타임 이야기를 한 문단으로 줄이는 것은 배관 작업보다 표현 방식의 선택이 더 중요하기 때문입니다. Prism은 Perceus 방식의 결정적 참조 카운팅, 프레임 범위로 제한된 셀 재사용, 언박싱된 곱 타입, 플랫 버퍼, 아레나, 검사되는 fip 또는 fbip 사용 계약을 활용하여, 함수형 코드가 추적 수집기 없이 예측 가능한 메모리 동작으로 실행되도록 합니다. 고유하게 소유된 리스트 셀이나 레코드 스파인은 그 자리에서 재사용될 수 있으며, 소스 코드는 평범한 값 의미론을 그대로 유지합니다. 공유된 값은 할당하는 경로를 거치며, 두 경로는 반드시 같은 결과를 내야 합니다. 이펙트 로어링도 마찬가지로 직접적인 증거 전달, 특수화된 상태, 일반적인 구체화 폴백 가운데 하나를 선택하되, 그 선택이 관찰되지 않도록 합니다. LLVM과 MLIR은 작은 모듈식 C 런타임을 대상으로 네이티브 코드를 생성하며, 인터프리터는 여전히 의미론적 기준(oracle) 역할을 합니다. 컴파일러는 할당, 재사용, 융합, 상수 스택 실행이 보장되는 정확한 조건을 명시하고, 그런 다음 그 조건을 검사합니다.

현대적인 타입과 표면 문법

표면 문법은 의도적으로 익숙하게 만들어졌습니다. 양방향 고계 랭크 추론, 행 다형적 이펙트, 일관된 표준 선택이 하나뿐인 Lean 방식의 이름 붙은 타입 클래스 인스턴스, 명시적 using 재정의, 파생(deriving), 점 체인, 레이아웃 블록, 컴프리헨션, 렌즈, 실패 연산자, 명령형 문법 설탕이 모두 같은 작은 Core 위에 얹혀 있습니다. v0.15는 실제 모델에 맞추어 표기법을 다듬었습니다. 결과 타입은 : Int ! {E}처럼 행보다 앞에 오고, 재개 가능한 절은 op(x) resume k로 쓰며, 재개 등급은 직설적이면서도 유용한 never, once, many입니다. 또한 임의의 패턴 깊이에서의 선택 패턴과 매개변수 위치에서의 생성자, 레코드, 튜플 패턴을 추가했으며, deriving에 ToJson, FromJson, Plate를 더해 확장했습니다. Plate가 특히 유용한 이유는, 이제 컴파일러 패스를 하나의 지역 분석 또는 재작성 규칙에 파생된 자식 목록 및 재구성 연산 위의 순회 전략을 더한 형태로 작성할 수 있어서, 마흔일곱 번째 방문자(visitor)를 만들어야 하는 수고에서 세상을 구해 주기 때문입니다. 접근하기 쉬운 문법은 기반 모델을 깔끔하게 드러냄으로써 제 몫을 다합니다.

#![allow(unused)]
fn main() {
type Pair = Pair(Int, Int)
type Shape = Dot | Line(Int, Int) | Ring(Int)

fn sum_pair(Pair(a, b)) = a + b

fn extent(s) =
  match s of
    Line(0 | 1, _) => 1
    Line(n, _) | Ring(n) => n
    Dot => 0
}

코이펙트

이펙트는 계산이 자신의 세계에 무엇을 할 수 있는지를 기술합니다. 코이펙트는 주변 세계가 값을 가지고 무엇을 할 수 있는지를 기술합니다. Prism은 전자를 ! 뒤에, 후자를 @ 뒤에 쓰므로, !는 바깥쪽으로 보고하고 @는 값을 소비하는 경계에서 안쪽으로 요구 사항을 둡니다. v0.15에서는 네 가지 사용 사실을 검사합니다. noalloc은 함수의 전체 호출 트리가 새로운 힙 셀을 전혀 할당하지 않음을 인증합니다. once는 클로저가 최대 한 번만 소비되도록 합니다. portable은 클로저를 새로운 런타임으로 옮겨도 안전한, 내용 주소 기반의 코드와 데이터로 제한합니다. noescape는 값이 자신의 경계를 넘어 반환되거나, 저장되거나, 캡처되는 것을 막습니다. 예약된 사용 어휘의 다른 단어들은 실제 검사기가 생길 때까지 거부됩니다. 이러한 명시적 계약은 API 경계에 존재하며 Core 이전에 소거되므로, 어떤 프로그램이 컴파일되는지는 제한하지만 받아들여진 프로그램의 런타임 표현은 그대로 둡니다. borrow, fip, fbip, 할당 인증서, 컨티뉴에이션 등급은 모두 같은 일반적인 질문, 즉 문맥이 이것을 가지고 무엇을 할 수 있는가라는 질문을 서로 다른 관점에서 바라본 것입니다.

세 가지 부분 순서 집합

Prism 시그니처는 세 가지 부분 순서 체계를 담고 있습니다. 이펙트 행은 집합 포함 관계로 순서가 매겨집니다. 순차 실행은 합집합, 즉 조인(join)을 취하고, 핸들링은 레이블을 제거하여 순수한 빈 행 쪽으로 되돌리므로, 더 많은 일을 하는 것은 언제나 표현할 수 있습니다. 코이펙트 행은 더 작은 사용 축들의 곱이며, 맨 위에는 침묵(아무 주장도 없음)이 있습니다. 아래로 내려가면 누군가가 증명해야 하는 약속이 강화되고, 위로 올라가면 약속 하나를 잊을 뿐이며, once와 many 같은 일부 배타적인 주장은 진정으로 모순되기 때문에 공통의 밋(meet)이 없습니다. 연산 등급은 단순한 전순서 격자 never < once < many를 이룹니다. 핸들러 절은 컨티뉴에이션을 버리거나, 꼬리 위치에서 정확히 한 번 재개하거나, 캡처하여 자유롭게 재개할 수 있지만, 그 절 자체의 동작은 연산에 선언된 등급보다 커서는 안 됩니다. 이것이 타입 시스템의 약간은 놀라운 단일화입니다. 이펙트는 언제나 조인이 필요하고, 코이펙트는 때때로 밋을 거부하며, 컨티뉴에이션 사용은 하나의 양(quantity)입니다. 그러면 이펙트 연산은 결국 자기 자신의 컨티뉴에이션에 대한 코이펙트임이 드러나는데, 이는 컴파일러가 실제로 검사하기 전까지는 무표정하게 늘어놓은 범주론적 헛소리처럼 들립니다.

영수증이 있는 검증

Prism은 검사를 마친 동일한 대상 옆에 여러 종류의 증거를 함께 보관합니다. logic fn, requires, ensures는 SMT 증명 의무와 명시적인 솔버 영수증을 생성합니다. total fn은 구현된 방법으로 종료성을 확립할 수 있는 곳에서는 이를 검사하고, 그렇지 못한 곳에서는 정직하게 보류 결과를 보고합니다. test fn은 결정론적 탐색과 격리된 실행을 제공합니다. 할당 및 이동성 어노테이션에는 각각 고유한 계약이 있습니다. Core의 일부는 결정론적 small-step 관계, 전함수인 CEK 스텝 함수, 그리고 이 기계를 big-step 의미론과 연결하는 정리를 갖춘 형태로 Lean 4에서 별도로 모델링되며, 이후 픽스처 코퍼스를 대상으로 Rust 인터프리터와 대조하여 실행됩니다. 이 정리가 증명하는 범위는 Lean 모델입니다. Rust 타입 검사기, 최적화기, 그리고 컴파일러 전체는 그 범위 밖에 남아 있습니다. 모든 보증은 이를 뒷받침하는 증거를 명시하며, 현재 기계화가 끝나는 지점에서는 차등 동등성 검사가 관문 역할을 계속 수행합니다.

Prism으로 작성한 Prism

장기적인 목표는 검증된 표현 경계를 하나씩 쌓아 올려 만드는 셀프 호스팅 컴파일러입니다. Rust 컴파일러는 stage 0입니다. 이 컴파일러가 Prism-in-Prism 소스를 컴파일하여 stage 1을 만들고, 이어서 stage 1이 같은 소스를 컴파일하여 stage 2를 만듭니다. 부트스트랩은 stage 1과 stage 2가 바이트 단위로 동일한 산출물을 생성할 때 고정점에 도달합니다. 각 컴파일러 단계는 Rust 오라클 옆에서 평범한 Prism 라이브러리로 출발하여, 파이프라인의 나머지 부분과 버전이 지정된 산출물을 주고받고, 차등 관문이 닫힌 뒤에 권한을 얻습니다. v0.15는 Prism 렉서, 보간 스캐너, 레이아웃 패스로 첫 번째 이음매를 차지하여, Rust가 계속 stage 0을 공급하는 가운데 프로젝트에 처음으로 성공한 부트스트랩 계층을 마련해 줍니다. 같은 접근 방식으로 구문, 검사, 정교화, 코드 생성을 차례로 옮겨 갈 수 있으며, 이때 콘텐츠 식별자가 모든 입력을 고정하고 독립적인 검증기가 모든 출력을 검사합니다. 최종 목적지는 소스, 중간 형태, 도구, 재현 증명이 모두 그것이 컴파일하는 프로그램과 동일한 결정론적 계약 아래에 놓이는 개방형 컴파일러입니다. 바닥 위에 선 채로 그 바닥을 다시 짓는 일은 그것만으로도 충분히 어렵습니다. 모든 마룻널이 콘텐츠 주소로 지정되고 독립적으로 검사되도록 요구하는 것은 터무니없는 일이지만, 실현된다면 정말 멋질 것입니다.

WebAssembly를 통해 브라우저에서 실행

브라우저 빌드는 프런트엔드와 인터프리터를 WebAssembly로 패키징하므로, 플레이그라운드에서는 설치 없이 Prism을 검사하고 실행할 수 있으며 추론된 시그니처, 검사된 HIR, Core, 관찰 결과, 시뮬레이션 상태를 보여 줄 수 있습니다. 플레이그라운드는 나머지 도구들이 사용하는 것과 동일한 인터프리터와 검사된 표준 라이브러리를 실행하므로, 브라우저에서도 같은 의미론적 어휘를 사용할 수 있습니다. 네이티브 Prism 프로그램은 여전히 LLVM 또는 MLIR을 거칩니다. Prism에서 WebAssembly로 직접 코드 생성하는 기능은 향후 과제로 남아 있습니다. 플레이그라운드는 의미론적 파이프라인을 들여다보는 편리한 창이자, 고정된 입력으로부터 N번째 스텝의 상태를 재구성할 수 있는 결정론적 시뮬레이션을 위한 좋은 보금자리를 제공합니다.

너드 이야기

여기까지 읽으셨다면 분명 인생에서 꽤 의심스러운 선택을 여러 번 하신 분일 테니(반갑습니다, 동지여!), 빠짐없이 다루는 의미에서 PL 너드 이야기를 덧붙입니다:

  • 타입 추론은 흔히 쓰이는 최신 기법인 양방향 및 고차 랭크 방식, 즉 complete-and-easy Dunfield-Krishnaswami 알고리즘을 사용하므로, 어노테이션을 잔뜩 달아 빠져나가지 않아도 rank-N 다형성이 동작합니다.
  • Lean 스타일의 이름 붙은 인스턴스(instance ordInt : Ord(Int)), 겹치는 인스턴스의 일관성을 유지하는 canonical 지정, 명시적 재정의(sort_by_ord(xs, using ordRev)), 그리고 deriving (Eq, Ord, Show)를 갖춘 타입클래스를 제공합니다.
  • 대체로 OCaml과 비슷한 모양의 표면 구문을 제공합니다. 레이아웃 블록, 점 체인(xs.over(f).keep(g).sum()), 연속 전달 코드를 위한 with 문법 설탕, 문자열 보간, 이펙트 행 별칭이 있습니다.
  • 깊은 재귀는 네이티브 환경(꼬리 호출, 그리고 생성자를 법으로 하는 꼬리 재귀)과 인터프리터(본질적으로 예전 CEK 기계의 더 발전된 버전이므로 프로그램의 어떤 부분도 호스트 스택을 터뜨릴 수 없습니다) 모두에서 상수 스택으로 실행됩니다.

그리고 정말로 중증인 분들을 위해, 지적 계보 전체를 소개합니다. 당연히 수많은 FP 거장들의 업적 위에 서 있지만, Prism의 설계에 가장 직접적으로 영감을 준 것들은 다음과 같습니다.

장난감 프로젝트에도 논지가 허락된다면, 저의 논지는 “순수 함수형“이 언제나 좋은 아이디어에 붙은 약간 방어적인 이름이었다는 것입니다. 그 좋은 아이디어란 효과가 눈에 보이고, 타입이 있고, 조합 가능해야 한다는 것입니다. 이를 얻기 위해 효과를 금지할 필요는 없습니다. 추적하기만 하면 됩니다. 그리고 효과를 정직하게 추적하기 시작하면 컴파일러는 효과의 비용을 없앨 만큼 충분한 정보를 얻게 됩니다. 순수성 담론은 IO 모나드와 눈을 마주치지 않느라 너무 바빠서 이 부분을 한 번도 약속해 준 적이 없습니다.

이 컴파일러는 본질적으로 훨씬 단순하고 행복했던 2010~20년대 함수형 프로그래밍 시대에 보내는 저의 연애편지입니다(어쩌면 그저 ZIRP 시대의 젊음이 씌워 준 장밋빛 안경 때문일지도 모릅니다). 이 컴파일러는 장난감이며, 트랜스포머 시대 이전의 지나간 프로그래밍 시대에서 온 일종의 예술 작품이라는 점을 빼면 사실상 아무 쓸모가 없습니다. 우리가 향하고 있는 세상에는 이런 것이 설 자리가 더 이상 없습니다. 우리는 점점 더 극도로 확률적이고 우스울 만큼 재미없는 Typescript가 세상의 대부분을 돌리는 세계로 향하고 있습니다. 그 세계에서 우리는 이 토큰 압출기들을 돌보고, 경제와 시장은 점점 더 에이전트에 의해 자동화됩니다. 이 사실이 저를 조금 슬프게 하지만, 그렇다고 이런 것을 그저 재미로 만들 수 없다는 뜻은 아니며, 제가 여기서 한 일이 바로 그것입니다. 어쩌면 함수형 프로그래밍 언어의 미래는 점점 더 틈새에 머물며 경제적으로는 무의미해지고, 그 바탕에 깔린 아이디어의 지적 아름다움 때문에 여전히 함수형 프로그래밍을 사랑하는 소수의 사람들을 위한 취미 활동에 가까워질지도 모릅니다. 그래서 저는 어쨌든 이것을 만들었고, 이것은 잘 돌아가며, 만드는 과정은 정말 즐거웠습니다. 그리고 어쩌면 소프트웨어의 이 멋진 신세계에서는 그것으로 충분할지도 모릅니다.


원문 © Stephen Diehl, stephendiehl.com · 글 CC BY-SA 4.0, 코드 MIT · Stephen Diehl은 런던의 소프트웨어 엔지니어이자 작가입니다(저자가 정한 소개 문구). 이 글은 저자가 개인 자격으로 쓴 것이며 어떤 조직과의 연관도 밝히거나 암시해서는 안 됩니다 (라이선스와 고지).
이 페이지는 원문을 한국어로 옮기며 변경한 비공식 번역이며 같은 CC BY-SA 4.0으로 제공합니다 · yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다.