바이너리 간 상호 운용이 가능하도록, 타입 안전하게 클로저를 직렬화합니다.
바이너리 간에 상호 운용되도록 타입 안전하게 클로저를 직렬화합니다.
이 패키지는 distributed-static과, 동일한 연구에서 나온 GHC의 정적 포인터(static pointers) (GHC.StaticPtr의)에서 영감을 받았습니다. 다만 아래에서 설명하듯 몇 가지 상당히 다른 설계 선택을 했습니다.
GHC는 정적 값이 완전히 동일한 바이너리를 실행 중인 노드들 사이에서 전달될 수 있음을 보장하는 데 초점을 맞추는 설계 선택을 했습니다. 그 이유는 이들이 컴파일러가 자동 생성하는 64비트 정수로 인덱싱되기 때문입니다. distributed-static은 서로 다른 버전의 GHC로 컴파일된 동일 소스 프로그램을 지원하려고 시도합니다. 이러한 안정성 보존 노력의 일환으로, 안정성을 나타내는 키를 갖고(키가 안정성을 대표), 값은 컴파일러 버전에 따라 잠재적으로 다르게 해석/해결될 수 있는 테이블(RemoteTable)을 전달해야 합니다.
호출자가 RemoteTable을 넘겨야 한다는 점은 부담으로 여겨지기 때문에, 후속 패키지인 distributed-closure와 static-closure는 정반대 접근을 택해 RemoteTable을 제거하되, 동일한 바이너리 프로그램을 실행하는 서로 다른 프로세스들 사이에서만 호환성을 보장한다는 GHC의 선택을 더 강화했습니다. 이들의 사용 사례는 계산 클러스터 등 중앙집중형 분산 컴퓨팅에 초점을 맞추며, 이런 환경에서는 동일 바이너리를 맞추기 쉽고 문제가 되지 않습니다.
때때로 이 제한의 이유로 보안이 언급되지만, 이는 허수아비 주장(bogus argument) 입니다. “동일 바이너리에서만 호환성 보장”은 “동일 바이너리 => 호환성”을 의미하는 반면, 그 허술한 보안 논리는 “호환성 => 동일 바이너리”에 기대고 있는데 이는 사실이 아닙니다. 누군가가 여러분의 바이너리를 분석하면 어떤 숫자를 위조해야 하는지 알 수 있고, 이 인터페이스를 통해 프로그램을 속여 “같은 코드를 실행 중”이라고 믿게 만들 수 있습니다. 적대적 환경에서 “동일 바이너리”를 보장하는 것은 실제로 극도로 어렵고 완벽히 달성할 수 없습니다. 현실에서는 근사적으로만 가능하며, 이를 위해 설계된 메커니즘을 통해 해야지, (기껏해야) 브루트포스가 조금 어렵거나 (최악에는) 알아내기 매우 쉬운 숫자에 의존해서는 안 됩니다.
이 패키지는 그와 반대 선택을 하며, 인터넷이나 탈중앙 프로토콜 같은 더 개방적이고 제약이 적은 분산 컴퓨팅 환경을 염두에 둡니다. 이런 맥락에서는 정확히 동일한 바이너리 프로그램을 실행해야 한다는 요구사항이 실제로 불가능합니다. 더 나아가, 코드가 정확히 같을 필요가 없다는 점을 장점 으로 봅니다. 예를 들어, 클로저와 그 입력들을 직렬화한 뒤 코드를 업그레이드 하고, 역직렬화된 동일 입력 인자들에 대해 버그가 수정된 클로저로 실행을 재개할 수 있습니다. 명시적인 RemoteTable(여기서는 단순히 staticTab이라 부름)을 넘겨야 한다는 점은 부담이 아니라, 높은 수준의 호환성과 상호 운용성을 표현하는 유용한 도구입니다. 두 노드가 각자의 staticTab에 동일한 키를 갖고 있다면, 구현이 크게 다르더라도 상호 운용 가능하게 통신할 수 있음을 압니다. 동일 프로토콜의 서로 다른 마이너 버전을 실행하는 여러 노드들과 통신하고자 하는 노드는, 같은 키를 공유하되 구현은 다른 두 개의 staticTab을 인스턴스화해 마이너 버전 간 행위 차이를 처리할 수 있습니다. 일반적으로 이는 프로그램 코드에 유지해 둘 만한 유용한 메타데이터이며, 비중앙화 네트워킹 프로토콜의 원활한 업그레이드를 더 쉽게 해줍니다.
또 distributed-static과의 기술적 차이도 몇 가지 있으며, 그중 일부는 distributed-static에도 다시 채택될 수 있을 것입니다:
우리는 Rank1Dynamic 대신 종속 타입(dependent types)과 타입 수준(type-level) 프로그래밍을 사용해 타입 안전성을 보장합니다. 이를 통해 랭크-1 다형 함수뿐 아니라 가능한 모든 클로저 타입을 저장할 수 있습니다. 여기에는 (이에 국한되지 않지만) 랭크-n 함수, 제약(constraint)이 있는 함수, 고차(kind) 타입(higher-kinded types)을 사용하는 함수 등이 포함됩니다.
우리의 직렬화 타입클래스는 다양한 직렬화 프레임워크와 상호 운용되도록 설계되었습니다. 편의를 위해 Data.Binary와 Codec.Serialise에 대한 인스턴스를 제공합니다.
다른 정적 값을 참조해야 하는 최상위(top-level) 정의로부터 정적 값을 생성하도록 지원하는 추가 Template Haskell 스플라이스를 제공합니다. 이는 Q 모나드에 대한 mfix 구현을 사용해 (재귀/상호 재귀/비재귀 모두) 달성합니다.
우리는 정적 참조를 합성(compose)하는 기능을 구현하지 않았습니다. 주된 이유는, 우리의 관점에서 정적 클로저의 목적 은 실행할 최상위 작업(task)이 무엇인지와, 그 작업을 어떤 입력에 대해 실행할지를 표현하는 데 있기 때문입니다. 이것이 이 개념의 인터페이스 또는 계약(contract) 입니다. 작업을 어떻게 실행하는지 는 구현 세부사항이며, 위에서 논의했듯이 서로 다른 머신 간 또는 시간이 지나 코드 업그레이드가 이뤄지면서 달라질 수 있습니다. 그러므로 “작업 A는 클로저 B와 클로저 C의 합성이다” 같은 표현을 직렬화하는 것은 인터페이스 와 무관하므로 의미가 없습니다.
만약 여러분의 인터페이스 가 실제로 “임의의 사용자 정의 코드를 실행하라”(예: VM이나 EDSL 평가기에서)라면, 합성을 지원하는 것이 타당합니다. 하지만 그 경우에는 여러분만의 AST, 평가기(evaluator), 직렬화기를 정의해야 하며, AST를 정적 값이 아니라 일반 런타임 값으로서 주고받아야 합니다. 이런 임의 AST 지원은 이 라이브러리의 범위를 벗어납니다.
또한 Haskell에서는 단순히 아래처럼만 값을 적용(apply)하는 것이 아니라,
(_ :: a
-> b) (_ :: a) :: b
제약과 함께, 정적/비정적 인자의 조합으로, 타입 애플리케이션을 사용해서 등 다양한 방식이 있습니다. 여러 언어에 걸쳐 상호 운용 가능할 법한 것은 단순한 형태 (_ :: a -> b) (_ :: a) :: b 정도뿐입니다. 따라서 AST 정적값을 지원하는 것은, 우리가 선택한 언어에서 정적 클로저의 동작을 구현하는 방식을 불필요하게 제한하게 됩니다.
사용 예시는 유닛 테스트를 참고하세요. 예: UnitTests
패키지 메인테이너와 hackage 트러스티를 위한 링크
Candidates