JavaScript, Python, Ruby, Swift 및 Scala의 Option/Maybe, Each 및 Future 모나드

게시 됨: 2022-03-11

이 모나드 튜토리얼은 모나드에 대한 간략한 설명을 제공하고 5가지 다른 프로그래밍 언어에서 가장 유용한 것을 구현하는 방법을 보여줍니다. JavaScript의 모나드, Python의 모나드, Ruby의 모나드, Swift의 모나드 및/또는 모나드를 찾고 있다면 Scala에서 또는 구현을 비교하기 위해 올바른 기사를 읽고 있는 것입니다!

이러한 모나드를 사용하면 널 포인터 예외, 처리되지 않은 예외 및 경쟁 조건과 같은 일련의 버그를 제거할 수 있습니다.

이것은 내가 아래에서 다루는 내용입니다.

  • 범주 이론 소개
  • 모나드의 정의
  • JavaScript, Python, Ruby, Swift 및 Scala에서 Option("Maybe") 모나드, Both 모나드 및 Future 모나드의 구현과 이를 활용하는 샘플 프로그램

시작하자! 첫 번째 중지는 모나드의 기초가 되는 범주 이론입니다.

범주 이론 소개

범주 이론은 20세기 중반에 활발히 발전한 수학 분야입니다. 이제 모나드를 포함한 많은 함수형 프로그래밍 개념의 기초가 되었습니다. 소프트웨어 개발 용어에 맞게 조정된 몇 가지 범주 이론 개념을 간단히 살펴보겠습니다.

따라서 범주를 정의하는 세 가지 핵심 개념이 있습니다.

  1. 유형 은 정적으로 유형이 지정된 언어에서 보는 것과 같습니다. 예: Int , String , Dog , Cat
  2. 함수 는 두 가지 유형을 연결합니다. 따라서 그들은 한 유형에서 다른 유형으로 또는 자신에게 화살표로 표시될 수 있습니다. $T$ 유형에서 $U$ 유형까지의 함수 $f$는 $f: T \to U$로 표시될 수 있습니다. $T$ 유형의 인수를 사용하고 $U$ 유형의 값을 반환하는 프로그래밍 언어 함수로 생각할 수 있습니다.
  3. 합성 은 $\cdot$ 연산자로 표시되는 작업으로, 기존 기능에서 새 기능을 빌드합니다. 범주에서 $f: T \to U$ 및 $g: U \to V$에 대해 항상 보장되는 고유한 함수 $h: T \to V$가 있습니다. 이 함수는 $f \cdot g$로 표시됩니다. 이 작업은 한 쌍의 기능을 다른 기능에 효과적으로 매핑합니다. 물론 프로그래밍 언어에서 이 작업은 항상 가능합니다. 예를 들어 문자열의 길이를 반환하는 함수($strlen: String \to Int$)와 숫자가 짝수인지 알려주는 함수($even: Int \to Boolean$)가 있는 경우 function $even{\_}strlen: String \to Boolean$은 String 의 길이가 짝수인지 알려줍니다. 이 경우 $even{\_}strlen = even \cdot strlen$. 구성은 두 가지 기능을 의미합니다.
    1. 연관성: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
    2. 항등 함수의 존재: $\forall T: \exists f: T \to T$ 또는 일반 영어로 모든 유형 $T$에 대해 $T$를 자신에 매핑하는 함수가 있습니다.

그럼 간단한 카테고리를 살펴볼까요?

String, Int, Double 및 그 중 일부 기능을 포함하는 간단한 범주입니다.

참고 사항: 여기서 Int , String 및 기타 모든 유형은 null이 아닌 것으로 보장된다고 가정합니다. 즉, null 값이 존재하지 않습니다.

참고 사항 2: 이것은 실제로 범주의 일부일 뿐이지만 필요한 모든 필수 부분이 포함되어 있고 이 방법으로 다이어그램이 덜 복잡하기 때문에 이것이 우리가 토론에서 원하는 전부입니다. 실제 범주에는 범주의 구성 절을 충족시키기 위해 $roundToString: Double \to String = intToString \cdot round$와 같은 모든 구성된 함수도 있습니다.

이 범주의 기능은 매우 간단하다는 것을 알 수 있습니다. 사실 이러한 기능에 버그가 있는 것은 거의 불가능합니다. null도 예외도 없고 산술과 메모리 작업만 있을 뿐입니다. 따라서 발생할 수 있는 유일한 나쁜 일은 프로세서 또는 메모리 오류(이 경우 프로그램을 중단해야 하는 경우)이지만 매우 드물게 발생합니다.

모든 코드가 이 수준의 안정성에서 작동한다면 좋지 않을까요? 전적으로! 그러나 예를 들어 I/O는 어떻습니까? 우리는 확실히 그것 없이는 살 수 없습니다. 여기 모나드 솔루션이 도움이 됩니다. 모든 불안정한 작업을 매우 작고 감사가 잘 된 코드 조각으로 분리합니다. 그러면 전체 앱에서 안정적인 계산을 사용할 수 있습니다!

모나드 입력

I/O side effect 와 같은 불안정한 동작을 호출합시다. 이제 우리는 length 와 같은 이전에 정의된 모든 함수와 이 부작용 이 있는 상태에서 안정적인 방식으로 String 과 같은 유형을 사용할 수 있기를 원합니다.

따라서 빈 범주 $M[A]$로 시작하여 특정 유형의 부작용이 있는 값과 부작용이 없는 값이 있는 범주로 만들어 보겠습니다. 이 범주를 정의했고 비어 있다고 가정해 보겠습니다. 지금 당장은 우리가 할 수 있는 유용한 것이 없으므로 유용하게 만들기 위해 다음 세 단계를 따릅니다.

  1. String , Int , Double 등과 같은 $A$ 범주의 유형 값으로 채우십시오(아래 다이어그램의 녹색 상자).
  2. 일단 이 값을 갖게 되면 여전히 의미 있는 작업을 수행할 수 없으므로 각 함수 $f: T \to U$를 $A$에서 가져와서 $g: M[T] \to M 함수를 만드는 방법이 필요합니다. [U]$(아래 다이어그램의 파란색 화살표). 이러한 기능이 있으면 $A$ 범주에서 수행할 수 있었던 $M[A]$ 범주 값으로 모든 작업을 수행할 수 있습니다.
  3. 이제 우리는 완전히 새로운 $M[A]$ 범주를 갖게 되었으며 서명 $h와 함께 새로운 클래스의 함수가 나타납니다. T \to M[U]$(아래 다이어그램의 빨간색 화살표). 그것들은 코드베이스의 일부로 1단계에서 값을 승격한 결과로 나타납니다. 즉, 필요에 따라 작성합니다. 이것은 $M[A]$ 작업과 $A$ 작업을 구별하는 주요 사항입니다. 마지막 단계는 이러한 함수가 $M[A]$의 유형에서도 잘 작동하도록 하는 것입니다. 즉, $m: M[T] \to M[U]$ 함수를 $h: T \에서 파생시킬 수 있습니다. ~으로 M[U]$

새 범주 만들기: 범주 A 및 M[A], 그리고 A의 Double에서 M[A]의 Int까지 "roundAsync" 레이블이 지정된 빨간색 화살표. M[A]는 이 시점에서 A의 모든 값과 기능을 재사용합니다.

따라서 $A$ 유형의 값을 $M[A]$ 유형의 값으로 승격시키는 두 가지 방법을 정의하여 시작하겠습니다. 하나는 부작용이 없는 함수이고 다른 하나는 부작용이 있습니다.

  1. 첫 번째는 $pure$라고 하며 안정적인 범주의 각 값에 대해 정의됩니다. $pure: T \to M[T]$. 결과 $M[T]$ 값에는 부작용이 없으므로 이 함수를 $pure$라고 합니다. 예를 들어, I/O 모나드의 경우 $pure$는 실패 가능성 없이 즉시 일부 값을 반환합니다.
  2. 두 번째는 $constructor$라고 하며 $pure$와 달리 일부 부작용이 있는 $M[T]$를 반환합니다. 비동기 I/O 모나드에 대한 이러한 $constructor$의 예는 웹에서 일부 데이터를 가져와 String 으로 반환하는 함수일 수 있습니다. $constructor$에 의해 반환된 값은 이 경우 $M[String]$ 유형을 갖습니다.

이제 값을 $M[A]$로 승격하는 두 가지 방법이 있으므로 프로그램 목표에 따라 사용할 함수를 선택하는 것은 프로그래머의 몫입니다. 여기에서 예를 살펴보겠습니다. https://www.toptal.com/javascript/option-maybe-either-future-monads-js와 같은 HTML 페이지를 가져오고 이를 위해 $fetch$ 함수를 만듭니다. 네트워크 오류 등을 생각하면 가져오는 동안 문제가 발생할 수 있으므로 이 함수의 반환 유형으로 $M[String]$을 사용합니다. 따라서 $fetch: String \to M[String]$과 같이 보일 것이며 함수 본문의 어딘가에서 $M$에 대해 $constructor$를 사용할 것입니다.

이제 테스트를 위한 모의 함수 $fetchMock: String \to M[String]$을 만든다고 가정해 보겠습니다. 여전히 동일한 서명을 가지고 있지만 이번에는 불안정한 네트워크 작업을 수행하지 않고 결과 HTML 페이지를 $fetchMock$ 본문에 삽입합니다. 따라서 이 경우에는 $fetchMock$ 구현에 $pure$를 사용합니다.

다음 단계로 임의의 함수 $f$를 $A$ 범주에서 $M[A]$(다이어그램의 파란색 화살표)로 안전하게 승격시키는 함수가 필요합니다. 이 함수를 $map: (T \to U) \to (M[T] \to M[U])$라고 합니다.

이제 카테고리($constructor$를 사용하면 부작용이 있을 수 있음)가 생겼습니다. 이 카테고리에는 stable 카테고리의 모든 기능도 포함되어 있습니다. 즉, $M[A]$에서도 안정적입니다. $f: T \to M[U]$와 같은 다른 클래스의 함수를 명시적으로 도입했음을 알 수 있습니다. 예를 들어, $pure$ 및 $constructor$는 $U = T$에 대한 이러한 함수의 예이지만 $pure$를 사용한 다음 $map$를 사용하는 경우와 같이 분명히 더 있을 수 있습니다. 따라서 일반적으로 $f: T \to M[U]$ 형식의 임의 함수를 처리하는 방법이 필요합니다.

$M[T]$에 적용할 수 있는 $f$를 기반으로 하는 새 함수를 만들고 싶다면 $map$를 사용할 수 있습니다. 그러나 그것은 우리가 $g: M[T] \to M[M[U]]$ 기능을 하도록 할 것입니다. $M[M[A]]$ 범주를 하나 더 갖고 싶지 않기 때문에 이것은 좋지 않습니다. 이 문제를 다루기 위해 우리는 $flatMap: (T \to M[U]) \to (M[T] \to M[U])$라는 마지막 함수를 소개합니다.

하지만 왜 그렇게 하고 싶습니까? $pure$, $constructor$ 및 $map$가 있는 2단계 이후에 있다고 가정해 보겠습니다. toptal.com에서 HTML 페이지를 가져온 다음 거기에 있는 모든 URL을 스캔하고 가져오려고 한다고 가정해 보겠습니다. 하나의 URL만 가져와서 HTML 페이지를 반환하는 $fetch: String \to M[String]$ 함수를 만들겠습니다.

그런 다음 이 함수를 URL에 적용하고 toptal.com에서 $x: M[String]$인 페이지를 가져옵니다. 이제 $x$에 대해 약간의 변환을 수행하고 마침내 URL $u: M[String]$에 도달합니다. $fetch$ 기능을 적용하고 싶지만 $M[String]$이 아니라 $String$ 유형을 취하기 때문에 적용할 수 없습니다. 이것이 $fetch: String \to M[String]$을 $m_fetch: M[String] \to M[String]$으로 변환하기 위해 $flatMap$이 필요한 이유입니다.

이제 세 단계를 모두 완료했으므로 실제로 필요한 값 변환을 작성할 수 있습니다. 예를 들어, $M[T]$ 및 $f: T \to U$ 유형의 $x$ 값이 있는 경우 $map$를 사용하여 $f$를 값 $x$에 적용하고 $y$ 값을 얻을 수 있습니다. $M[U]$ 유형. $pure$, $constructor$, $map$ 및 $flatMap$ 구현이 버그가 없는 한 값의 모든 변환은 100% 버그 없는 방식으로 수행될 수 있습니다.

따라서 코드베이스에서 발생할 때마다 불쾌한 영향을 처리하는 대신 이 네 가지 기능만 올바르게 구현되었는지 확인하기만 하면 됩니다. 프로그램이 끝나면 $X$ 값을 안전하게 풀고 모든 오류 사례를 처리할 수 있는 $M[X]$ 하나만 얻을 수 있습니다.

이것이 모나드입니다: $pure$, $map$ 및 $flatMap$을 구현하는 것. (실제로 $map$은 $pure$와 $flatMap$에서 파생될 수 있지만 매우 유용하고 널리 사용되는 기능이므로 정의에서 생략하지 않았습니다.)

옵션 모나드, 일명 어쩌면 모나드

자, 모나드의 실제 구현과 사용법에 대해 알아보겠습니다. 첫 번째로 정말 유용한 모나드는 Option 모나드입니다. 고전 프로그래밍 언어를 사용하는 경우 악명 높은 null 포인터 오류로 인해 충돌이 많이 발생했을 것입니다. null의 발명가인 Tony Hoare는 이 발명을 "The Billion Dollar Mistake"라고 부릅니다.

이로 인해 지난 40년 동안 수십억 달러의 고통과 피해를 일으킨 수많은 오류, 취약성 및 시스템 충돌이 발생했습니다.

그래서 그것을 개선하기 위해 노력합시다. Option 모나드는 null이 아닌 값을 보유하거나 값을 보유하지 않습니다. null 값과 매우 유사하지만 이 모나드를 사용하면 null 포인터 예외를 두려워하지 않고 잘 정의된 함수를 안전하게 사용할 수 있습니다. 다양한 언어로 구현된 내용을 살펴보겠습니다.

JavaScript - 옵션 모나드/아마도 모나드

 class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }

Python - 옵션 모나드/아마도 모나드

 class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()

Ruby - 옵션 모나드/아마도 모나드

 class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()

Swift - 옵션 모나드/아마도 모나드

 import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }

스칼라 - 옵션 모나드/아마도 모나드

 import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]

모든 모나드 구현의 기반이 될 Monad 클래스를 구현하는 것으로 시작합니다. 이 클래스를 사용하면 매우 편리합니다. 특정 map 에 대해 pureflatMap 두 가지 메서드만 구현하면 많은 메서드를 무료로 얻을 수 있기 때문입니다. Monad 배열로 작업하기 위한 sequencetraverse 와 같은 다른 유용한 방법).

mappureflatMap 의 합성으로 표현할 수 있습니다. flatMap 의 서명 $flatMap: (T \to M[U]) \to (M[T] \to M[U])$ 에서 $map: (T \to U) \에 정말 가깝다는 것을 알 수 있습니다. (M[T] \로 M[U])$로. 차이점은 중간에 추가 $M$이지만 pure 함수를 사용하여 $U$를 $M[U]$로 변환할 수 있습니다. 그런 식으로 우리는 flatMappure 의 관점에서 map 을 표현합니다.

이것은 고급 유형 시스템을 가지고 있기 때문에 Scala에서 잘 작동합니다. JS, Python 및 Ruby에서도 동적으로 유형이 지정되기 때문에 잘 작동합니다. 불행히도 이것은 정적으로 유형이 지정되고 고급 유형과 같은 고급 유형 기능이 없기 때문에 Swift에서는 작동하지 않습니다. 따라서 Swift의 경우 각 모나드에 대한 map 을 구현해야 합니다.

또한 Option 모나드는 이미 Swift 및 Scala와 같은 언어의 사실상 표준이므로 모나드 구현에 약간 다른 이름을 사용합니다.

이제 기본 Monad 클래스가 있으므로 Option 모나드 구현으로 이동하겠습니다. 이전에 언급했듯이 기본 아이디어는 Option 이 어떤 값을 보유하거나( Some 이라고 함) 값을 전혀 보유하지 않는( None ) 것입니다.

pure 메서드는 단순히 값을 Some 으로 승격하는 반면, flatMap 메서드는 Option 의 현재 값을 확인합니다. None 이면 None 을 반환하고, 기본 값이 있는 Some 이면 기본 값을 추출하고 f() 를 적용합니다. 그리고 결과를 반환합니다.

이 두 함수와 map 을 사용하는 것만으로는 null 포인터 예외가 발생하는 것이 불가능합니다. ( flatMap 메서드 구현에서 문제 잠재적으로 발생할 수 있지만 이는 한 번 확인하는 코드의 몇 줄에 불과합니다. 그 후에는 수천 곳에서 코드 전체에 걸쳐 Option 모나드 구현을 사용합니다. 널 포인터 예외를 전혀 두려워해야 합니다.)

어느 쪽 모나드

두 번째 모나드에 대해 알아보겠습니다. 둘 중 하나입니다. 이것은 기본적으로 Option 모나드와 동일하지만 SomeRight 라고 하고 NoneLeft 라고 합니다. 그러나 이번에는 Left 도 기본 값을 가질 수 있습니다.

예외를 던지는 것을 표현하는 것이 매우 편리하기 때문에 필요합니다. 예외가 발생하면 Either 의 값도 Left(Exception) 이 됩니다. 값이 Left 이면 flatMap 함수는 진행되지 않습니다. 이는 예외를 던지는 의미를 반복합니다. 예외가 발생하면 추가 실행을 중지합니다.

JavaScript - 모나드

 import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure

Python - 모나드 중 하나

 from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False

루비 - 모나드

 require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end

스위프트 - 모나드 중 하나

 import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }

스칼라 - 모나드

 package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }

또한 예외를 잡는 것은 쉽습니다. Left 에서 Right 으로 매핑하기만 하면 됩니다. (하지만 간결함을 위해 예제에서는 하지 않습니다.)

미래의 모나드

우리에게 필요한 마지막 모나드인 Future 모나드를 살펴보겠습니다. Future 모나드는 기본적으로 현재 사용 가능하거나 가까운 장래에 사용 가능하게 될 값에 대한 컨테이너입니다. 먼저 해결되는 값에 따라 달라지는 다음 코드를 실행하기 전에 Future 값이 해결될 때까지 대기하는 mapflatMap 을 사용하여 Future 체인을 만들 수 있습니다. 이것은 JS의 Promise 개념과 매우 유사합니다.

이제 우리의 설계 목표는 서로 다른 언어로 된 기존 비동기 API를 하나의 일관된 기반으로 연결하는 것입니다. 가장 쉬운 디자인 접근 방식은 $constructor$에서 콜백을 사용하는 것입니다.

콜백 디자인이 JavaScript 및 기타 언어에서 콜백 지옥 문제를 도입했지만 모나드를 사용하기 때문에 문제가 되지 않습니다. 사실, 콜백 지옥에 대한 JavaScript 솔루션의 기초인 Promise 객체는 모나드 자체입니다!

Future 모나드의 생성자는 어떻습니까? Is에는 다음과 같은 서명이 있습니다.

constructor :: ((Either err a -> void) -> void) -> Future (Either err a)

조각으로 나누어 봅시다. 먼저 다음을 정의하겠습니다.

type Callback = Either err a -> void

따라서 Callback 은 오류 또는 해결된 값을 인수로 취하고 아무 것도 반환하지 않는 함수입니다. 이제 서명은 다음과 같습니다.

constructor :: (Callback -> void) -> Future (Either err a)

따라서 비동기 계산이 오류 또는 일부 값으로 해결되자마자 아무 것도 반환하지 않고 콜백을 트리거하는 함수를 제공해야 합니다. 모든 언어에 대한 다리를 만들기에 충분히 쉬워 보입니다.

Future 모나드 자체의 디자인에 관해서는 내부 구조를 살펴보자. 핵심 아이디어는 Future 모나드가 해결된 경우 값을 보유하는 캐시 변수를 갖는 것입니다. 그렇지 않으면 아무 것도 보유하지 않습니다. 값이 해결되면 즉시 트리거되는 콜백으로 Future를 구독할 수 있습니다. 그렇지 않으면 콜백을 구독자 목록에 넣습니다.

Future가 해결되면 이 목록의 각 콜백은 개별 스레드에서 해결된 값으로 정확히 한 번 트리거됩니다(JS의 경우 이벤트 루프에서 실행될 다음 함수로). 동기화 프리미티브를 주의해서 사용하십시오. 그렇지 않으면 경쟁 조건이 발생할 수 있습니다.

기본 흐름은 다음과 같습니다. 생성자 인수로 제공된 비동기 계산을 시작하고 해당 콜백이 내부 콜백 메서드를 가리키도록 합니다. 그 동안 Future 모나드를 구독하고 콜백을 대기열에 넣을 수 있습니다. 계산이 완료되면 내부 콜백 메서드는 대기열의 모든 콜백을 호출합니다. Reactive Extensions(RxJS, RxSwift 등)에 익숙하다면 비동기 처리와 매우 유사한 접근 방식을 사용합니다.

Future 모나드의 공개 API는 이전 모나드와 마찬가지로 pure , map , flatMap 으로 구성됩니다. 또한 몇 가지 편리한 방법이 필요합니다.

  1. async 는 동기 차단 기능을 사용하여 별도의 스레드에서 실행합니다.
  2. traverse 는 값의 배열과 값을 Future 에 매핑하는 함수를 사용하고 확인된 값 배열의 Future 를 반환합니다.

그것이 어떻게 작동하는지 봅시다:

JavaScript—미래의 모나드

 import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )

파이썬—미래의 모나드

 from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()

루비—미래의 모나드

 require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end

스위프트 - 미래 모나드

 import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }

스칼라 - 미래 모나드

 package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }

이제 Future 의 공개 API가 스레드, 세마포어 또는 그와 같은 저수준 세부 정보를 포함하지 않는 방법에 주목하십시오. 기본적으로 콜백과 함께 무언가를 제공하기만 하면 됩니다. 그게 전부입니다!

모나드에서 프로그램 작성하기

자, 이제 모나드를 사용하여 실제 프로그램을 만들어 보겠습니다. URL 목록이 있는 파일이 있고 이러한 각 URL을 병렬로 가져오려고 한다고 가정해 보겠습니다. 그런 다음 간결함을 위해 응답을 각각 200바이트로 자르고 결과를 인쇄하려고 합니다.

우리는 기존 언어 API를 모나딕 인터페이스로 변환하는 것으로 시작합니다( readFilefetch 함수 참조). 이제 우리는 그것들을 구성하여 최종 결과를 하나의 체인으로 얻을 수 있습니다. 모든 피투성이의 세부 사항이 모나드에 포함되어 있으므로 체인 자체는 매우 안전합니다.

JavaScript—샘플 모나드 프로그램

 import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)

Python—Sample Monad Program

 import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()

Ruby—Sample Monad Program

 require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop

Swift—Sample Monad Program

 import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()

Scala—Sample Monad Program

 import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }

There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.

Overhead: Done. Benefits: Ongoing

For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! 엄청난!