문서의 선택한 두 판 사이의 차이를 보여줍니다.
| 다음 판 | 이전 판 | ||
|
비동기_프로그래밍과_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) 전문가들은 애초에 병행성이 생기지 않도록 하는 데 그 누구보다 전문적입니다. 💀 | ||