순수 함수적 병렬성 (2)

Jason Kim·2020년 5월 10일
0

fp in scala with TypeScript

목록 보기
17/17

이 시리즈는 "스칼라로 배우는 함수형 프로그래밍"을 TypeScript로 실습하는 과정을 정리하고 있습니다.

표현의 선택

run이 비동기 task를 실행하는 구체적인 방법으로 ExecutorService를 선택하고 형식을 나타낸다면, Par는 ExecutorService를 입력받고 A를 돌려주는 함수로 표현된다. 그러나 단순히 결과만 돌려받기 보다는 run함수의 호출자에게 보다 많은 편의를 제공하기 위해 Future를 돌려주도록 하자. ExecutorService가 제공되기 전까지는 Future는 생성되지 않는다.

JavaScript에서는 명시적인 쓰레드가 없고 비동기 함수를 blocking 할 수가 없어서 첫번째 Par 구현은 건너뛰었다. 이후에 Future와 Promise를 조합해서 non-blocking Par를 구현할 것이다.

API의 정련

Future의 인터페이스는 순수 함수적이지 않기 때문에 이 라이브러리의 사용자가 Future를 직접 다루는것은 바람직하지 않다. Future의 내부 작동 방식은 오직 사용자가 run을 호출해서 ExecutorService를 넘겨야하기 때문에 Par API 자체는 여전히 순수하다.

map2의 둘째 인수를 무시하는 방법으로 map을 구현하는것은 꼼수같은것이 아니다. 반대로 map을 이용해서 map2를 구현할 수 없기 때문에 이것은 map2가 map보다 더 강력한 기본수단임을 알려주는것이다.

함수적 설계를 진행하다보면 기본수단이라고 생각했던 함수를 다른 좀 더 근본적인 기본수단들로 표현 할 수 있음을 깨닫는 일이 자주 생긴다.

라이브러리에 어떤 함수를 추가할때 이것을 새로운 기본 수단으로 작성할수도 있지만 이미 존재하는 기본수단을 재사용하는것이 더 좋을때가 많다.

기본적인 조합기들은 다소 까다로운 논리를 캡슐화하고 있는 경우가 많으며, 그런 조합기들을 재사용하면 까다로운 논리를 되풀이해서 다룰 필요가 없다.

API의 대수

앞에서도 다룬적이 있지만, "형식을 따라가다 보면" 구현에 도달하는 경우가 많다. 대수 방정식을 단순화할 때 하는 추론과 비슷하게 구체적인 문제 영역을 완전히 잊어버리고 형식들이 잘 맞아떨어지게 하는 데에만 집중할 수 있다.

API를 하나의 대수(algebra), 즉 일단의 법칙(law) 또는 참이라고 가정하는 속성(property)들을 가진 추상적인 연산 집합으로 간주하고 거기에 정의된 규칙에 따라 형식적으로 기호를 조작하면서 문제를 풀어나간다.

이 부분의 설명들은 직접적으로 지칭하지는 않지만 functor law, monad law등이 왜 중요한지를 잘 설명해주고 있다.

map에 관한 법칙

y = unit x라면
f <$> unit x = unit (f x)
id <$> unit x = unit (id x)
id <$> unit x = unit x
id <$> y = id y
id <$> y = y
f <$> y = f y
모두 같은 표현이며 map에 관한 법칙만 이야기하면 충분하며 unit의 언급은 군더더기이다.

이같은 법칙의 기반에서 map, unit의 구현은 하향 캐스팅이나 형식 캐스팅등을 허용할 수 없다. map은 단지 함수 f에 x의 결과를 적용할 뿐이다. map은 구조 보존적(structure-preserving)이어야 하며 Par에서는 병렬 계산의 구조를 변경해서는 안 되며, 오직 계산 '내부'의 값만 변경해야한다.

연습문제 7.7

f <$> (g <$> y) = f.g <$> y
f.id = id.f = f 이고 (compose도 map이다.)
id <$> (g <$> y) = id.g <$> y
id <$> y = y 이기 때문에
g <$> y = g <$> y

fork에 관한 법칙

fork는 병렬 계산의 결과에 영향을 미치지 말아야 하기 때문에 fork(x) = x이다. x와 동일한 일을 수행하되, 주 스레드가 아닌 개별적인 논리 스레드에서 비동기적으로 수행한다는 것이 fork에 대한 우리의 기대이다.

이런 법칙에 대해서 구현자의 관점이 아닌 디버거의 관점에서 반례(conterexample)를 생각해내면서 법칙이 깨지는 경우를 찾아보자.

코드에 관한 법칙과 증명이 중요한 이유

구성요소를 블랙박스로 취급하지 못하게 만드는 숨겨진 또는 부차적인 가정이 존재하면 합성이 어렵거나 불가능해진다. 이것은 공통의 기능성을 추출하고 합성을 통한 재사용 가능한 일반적 구성요소를 만들기 어렵게 한다. 또한 이런 법칙들이 성립하지 않는다면 여러 범용 조합기들이 더이상 유효하지 않게 된다.

반복해서 나오는 이야기지만 이것은 마치 대수적 방정식을 참이라고 가정한 속성들을 사용해서 증명하는것과 비슷한 효과를 지니게 한다.

법칙 깨기: 미묘한 버그 하나

fork(x) = x에서 x로 적합한 것은 fork, unit, map2 그리고 이들로부터 파생된 조합기들을 이용한 어떤 표현식이다.

ExecutorService의 구현은 교착에 빠지기가 쉬우며, 이로 인해 현재의 fork구현은 교착 상태로 빠지게 될 것이다.

이런 문제를 해결하기 위해서는 법칙이 성립하도록 구현을 고치거나 법칙이 성립하는 조건들을 좀 더 명시적으로 밝히도록 법칙을 정련할 수 있다.(스레드풀이 부족해지면 증가한다는 조건을 추가하는 등) 이것은 암묵적으로 존재했던 불변식(invariant)이나 가정을 명시적으로 문서화하게 만든다.

현재의 fork 구현을 교착을 방지하게 구현을 수정하면 개별적인 논리적 스레드를 띄워서 계산을 실행하는 대신 주 스레드에서 실행이 된다. 이것은 fork를 만들어 피하고자 했던 상황을 반복되게 만든다.

수정된 버전의 fork는 계산의 인스턴스화를 미루는 용도로 바꼈을 뿐 쓸모가 없어진건 아니다. 이 구현의 이름을 더 적합한 delay로 바꾸자.

또한 고정 크기 스레드 풀로 임의의 계산을 돌리기 위해서 Par를 다른 방식으로 구현하자.

0개의 댓글