par_map: 흔한 경우를 인간적으로

spawn과 channel은 병렬성의 어셈블리 언어 —— 옳지만, 대부분의 코드가 써야 할 것은 아니다. 병렬 맵은 사람이 실제로 손을 뻗는 형태이고, Mere는 그것을 네 번 쓰지 않고 네 backend에서 얻는다: par_map은 spawn·channel·평범한 map으로 desugar된다. 그것이 lower하는 primitive에서 모든 보장 —— 안전성, 바이트 단위의 parity —— 을 공짜로 잇고, 그리고 앞선 결정으로부터의 한 기미가, desugar를 call site마다 일어나게 한다.

mereconcurrencypar_mapdesugaringlanguage-design

spawnchannel은 이제 모든 backend에서 안전하게 돌지만, 그것들은 생 primitive —— 병렬성의 어셈블리 언어다. 그것들로 병렬 계산을 쓰는 것은, 일을 spawn된 스레드로 손으로 fan-out하고, 결과를 channel로 fan-in하는 것을, 매번, 손으로 뜻한다. 그것은 primitive에는 괜찮고, 일상의 도구로는 적대적이다. 이 짧은 회는, 대부분의 코드가 실제로 원하는 인간적인 형태 —— 병렬 —— 에 대해, 그리고 그것을 네 번 쓰지 않고 네 backend에서 얻는 것에 대한 것이다.

사람이 손을 뻗는 형태

일상의 병렬 조작은 맵이다: 리스트의 모든 요소에 함수를 적용하되, 요소를 동시에 한다. par_map은 꼭 그것을 준다 —— 함수와 리스트를 취해, 결과의 리스트를, 병렬로 계산해 돌려준다:

par_map heavy_work items

한 줄이, 손으로 쓴 fan-out과 fan-in을 대신한다. “요소마다 worker를 spawn하고, channel로 순서대로 모은다”를 써 내던 데이터 병렬 프로그램이, 단일한 호출이 된다. 그것이 콤비네이터의 요점의 전부다: primitive가 제어를 주고, 콤비네이터가 흔한 경우를 의식 없이 준다.

네 구현이 아니라, desugar로 세운다

par_map을 더하는 솔깃한 방식은, 각 backend의 런타임에서 구현하는 것 —— 써서 일치에 유지해야 할, 네 병행 코드가 더 —— 일 것이다. Mere는 반대로 했다. par_mapdesugar로 실현된다: saturated한 호출이, 프로그램이 파싱되는 대로, 이미 존재하는 것의 조합으로 재작성된다. 대략, 각 요소는 자기 결과를 새 channel에 보내는 worker를 얻고, channel은 순서대로 리스트에 모아지고, 그다음 각 channel이 같은 순서로 수신된다 —— fan-out 맵에 이은 fan-in 맵, spawn·channel·평범한 리스트 map으로 표현된.

spawn·channel·map이 이미 네 backend에서 돌기에, 이 단일한 재작성이 par_map을 네 backend에서 공짜로 돌린다. 새 코드 생성 없음, 네 겹 구현 없음, backend가 어긋날 새 기회 없음. 그것이 desugar의 일반적인 힘이다: 새 구문을, 이미 도는 것으로 표현하면, 그것은 그 도는 것을 통째로 잇는다. 순서의 보장은 channel을 리스트에 모으는 것에서 오고; 안전성은 primitive에서 오고; 바이트 단위의 parity도 primitive에서 온다. par_map은 그 어느 것도 벌 필요가 없었다 —— 그것이 lower하는 곳에 건네졌다.

왜 desugar가 call site마다 일어나는가

여기에 기미가 있고, 그것은 두 회 전에 내린 결정의 직접적 귀결이다. 왜 par_map을 한 번, 표준 라이브러리의 평범한 다형 함수로, Mere 자신으로 쓰지 않는가? Send 바운드가 단형화 제약에 곧장 부딪히기 때문이다: 그 타입 변수가 아직 채워지지 않은 Send 의무를 나르는 정의는 generalize되지 않고, 그래서 평범한 라이브러리 함수로 쓰인 par_map은 1 프로그램 1 요소 타입에 고정된다 —— 어떤 타입의 리스트를 맵할 수 있지만, 같은 프로그램의 다른 데서 다른 타입의 리스트를 못 한다. 그것은, 모두가 손을 뻗는 유일한 조작에는, 너무 제한적이다.

desugar는 그것을 꼭 회피한다. 각 par_map 호출이 그 자신의 call site에서, 그 사이트의 구체 타입으로 lower되기에, 모든 사용이 자기 특수화된 전개를 얻는다 —— 그리고 조작은 프로그램 전체에서 진짜로 다형이다, 여기서 한 타입, 저기서 다른 타입, 단형화 제약이 물지 않고, 제한할 단일한 generalize된 정의가 없기에. 앞선 선택 —— Send 제약 변수를 generalize하기를 거부하여 타입 시스템을 solver 없이 유지한다 —— 에는 대가가 있었고, 여기가 그 대가가 치러지고 회피되는 곳이다: 다형 콤비네이터가, 라이브러리 함수가 아니라 lowering으로 배달된다. 결정은 합성된다; 한 답의 형태가, 세 회 전의 답에 정해진다.

공짜로 딸려 오는 것

primitive가 보장하는 모든 것을, desugar된 par_map이 여분의 일 없이 보장한다. Send 요건은 여전히 강제된다 —— 결과는 channel을 통해 이동하고, 그 요소 타입은 보낼 수 있어야 하며, 보낼 수 없는 값을 반환하는 par_map은 거부된다, 생 channel이 그것을 거부하는 것과 꼭 같이. move 추적도 변경을 요하지 않는다: par_map에 넘겨지는 함수는 평범한, 여러 번 돌 수 있는 클로저이므로, owned cap을 그것에 move하려는 것은 multi-run 규칙으로 이미 금지되고, 그것은 요소마다 한 번 도는 함수에 꼭 옳다. 그리고 네 backend는 여전히 동일한 결과를 낳는다, 진짜 병렬 프로그램으로 검증되어, 모두 같은 lower된 primitive를 돌리기에.

그것이 확립하는 수법은, 이 하나의 콤비네이터보다 오래 남는다: 고수준의 병렬 조작은, 그것을 spawnchannel로 desugar함으로써 배달되고, 아래에서 안전성과 parity를 잇고, 미래의 콤비네이터는, 저마다 네 번 세워지는 것이 아니라, 같은 길을 따를 수 있다.

par_map은 안전한 primitive 위의 인간적인 표면이다 —— 그리고 그 존재가, 다음 회가 정말로 다루는 물음을 세운다. 병행 처리 로드맵에는 더 있었다: atomic, mutex, “완전한” 병행 처리의 이야기가 포함해야 할, 더 저수준의 공유 상태 기구. 그것들은 세워질 필요가 있는가? 답은 체크리스트에서가 아니라, 수요에서 왔다 —— 어느 프로그램이 실제로 그것들을 요하는가를 묻는 것에서. 다음 회, Part VII의 마무리: 무언가를 만들지 않는다는 결정.

← Back to Mere: 언어를 만들다