문서의 선택한 두 판 사이의 차이를 보여줍니다.
다음 판 | 이전 판 | ||
비동기_프로그래밍과_scala [2017/02/26 05:57] cumul0529 만듦 |
비동기_프로그래밍과_scala [2020/08/09 07:39] (현재) cumul0529 [함수형 프로그래밍과 타입 클래스] 마크업 수정 |
||
---|---|---|---|
줄 1: | 줄 1: | ||
- | 비동기(asynchrony)는 어떤 곳에서도 사용될 | + | {{tag> |
- | ====== | + | ====== |
+ | |||
+ | 비동기(asynchrony)는 어떤 곳에서도 사용될 수 있는, 병행성(concurrency)을 포괄하는 | ||
+ | |||
+ | ===== 소개 | ||
비동기는 멀티스레딩(multithreading)보다 넓은 개념임에도 불구하고, | 비동기는 멀티스레딩(multithreading)보다 넓은 개념임에도 불구하고, | ||
줄 8: | 줄 12: | ||
Multithreading <: Asynchrony | Multithreading <: Asynchrony | ||
</ | </ | ||
+ | |||
비동기 연산은 다음과 같이 형(type)을 통해서 표현할 수도 있습니다. | 비동기 연산은 다음과 같이 형(type)을 통해서 표현할 수도 있습니다. | ||
줄 13: | 줄 18: | ||
type Async[A] = (Try[A] => Unit) => Unit | type Async[A] = (Try[A] => Unit) => Unit | ||
</ | </ | ||
+ | |||
여러 개의 '' | 여러 개의 '' | ||
- | - 프로그램의 메인 플로우 밖에서 실행되거나, | + | - (호출자의 입장에서) |
- 작업이 끝나고 나면 실행되는 콜백을 사용함 | - 작업이 끝나고 나면 실행되는 콜백을 사용함 | ||
- | - 결과가 | + | - 결과가 나올 것인지, 혹은 언제 |
- | 비동기가 병행성(concurrency)을 포괄하긴 하지만, 그것이 필연적으로 멀티스레딩을 포괄하는 것이 아님은 매우 중요합니다. 예를 들어, JavaScript에서는 대부분의 I/O 동작이 비동기적일 뿐만 아니라, 아주 무거운 비지니스 로직도 ('' | + | 비동기가 병행성(concurrency)을 포괄하긴 하지만, 그것이 필연적으로 멀티스레딩을 포괄하는 것이 아님은 매우 중요합니다. 예를 들어, JavaScript에서는 대부분의 I/O 동작이 비동기적일 뿐만 아니라, 아주 무거운 비지니스 로직도 ('' |
- | 프로그램에 비동기를 도입하는 일은, 병행성 문제(concurrency problems)를 일으킵니다. 우리는 비동기 연산이 언제 끝날지 절대로 알 수 없어서, 동시에 작동한 여러 개의 비동기 연산의 결과를 모으는 일은 동기화(synchronization)를 필요로 하게 되기 때문입니다. 동기화 작업은 프로그램이 더 이상 동작의 순서에 의존하지 않도 하는 작업이기도 한데, 순서로부터의 독립은 바로 비결정론적 알고리즘의 핵심 요소이기도 합니다. | + | 프로그램에 비동기를 도입하는 일은, 병행성 문제(concurrency problems)를 일으킵니다. 우리는 비동기 연산이 언제 끝날지 절대로 알 수 없어서, 동시에 작동한 여러 개의 비동기 연산의 결과를 모으는 일은 동기화(synchronization)를 필요로 하게 되기 때문입니다. 동기화 작업은 프로그램이 더 이상 동작의 순서에 의존하지 않도록 하는 작업이기도 한데, 순서로부터의 독립은 바로 비결정론적 알고리즘의 핵심 요소이기도 합니다. |
- | > [[https:// | + | < |
+ | // | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | {{비동기_프로그래밍과_scala: | ||
실력 있는 독자라면, | 실력 있는 독자라면, | ||
줄 38: | 줄 49: | ||
위의 모든 추상화들은 비동기를 처리하는 조금 나은 방법들입니다. | 위의 모든 추상화들은 비동기를 처리하는 조금 나은 방법들입니다. | ||
- | ====== 커다란 허상 | + | ===== 커다란 허상 ===== |
비동기적인 결과를 동기적인 결과로 변환하는 함수는 흔히 아래와 같이 기술됩니다. | 비동기적인 결과를 동기적인 결과로 변환하는 함수는 흔히 아래와 같이 기술됩니다. | ||
줄 45: | 줄 56: | ||
def await[A](fa: | def await[A](fa: | ||
</ | </ | ||
+ | |||
그러나 우리는 비동기 처리가 다른 일반적인 함수들과 같다고 가정해서는 안됩니다. 그 이유는 CORBA의 실패로부터도 배울 수 있습니다. | 그러나 우리는 비동기 처리가 다른 일반적인 함수들과 같다고 가정해서는 안됩니다. 그 이유는 CORBA의 실패로부터도 배울 수 있습니다. | ||
줄 66: | 줄 78: | ||
* Scheme의 [[https:// | * Scheme의 [[https:// | ||
* C#, [[https:// | * C#, [[https:// | ||
- | * 런타임에 관리되는 [[https:// | + | * 런타임에 관리되는 [[https:// |
* Erlang과 Akka에 구현된 [[https:// | * Erlang과 Akka에 구현된 [[https:// | ||
* 순서를 지정하고 결과를 통합하는 모나드. Haskell에서 [[https:// | * 순서를 지정하고 결과를 통합하는 모나드. Haskell에서 [[https:// | ||
- | 이렇게 해결법이 많다는 것은, 그 중에 무엇도 일반적인 목적으로 비동기를 처리하기에 적합하지 않다는 것을 의미합니다. 메모리 관리와 | + | 이렇게 해결법이 많다는 것은, 그 중에 무엇도 일반적인 목적으로 비동기를 처리하기에 적합하지 않다는 것을 의미합니다. 메모리 관리와 |
- | > 원주: 경고 | + | > **원주** 경고: 개인적인 의견과 불평입니다. |
> | > | ||
> 어떤 사람들은 Golang과 같은 M:N 플랫폼을 자랑합니다만, | > 어떤 사람들은 Golang과 같은 M:N 플랫폼을 자랑합니다만, | ||
줄 80: | 줄 92: | ||
> 모든 가능한 해결법을 배우고 결정을 내리는 것은 실로 엄청나게 고통스러운 작업이지만, | > 모든 가능한 해결법을 배우고 결정을 내리는 것은 실로 엄청나게 고통스러운 작업이지만, | ||
- | ====== 콜백 지옥 | + | ===== 콜백 지옥 ===== |
예를 들어서, 두 개의 비동기 작업을 시작하고 그 결과들을 합친다고 합시다. | 예를 들어서, 두 개의 비동기 작업을 시작하고 그 결과들을 합친다고 합시다. | ||
줄 106: | 줄 118: | ||
</ | </ | ||
- | ===== 연속 실행 (side-effect의 연옥) | + | ==== 연속 실행 (side-effect의 연옥) ==== |
하나의 실행이 끝나면 다음을 연속해서 실행하는 방법으로 다음과 같이 두 개의 비동기 결괏값을 합칠 수 있습니다. | 하나의 실행이 끝나면 다음을 연속해서 실행하는 방법으로 다음과 같이 두 개의 비동기 결괏값을 합칠 수 있습니다. | ||
줄 134: | 줄 146: | ||
</ | </ | ||
- | 하지만 EJB를 사용하려는 EA가 여러분에게 순수 함수가 아닌 비동기 '' | + | 하지만 EJB를 사용하려는 EA가 여러분에게 순수 함수가 아닌 비동기 '' |
게다가 문제는 점점 더 복잡해집니다. 😷 | 게다가 문제는 점점 더 복잡해집니다. 😷 | ||
- | ===== 병렬 실행 (비결정론의 림보) | + | ==== 병렬 실행 (비결정론의 림보) ==== |
- | 위의 ' | + | 위의 ' |
안타깝게도 병렬 실행에서는 일이 조금 복잡해집니다. 아래와 같은 순진한 접근 방법은 완전히 틀렸습니다. | 안타깝게도 병렬 실행에서는 일이 조금 복잡해집니다. 아래와 같은 순진한 접근 방법은 완전히 틀렸습니다. | ||
<code scala> | <code scala> | ||
- | // 영 좋지 못한 | + | // 나쁜 |
def timesFourInParallel(n: | def timesFourInParallel(n: | ||
onFinish => { | onFinish => { | ||
줄 166: | 줄 177: | ||
</ | </ | ||
- | 바로 이것이 비결정론의 실례(實例)입니다. 두 개의 작업이 종료되는 순서가 보장되지 않기 때문에 우리는 동기화를 수행하는 소규모의 상태 기계(state machine)를 구현해야 합니다. | + | 바로 이것이 비결정론의 실제 예시입니다. 두 개의 작업이 종료되는 순서가 보장되지 않기 때문에 우리는 동기화를 수행하는 소규모의 상태 기계(state machine)를 구현해야 합니다. |
먼저 상태 기계의 ADT를 정의합니다. | 먼저 상태 기계의 ADT를 정의합니다. | ||
줄 184: | 줄 195: | ||
<code scala> | <code scala> | ||
- | // JVM에서는 | + | // JVM에서는 |
def timesFourInParallel(n: | def timesFourInParallel(n: | ||
onFinish => { | onFinish => { | ||
줄 219: | 줄 229: | ||
위의 해결법을 시각화하면 아래와 같은 상태 기계로 나타낼 수 있습니다. | 위의 해결법을 시각화하면 아래와 같은 상태 기계로 나타낼 수 있습니다. | ||
+ | {{ 비동기_프로그래밍과_scala: | ||
하지만 아직 문제가 남아있습니다. JVM은 1:1 멀티스레딩 플랫폼이기 때문에 공유 메모리에 병행적으로 접근(shared memory concurrency)하게 됩니다. 따라서 '' | 하지만 아직 문제가 남아있습니다. JVM은 1:1 멀티스레딩 플랫폼이기 때문에 공유 메모리에 병행적으로 접근(shared memory concurrency)하게 됩니다. 따라서 '' | ||
줄 292: | 줄 302: | ||
</ | </ | ||
- | > 원주: [[https:// | + | > **원주** [[https:// |
이제 조금 어려워졌나요? | 이제 조금 어려워졌나요? | ||
- | ===== 재귀 실행 (분노의 StackOverflow) | + | ==== 재귀 실행 (분노의 StackOverflow) ==== |
- | 위의 '' | + | 위의 '' |
말 그대로의 의미입니다. 앞서 작성한 코드를 제네릭한 방법(generic way)으로 다시 작성해 봅시다. | 말 그대로의 의미입니다. 앞서 작성한 코드를 제네릭한 방법(generic way)으로 다시 작성해 봅시다. | ||
줄 400: | 줄 410: | ||
</ | </ | ||
- | 앞서 말씀드린 것과 같이, 강제된 비동기 범위 없이 실행된 '' | + | 앞서 말씀드린 것과 같이, 강제된 비동기 범위 없이 실행된 '' |
- | ====== Future와 Promise | + | ===== Future와 Promise ===== |
- | '' | + | '' |
- | > [[https:// | + | < |
+ | Future와 Promise는 일부 병행 프로그래밍 언어(concurrent programming language)에서 프로그램의 실행을 동기화하기 위해서 사용하는 생성자이다. Future와 Promise가 기술하는 객체는 연산이 아직 완료되지 않아 초기에는 계산할 수 없는 값에 대한 대리자(proxy)이다. | ||
+ | < | ||
+ | </ | ||
- | > 원주: 필자의 불평입니다. | + | > **원주** 필자의 불평: |
> | > | ||
- | > Future와 Promise에 관한 | + | > Future와 Promise에 관한 [[http:// |
> | > | ||
> " | > " | ||
> | > | ||
- | > '' | + | > '' |
'' | '' | ||
줄 431: | 줄 444: | ||
// 값 변형 | // 값 변형 | ||
def map[U](f: T => U)(implicit ec: ExecutionContext): | def map[U](f: T => U)(implicit ec: ExecutionContext): | ||
+ | |||
// 연속 실행 ;-) | // 연속 실행 ;-) | ||
def flatMap[U](f: | def flatMap[U](f: | ||
+ | |||
// ... | // ... | ||
} | } | ||
줄 439: | 줄 454: | ||
'' | '' | ||
- | * [[https:// | + | * [[https:// |
- | * [[https:// | + | * [[https:// |
* 단일한 값을 흘려보내고(stream) 나타냅니다(show). 메모아이제이션이 적용되었기 때문입니다. 따라서 작업 완료에 대한 소비자(listener)는 최대 한 번까지만 호출됩니다. | * 단일한 값을 흘려보내고(stream) 나타냅니다(show). 메모아이제이션이 적용되었기 때문입니다. 따라서 작업 완료에 대한 소비자(listener)는 최대 한 번까지만 호출됩니다. | ||
줄 449: | 줄 464: | ||
* 모든 콤비네이터와 유틸리티들은 '' | * 모든 콤비네이터와 유틸리티들은 '' | ||
- | 왜 모든 시그니처에 '' | + | 왜 모든 시그니처에 '' |
- | ===== 연속 실행 | + | ==== 연속 실행 ==== |
- | 3장에서 만든 함수를 '' | + | [[비동기_프로그래밍과_scala# |
<code scala> | <code scala> | ||
줄 472: | 줄 487: | ||
아주 쉽습니다. '' | 아주 쉽습니다. '' | ||
- | 3.1절의 연속 실행은 다음과 같이 구현할 수 있습니다. | + | [[비동기_프로그래밍과_scala# |
<code scala> | <code scala> | ||
줄 486: | 줄 501: | ||
} | } | ||
</ | </ | ||
- | 역시 아주 쉬워졌습니다. 위의 for-컴프리헨션은 아래와 같이 '' | + | 역시 아주 쉬워졌습니다. 위의 for-comprehension은 아래와 같이 '' |
<code scala> | <code scala> | ||
줄 514: | 줄 529: | ||
<code scala> | <code scala> | ||
- | // 영 좋지 못한 | + | // 나쁜 |
def sum(list: List[Future[Int]])(implicit ec; ExecutionContext): | def sum(list: List[Future[Int]])(implicit ec; ExecutionContext): | ||
async { | async { | ||
줄 529: | 줄 544: | ||
'은 총알은 없다' | '은 총알은 없다' | ||
- | ===== 병렬 실행 | + | ==== 병렬 실행 ==== |
- | 3.2절에서와 마찬가지로 두 번의 함수 호출은 서로에 대해 독립적이기 때문에 병렬적으로 실행할 수 있습니다. 초보자에게는 계산 구문(evaluation semantics)이 조금 난해할 수 있지만, '' | + | [[비동기_프로그래밍과_scala# |
<code scala> | <code scala> | ||
def timesFourInParallel(n: | def timesFourInParallel(n: | ||
- | // Future는 | + | // Future는 |
val fa = timesTwo(n) | val fa = timesTwo(n) | ||
val fb = timesTwo(n) | val fb = timesTwo(n) | ||
줄 553: | 줄 568: | ||
</ | </ | ||
- | 이 방법도 초보자에게는 조금 충격적일 수도 있습니다. 여기서 '' | + | 이 방법도 초보자에게는 조금 충격적일 수도 있습니다. 여기서 '' |
- | ===== 재귀 실행 | + | ==== 재귀 실행 ==== |
- | '' | + | '' |
<code scala> | <code scala> | ||
줄 591: | 줄 606: | ||
</ | </ | ||
- | ===== 성능 고려사항 | + | ==== 성능 고려사항 ==== |
'' | '' | ||
줄 618: | 줄 633: | ||
CPU-bound 작업은 [[https:// | CPU-bound 작업은 [[https:// | ||
- | > 원주: 이 벤치마크 결과는 한정적입니다. 여전히 '' | + | > **원주** 이 벤치마크 결과에는 한계가 있습니다. 여전히 '' |
+ | |||
+ | ===== Task와 Scala의 IO 모나드 ===== | ||
+ | |||
+ | '' | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | > 또 '' | ||
+ | > | ||
+ | > Scalaz은 동기 작업만을 위한 별도의 IO형을 갖고 있습니다. 즉 Scalaz의 IO형은 비동기가 아니기 때문에 JVM에서 어떻게든 별도로 비동기 연산을 처리할 필요가 생깁니다. 반면 Haskell에서는 Async형이 있고, 이 Async형은 IO형으로 변환되기 때문에 런타임에 관리될 수 있습니다. (이때 그린스레드 등이 사용됩니다.) | ||
+ | > | ||
+ | > JVM를 위한 Scalaz의 구현을 따르면, 비동기 연산을 IO형으로 나타낼 방법이 없습니다. 또 Scalaz의 구현에서는 스레드를 봉쇄(block)하지 않고도 비동기 연산을 처리할 방법이 없습니다. 그러나 [[https:// | ||
+ | |||
+ | 요약하자면 '' | ||
+ | |||
+ | * Lazy하고 비동기적인 연산을 표현합니다. | ||
+ | * 하나 혹은 여러 개의 소비자(consumer)에게 오직 하나의 값만을 전달하는 생산자(producer)를 표현합니다. | ||
+ | * Lazy evaluation을 수행하기 때문에, '' | ||
+ | * 메모아이제이션이 가능합니다. | ||
+ | * 반드시 다른 논리 스레드에서 실행되어야 하는 것은 아닙니다. | ||
+ | |||
+ | 특히 Monix의 구현에서 '' | ||
+ | |||
+ | * 연산을 도중에 취소할 수 있습니다. | ||
+ | * 그 어떤 스레드도 절대 봉쇄(block)하지 않습니다. | ||
+ | * 스레드를 봉쇄(block)할 가능성이 있는 그 어떤 API도 호출하지 않습니다. | ||
+ | * 모든 비동기 작업은 스택-안전(stack-safe)합니다. | ||
+ | |||
+ | Monix의 구현에서 '' | ||
+ | |||
+ | ^ ^ Eager ^ Lazy ^ | ||
+ | | 동기 작업 | ||
+ | | ::: | ::: | [[https:// | ||
+ | | 비동기 작업 | (A => Unit) => Unit | (A => Unit) => Unit | | ||
+ | | ::: | Future[A] | ||
+ | |||
+ | ==== 연속 실행 ==== | ||
+ | |||
+ | [[비동기_프로그래밍과_scala# | ||
+ | |||
+ | <code scala> | ||
+ | import monix.eval.Task | ||
+ | |||
+ | def timesTwo(n: Int): Task[Int] = | ||
+ | Task(n * 2) | ||
+ | |||
+ | // 사용법 | ||
+ | { | ||
+ | // 계산되는 시점에 ExecutionContext가 필요합니다 | ||
+ | import monix.execution.Scheduler.Implicits.global | ||
+ | |||
+ | timesTwo(20).foreach { result => println(s" | ||
+ | //=> 결과: 40 | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 이 코드는 '' | ||
+ | |||
+ | 이제 [[비동기_프로그래밍과_scala# | ||
+ | |||
+ | <code scala> | ||
+ | def timesFour(n: | ||
+ | for (a <- timesTwo(n); | ||
+ | |||
+ | // Usage | ||
+ | { | ||
+ | // 계산되는 시점에 ExecutionContext가 필요합니다 | ||
+ | import monix.execution.Scheduler.Implicits.global | ||
+ | |||
+ | timesFour(20).foreach { result => println(s" | ||
+ | //=> 결과: 80 | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | <code scala> | ||
+ | def timesFour(n: | ||
+ | timesTwo(n).flatMap { a => | ||
+ | timesTwo(n).map { b => a + b } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== 병렬 실행 ==== | ||
+ | |||
+ | '' | ||
+ | |||
+ | 그러나 다음과 같이 단순히 '' | ||
+ | |||
+ | <code scala> | ||
+ | // 나쁜 예 (이 코드는 여전히 연속 실행됩니다) | ||
+ | def timesFour(n: | ||
+ | // Task는 lazy하게 계산되므로 여기서는 아직 계산되지 않습니다. | ||
+ | val fa = timesTwo(n) | ||
+ | val fb = timesTwo(n) | ||
+ | // Lazy evaluation으로 인해 값이 연속으로 계산됩니다. | ||
+ | for (a <- fa; b <- fb) yield a + b | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | <code scala> | ||
+ | def timesFour(n: | ||
+ | Task.mapBoth(timesTwo(n), | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | ==== 재귀 실행 ==== | ||
+ | |||
+ | '' | ||
+ | |||
+ | '' | ||
+ | |||
+ | <code scala> | ||
+ | def sequence[A](list: | ||
+ | val seed = Task.now(List.empty[A]) | ||
+ | list.foldLeft(seed)((acc, | ||
+ | .map(_.reverse) | ||
+ | } | ||
+ | |||
+ | // 사용법 | ||
+ | { | ||
+ | // 계산되는 시점에 ExecutionContext가 필요합니다 | ||
+ | import monix.execution.Scheduler.Implicits.global | ||
+ | |||
+ | sequence(List(timesTwo(10), | ||
+ | // => List(20, 40, 60) | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ((**역주** 이 아래부터 6장에서는, | ||
+ | |||
+ | ===== 함수형 프로그래밍과 타입 클래스 ===== | ||
+ | |||
+ | '' | ||
+ | |||
+ | 이것이 바로 Hakell의 '' | ||
+ | |||
+ | 그렇다면 '' | ||
+ | |||
+ | 가능합니다. 우리는 이미 순차 실행을 추상화하는 '' | ||
+ | |||
+ | 마침 Scala는 상류 타입(higher kinded types)을 지원하는 몇 안되는 언어에 속하고 [[https:// | ||
+ | |||
+ | > **원주** 저자의 분노: '' | ||
+ | > | ||
+ | > 하지만 그러한 설명 방식은 Scala와 사용자 모두에게 민폐입니다. 다른 언어에서 저 개념들은 단순히 설명하기 어려운 디자인 패턴에 불과합니다. 대부분의 다른 언어들은 형에 대한 표현성이 부족하기 때문입니다. 저 개념들을 표현할 수 있는 언어는 손에 꼽습니다. 언어 사용자의 입장에서도 문제가 생겼을 때 저 개념들을 모른 채 관련된 자료를 검색하는 것은 매우 고통스러운 일입니다. | ||
+ | > | ||
+ | > 또 저는 이것이 모르는 것에 대한 본능적인 공포에서 나오는 일종의 [[https:// | ||
+ | |||
+ | ==== Monad (연속 실행과 재귀 실행) ==== | ||
+ | |||
+ | 이 글의 목적은 모나드에 대해 설명하는 것이 아닙니다. 모나드에 관해서는 다른 좋은 글들이 있습니다. 모나드는 잘 모르지만 비동기 프로그래밍에 대한 감을 쌓기 위해서 이 글을 읽고 있다면 적어도 알아두어야 할 것이 있습니다. '' | ||
+ | |||
+ | < | ||
+ | 관찰 결과: 명령형 언어(imperative language)로 병행성(concurrency)을 다루는 프로그래머들은 ";" | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | <code scala> | ||
+ | // 이 줄을 적지 않아도 된다면 좋을텐데 말이죠 :-( | ||
+ | import scala.language.higherKinds | ||
+ | |||
+ | trait Monad[F[_]] { | ||
+ | /** 생성자 (`A`를 `F[A]` Monad로 리프팅합니다.) | ||
+ | * `Applicative`의 일부이기도 합니다. 아래를 보세요. | ||
+ | */ | ||
+ | def pure[A](a: A): F[A] | ||
+ | |||
+ | /** 빰 */ | ||
+ | def flatMap[A, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 이어서 '' | ||
+ | |||
+ | <code scala> | ||
+ | import scala.concurrent._ | ||
+ | |||
+ | // Supplying an instance for Future isn't clean, ExecutionContext needed | ||
+ | class FutureMonad(implicit ec: ExecutionContext) | ||
+ | extends Monad[Future] { | ||
+ | |||
+ | def pure[A](a: A): Future[A] = | ||
+ | Future.successful(a) | ||
+ | |||
+ | def flatMap[A, | ||
+ | fa.flatMap(f) | ||
+ | } | ||
+ | |||
+ | object FutureMonad { | ||
+ | implicit def instance(implicit ec: ExecutionContext): | ||
+ | new FutureMonad | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | <code scala> | ||
+ | /** 피보나치 수열의 N번째 숫자를 구합니다. */ | ||
+ | def fib[F[_]](n: | ||
+ | def loop(n: Int, a: BigInt, b: BigInt): F[BigInt] = | ||
+ | F.flatMap(F.pure(n)) { n => | ||
+ | if (n <= 1) F.pure(b) | ||
+ | else loop(n - 1, b, a + b) | ||
+ | } | ||
+ | |||
+ | loop(n, BigInt(0), BigInt(1)) | ||
+ | } | ||
+ | |||
+ | // 사용법 | ||
+ | { | ||
+ | // 유효범위(scope)를 만듭니다. | ||
+ | import FutureMonad.instance | ||
+ | import scala.concurrent.ExecutionContext.Implicits.global | ||
+ | |||
+ | // 실행 | ||
+ | fib[Future](40).foreach(r => println(s" | ||
+ | //=> 결과: 102334155 | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | > **원주** 이 코드는 그저 장난 수준입니다. 본격적인 프로젝트를 보고 싶다면 Typelevel의 [[http:// | ||
+ | |||
+ | ==== Applicative (병렬 실행) ==== | ||
+ | |||
+ | 모나드는 실행의 순서를 정의합니다. 하지만 동시에 실행 가능한 서로 독립적인 연산들의 결과를 합쳐야 할 때도 있습니다. 그런 경우에는 '' | ||
+ | |||
+ | 먼저 간단한 타입백과사전(Typeclassopedia)을 만들어 봅시다. ((**역주** ' | ||
+ | |||
+ | <code scala> | ||
+ | trait Functor[F[_]] { | ||
+ | /** 이 코드는 독자 여러분 모두 이해하리라고 믿습니다. */ | ||
+ | def map[A, | ||
+ | } | ||
+ | |||
+ | trait Applicative[F[_]] extends Functor[F] { | ||
+ | /** 생성자 (`A`를 `F[A]` Applicative로 리프팅합니다.) */ | ||
+ | def pure[A](a: A): F[A] | ||
+ | |||
+ | /** 두 참조에 대해서 동시에 map을 실행합니다. | ||
+ | * | ||
+ | * 다른 구현체에서는 Applicative 연산자가 `ap`일 수 있습니다. | ||
+ | * 하지만 `map2` 구현이 훨씬 이해하기 쉽습니다. | ||
+ | */ | ||
+ | def map2[A, | ||
+ | } | ||
+ | |||
+ | trait Monad[F[_]] extends Applicative[F] { | ||
+ | def flatMap[A, | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 이어서 '' | ||
+ | |||
+ | <code scala> | ||
+ | class FutureMonad(implicit ec: ExecutionContext) | ||
+ | extends Monad[Future] { | ||
+ | |||
+ | def pure[A](a: A): Future[A] = | ||
+ | Future.successful(a) | ||
+ | |||
+ | def flatMap[A, | ||
+ | fa.flatMap(f) | ||
+ | |||
+ | def map2[A, | ||
+ | // flatMap에 기반하지 않은 구현체를 위한 함수이지만, | ||
+ | // Task는 해당되지 않습니다 ;-) | ||
+ | for (a <- fa; b <- fb) yield f(a,b) | ||
+ | } | ||
+ | |||
+ | object FutureMonad { | ||
+ | implicit def instance(implicit ec: ExecutionContext): | ||
+ | new FutureMonad | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 이제 '' | ||
+ | |||
+ | <code scala> | ||
+ | def sequence[F[_], | ||
+ | (implicit F: Applicative[F]): | ||
+ | |||
+ | val seed = F.pure(List.empty[A]) | ||
+ | val r = list.foldLeft(seed)((acc, | ||
+ | F.map(r)(_.reverse) | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | > **원주** 다시 한 번 강조하지만, | ||
+ | |||
+ | ==== 비동기 계산을 위한 타입 클래스를 정의할 수 있을까요? | ||
+ | |||
+ | 지금까지 살펴 본 내용에서 빠진 부분은 실제로 계산을 시작하고 결괏값을 받아오는 방법입니다. Scala의 '' | ||
+ | |||
+ | [[https:// | ||
+ | |||
+ | <code scala> | ||
+ | trait Effect[F[_]] extends Monad[F] { | ||
+ | def unsafeRunAsync[A](fa: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 우리가 처음에 만들었던 '' | ||
+ | |||
+ | 하지만 이것은 다음의 이유로 진짜 타입 클래스라고 하기는 어렵습니다. | ||
+ | |||
+ | * 이 구현은 불규칙적(lawless)입니다. 그러나 이것만으로 타입 클래스가 아니라고 하기는 곤란합니다. ('' | ||
+ | * [[비동기_프로그래밍과_scala# | ||
+ | |||
+ | 실행 문맥(execution context)을 지정하는 방법은 구현 방식에 따라 다릅니다. Java는 '' | ||
+ | |||
+ | 우리는 이와 같은 전략을 '' | ||
+ | |||
+ | 그래서 처음 주어진 질문(" | ||
+ | |||
+ | 여러분이 이 사고 실험을 재밌게 즐기셨다면 좋겠습니다. 설계는 늘 즐겁죠. 😎 | ||
+ | |||
+ | ===== 올바른 도구를 고르자 ===== | ||
+ | |||
+ | 어떤 추상화는 다른 추상화보다 더 일반적인 목적을 가지고 있습니다. 저는 개인적으로 " | ||
+ | |||
+ | 이 시점에서 우리는 Rúnar Bjarnason의 [[https:// | ||
+ | |||
+ | 이미 말했듯이, | ||
- | ====== Task와 Scala의 IO 모나드 ====== | + | 그리고 이 두 가지 규칙을 마음에 새기도록 합시다. |
+ | * 콜백, 스레드와 스레드 봉쇄(block)를 지양하자. 오류를 일으키기 쉽고 결과를 합치는 것도 불가능하다. | ||
+ | * 병행성(concurrency)을 지양하자. 그것은 전염병과 같다. | ||
+ | 마지막으로 이것만 말하겠습니다. 병행성(concurrency) 전문가들은 애초에 병행성이 생기지 않도록 하는 데 그 누구보다 전문적입니다. 💀 |