원문 캡처 · entropicthoughts.com
원문 캡처 · entropicthoughts.com

부작용이 많은 회로 시뮬레이터와 순수하게 만드는 시도에 이어, 이 글은 Haskell에서 흔히 쓰이는 MTL 스타일 효과를 중간 접근법으로 설명한다. 회로 시뮬레이션에 필요한 연산만 타입 클래스 인터페이스로 정하고, 구성 요소가 그 인터페이스만 사용하도록 제한하는 방식이다.

앞서 만든 부작용이 많은 회로 시뮬레이터와 순수한 회로 시뮬레이터를 다룬 뒤, 글쓴이는 프로덕션 Haskell 코드에서 흔히 사용하는 중간 방식을 소개한다. 이를 MTL 스타일 효과라고 부르며, 특정한 종류의 최종 태그리스 인코딩(final tagless encoding)이라고 설명한다.

먼저 시뮬레이터가 지원해야 할 연산을 정하고, 이를 타입 클래스에 담는다. 글에서는 다음과 같은 인터페이스를 예로 든다.

class CircuitOperations m where make_wire :: m Wire get_signal :: Wire -> m Signal set_signal :: Wire -> Signal -> m () add_action :: Wire -> m () -> m () after_delay :: Int -> m () -> m () get_time :: m Int probe_output :: String -> m () propagate :: m ()

이전 구현에서 get_signal의 타입은 get_signal :: Wire -> IO Signal 였다. 즉, 신호를 반환하려면 IO 문맥에서 실행해야 했다. IO를 사용하면 파일 읽기나 외부 프로그램 실행 같은 임의의 부작용도 구현할 수 있다.

새 인터페이스에서는 타입이 get_signal :: CircuitOperations m => Wire -> m Signal 로 바뀐다. 구체적으로 어떤 문맥에서 실행될지는 나중에 결정되며, 함수 구현은 CircuitOperations가 제공하는 연산만 알 수 있다. 실행 문맥이 IO로 정해지더라도 구현 자체는 IO에 직접 접근할 수 없으므로 인터페이스 밖의 작업을 임의로 수행하지 못한다.

이 인터페이스에 속하지 않는 구성 요소도 같은 방식으로 제한된다. 예를 들어 inverter는 코드 변경 없이 inverter :: CircuitOperations m => Wire -> Wire -> m () 타입을 갖는다. 구성 요소가 잘못 동작할 가능성까지 사라지는 것은 아니다. 다른 구성 요소의 효과 예약을 취소하는 등의 행동은 가능하지만, 이전보다 허용되는 잘못된 동작의 범위가 좁아진다.

이제 연산의 구현을 CircuitOperations IO 인스턴스에 넣는다. 글에서는 기존 구현을 바꾸지 않고 그대로 옮길 수 있다고 설명한다. 이 시점에는 모든 회로 연산을 지원하는 타입이 IO뿐이므로 컴파일러는 이를 선택하고, 실행도 이전처럼 IO에서 이뤄진다.

따라서 이 방식은 프로그램을 순수하게 만들지는 않는다. 다만 회로 시뮬레이션 코드를 IO 전체가 아니라 회로 시뮬레이션에 필요한 연산만 사용하도록 제한한다.

글에서 제시한 추가 제약은 전역 가변 변수 the_agenda다. 이 변수 때문에 현재 인터페이스를 IO 이외의 타입으로 구현할 수는 없다. 글쓴이는 의제(agenda) 관련 연산을 인터페이스나 하위 인터페이스에 추가하면, 부작용을 캡슐화하는 ST 같은 타입으로 구현할 수 있다고 설명한다. 그러면 구성 요소는 IO와 ST에서 모두 실행할 수 있으며, 구성 요소를 특정 실행 문맥에 묶지 않고 시뮬레이션에 필요한 연산을 분명히 구분할 수 있다.