순수 함수적 병렬성 (1)

Jason Kim·2020년 5월 10일
0

fp in scala with TypeScript

목록 보기
16/17

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


2부의 목표

1부를 마치고 2부를 들어간다. 2부에서는 함수형 프로그래밍 관점에서의 라이브러리 설계를 다룬다. 우리의 주된 관심사는 합성 능력과 모듈화가 좋은 라이브러리를 만드는 것이며, 이를 위해서 계산의 서술과 계산의 실제 실행이라는 각각의 관심사를 분리하는것을 유지한다. 프로그램의 구체적인 실행 방식에 관한 세부사항과는 격리된 수준에서 프로그램을 작성할 수 있어야 한다.

병렬 처리의 어려움

공유 변이 가능 메모리를 이용한 스레드들 사이의 통신은 추론이 어렵다. 경쟁 조건과 교착 상태가 발생하고 손쉽게 검사하기 어려우며, 규모 가변성(scalability)도 좋지 않은 프로그램이 만들어지기 쉽다. 이 장에서는 병렬적 프로그램에 내재하는 복잡성을 순수 함수만으로 서술해 통제해 나갈 것이다. 이렇게 되면 치환 모형을 이용해서 추론을 단순화 할 수 있다.

parMap 조합기의 개발 과정

함수 f를 한 컬렉션의 모든 요소에 동시 적용하는 parMap이라는 조합기를 개발해 나갈 것이다. 이 라이브러리는 부수 효과를 절대로 허용하지 않는다.

  • 간단한 용례에서 출발
  • 그 용례를 촉진하는 인터페이스 개발
  • 인터페이스의 구현 방법을 고민
  • 인터페이스와 구현을 오가며 설계를 계속 개선
  • 더 복잡한 용례들을 거치면서 문제 영역과 설계 공간에 대한 이해도 상승

의 과정을 거치면서 대수적 추론에 역점을 두고 API를 특정 법칙(law)들을 따르는 하나의 대수(algebra)로서 서술할 수 있다는 착안을 소개한다.

자료 형식과 함수의 선택

함수적 라이브러리 설계 공정의 어려움은 아이디어를 정련(refining)하고 원하는 기능성을 가능하게 하는 자료 형식을 찾는 데 있다. 목록의 합을 구하는 것에서부터 출발하자.

병렬 계산을 생성한다는것은 무엇인가?

foldLeft를 사용해서 목록을 순차적으로 접는 대신 분할정복(divide-and-conquer) 알고리즘을 선택하면 두 절반을 병렬로 합할 수 있다.

이 병렬성을 어떻게 구현할것인지에 초점을 두는대신 이상적인 API를 직접 설계하고 그것으로부터 구현으로 되돌아가는 접근법을 사용한다.

간단한 예제의 중요성

복잡한 예제는 부차적인 자료구조나 핵심과 거리가 먼 세부사항들 때문에 초기 설계 과정이 혼란스러울 수 있다. 자명한 예제들로 시작해서 공통의 관심사를 추출하고 점차 복잡성을 더해 나가면서 문제 영역의 정수(essence)를 설명한다. 수많은 특수 경우들을 두는 대신 간단하고 함성 가능한 핵심 자료 형식들과 함수들을 구축함으로써 표현력을 갖춘다.

첫번째 병렬 계산

sum의 예제를 토대로 병렬 계산이 하나의 결과를 담을 수 있는 자료 형식임을 알 수 있다. 결과는 어떤 의미 있는 형식이어야 하며, 그 결과를 추출하는 수단도 있어야 한다.

  • unit
    • 평가되지 않은 표현을 받고 개별적인 스레드에서 평가 할 수 있는 계산을 돌려준다.
  • get
    • 병렬 계산에서 값을 추출한다.

unit과 get의 의미는 구현을 반복해 나가면서 더 정교한 의미를 가지게 된다. 지금 시점에서는 unit이 즉시 평가를 시작할지, get이 호출되면 평가를 시작할지 모호하다.

unit이 즉시 평가를 하도록 선택한다면 get은 전달받은 Par가 완료될때 까지 실행이 차단되는 부수효과가 발생하게 된다. 이는 unit에는 get에만 관련된 한정적인 부수 효과가 존재함을 의미한다.

관심사를 분리한다면

'논리적 스레드'를 주어진 문제에 필요한 만큼 얼마든지 생성할 수 있게 하고 실제 OS 스레드들에 대응 시키는것은 나중에 처리함으로서 계산의 서술과 실행을 분리하는것이 바람직한 방식이다.

병렬 계산의 조합

지금까지의 Par는 비동기 계산들의 완료되어야만 조합이 가능하다. sum 함수의 리턴 타입을 Int대신 Par<Int>바꾼다면 get을 호출하지 않음으로서 이런 문제를 피할 수 있다.

연습문제 7.1

https://github.com/JsonKim/fpinscala-with-typescript/commit/7a80ba3fd6a6285ee68b4bf8b0b6a960a8056578

엄격한 버전의 map2

엄격한 map2는 왼쪽에서 오른쪽으로 평가한다. 합산 트리의 왼쪽 절반 전체를 구축하고나서야 오른쪽 절반을 구축하는 문제가 있다.

map2를 엄격하게 유지하되 평가를 미룬다면 Par값이 단지 병렬로 계산해야 할 것의 서술을 구축하는 것임을 의미한다. 이 서술은 평가하기 전까지는 아무일도 일어나지 않는다.

문제는 서술이 수행할 연산들의 전체 트리를 담아야하기 때문에 상당히 무거운 객체가 될 것이라는 점이다.

map2를 게으르게 만들고 양변을 즉시 실행하도록 개선하자.

명시적 분기

현재의 API는 계산을 주 스레드로부터 분기하는 시점이 명료하지 않다. 프로그래머는 분기(forking)가 일어나는 시점을 지정할 수 없다. 주어진 Par가 개별 논리적 스레드에서 실행되어야함을 명시적으로 지정하는 함수 fork를 추가하자.

fork는 map2를 엄격한 함수로 유지하게 해주며, 병렬계산들에 대한 지연 평가 여부를 프로그래머가 결정함으로서 병렬성을 명시적으로 프로그래머 통제하에 두는 역할을 한다.

우리가 다루는 두 가지 관심사는 두 병렬 과제의 결과들이 조합되어야 함을 지저하는 수단이 필요하다는것과 특정 과제를 비동기적으로 수행할지 아닐지를 선택하는 수단이 필요하다는 것이다. 이런 관심사의 분리 덕분에 map2를 포함한 조합기들에 병렬성에 대한 전역 방침이 내장될 필요가 없다.

파생된(derived) 조합기

lazyUnit은 unit, fork로 구현된 파생 조합기이다. lazyUnit은 Par의 구체적인 표현은 아무것도 알 필요가 없으며 단지 Par의 정의된 연산 unit, fork를 거치는 것만 알고 있으면 된다.

평가의 책임은 누가?

fork와 get의 구현에 어떤 정보가 필요한가를 생각해보면 서로 다른 의미의 선택에 따른 장단점을 찾는데 도움이 된다.

fork가 자신의 인수를 즉시 병렬로 평가한다면 이것의 구현은 스레드의 구체적인 사용 방법에 대해서 알고 있어야 한다. 이것은 스레드 풀등의 자원이 전역적이어야하며 fork 호출 시점에 이미 초기화가 되어있어야 함을 의미한다.

fork의 의미를 평가되지 않은 Par 인수를 받고 그 인수에 동시적 평가가 필요하다는 점을 '표시'만 해두도록 선택한다면 Par는 병렬성의 구체적인 구현 방법을 알 필요가 없으며 get같은 함수에 의해 해석될 병렬 계산에 대한 서술에 가깝다.

이제 Par는 나중에 준비되었을때 조회(get)할 값을 담은 컨테이너가 아닌 실행이 가능한 일급 프로그램에 가까워졌다. get을 run으로 이름을 바꾸고 병렬성이 실제로 구현되는 지점으로 만들자.

정리하자면, Par는 병렬 계산을 서술만 하는 순수 자료구조이고 실제적인 병렬성의 구현은 run이 담당하게 되었다.

0개의 댓글