본문 바로가기

BackEnd

DDD 구현 : 모나드와 Async

반응형

Monads and More

 
모나드는 "모나딕" 함수를을 직렬로 연결할 수 있는 프로그래밍 패턴일 뿐입니다.
"모나딕" 함수는 무엇입니까? "normal" 값을 취하고 일종의 "enhanced" 값을 반환하는 함수입니다.
오류 처리 접근 방식에서 "enhanced" 값은 Result type에 래핑된 것이므로
모나딕 함수는 Result 생성 "switch" 함수의 종류입니다. (Ok, 혹은 serviceExceptionAdaptor)
(아래의 switchFn 위치에 들어가는 Result 생성자 + map 기능 결합 함수)
 

모나딕 함수 (return)

// 생성자 함수 예시
​/// "Adapter block" that converts exception-throwing services​
​ 	​/// into Result-returning services.​
​ 	​let​ serviceExceptionAdapter serviceInfo serviceFn x =
​ 	  ​try​
​ 	    ​// call the service and return success​
​ 	    Ok (serviceFn x)
​ 	  ​with​
​ 	  | :? TimeoutException ​as​ ex ->
​ 	    Error {Service=serviceInfo; Exception=ex}
​ 	  | :? AuthorizationException ​as​ ex ->
​ 	    Error {Service=serviceInfo; Exception=ex}

모나딕 함수 직렬 연결을 위한 Adaptor(bind) (하스켈의 >>= : monad m => ma  -> (a->mb) -> mb)

​ 	​let​ bind switchFn  =
​ 	  ​fun​ twoTrackInput ->
​ 	    ​match​ twoTrackInput ​with​
​ 	    | Ok success -> switchFn success
​ 	    | Error failure -> Error failure​​

예시 2 : DeadEnd

module DeadEnd = 

    //>DeadEnd1
    // string -> unit
    let logError msg =
        printfn "ERROR %s" msg
    //< 
    
    //>DeadEnd2 - bind 역할 : tap이라는 이름으로도 불린다.
    // ('a -> unit) -> 'a -> 'a
    let tee f x = 
        f x 
        x
    //< 

    //>DeadEnd3
    // ('a -> unit) -> (Result<'a,'error> -> Result<'a,'error>)
    let adaptDeadEnd f = 
        Result.mapError (tee f)
    //<
    
    //>DeadEnd4
    // val logErrorR:   x: Result<string,'a>   -> Result<string,'a>
    let logErrorR x = 
        (adaptDeadEnd logError) x
    //<
	// switch 함수 = Ok
    logErrorR (Ok "hello")
기술적으로 "모나드"는 단순히 세 가지 구성 요소가 있는 항목에 대한 용어입니다.
  • A data structure
    • 예 : Result Type
  • Some related functions
    • 모나드가 되려면 데이터 타입에 두 가지 관련 함수인 return 및 bind가 있어야 합니다.
      • return (pure라고도 함)은 일반적인 값을 모나딕 타입으로 변환하는 함수입니다. 우리가 사용하는 유형이 Result이므로 return 함수는 Ok 생성자일 뿐입니다.
      • bind(flatMap이라고도 함)은 모나딕 함수(이 경우 Result 생성 함수 Ok)를 함께 연결할 수 있도록 해주는 함수입니다. 이 장의 앞부분에서 Result에 대한 bind을 구현하는 방법을 보았습니다.
        • 양방향 판단이 필요할 때 bind를 사용
        • 한쪽 값만 가져다 쓰려면 mapError나 map 사용
    • 함수형 프로그래밍 관점에서 설명하자면, 해당 데이터 타입이 모나드의 인스턴스여야 하며, 이는 해당 타입에 대해 두 함수를 구현함을 의미합니다.
  • Some rules about how the functions must work
    • 이러한 함수가 어떻게 작동해야 하는지에 대한 규칙을 "모나드 법칙"이라고 합니다. 구현이 올바르고 이상한 일을 하지 않도록 하기 위한 상식적인 지침입니다.

 

모나드 법칙 (여기서 f는 모두 effective 함수)

of는 (pure-return)

flatMap (bind)

of: <A>(a: A) => HKT<M, A>
flatMap: <A, B>(f: (a: A) => HKT<M, B>) => ((ma: HKT<M, A>) => HKT<M, B>)
f: (a: A) => HKT<M, A>
Left identity 1B ∘ f' = f' flatMap(of) ∘ f = f
Right identity f' ∘ 1A = f' flatMap(f) ∘ of   = f
Associativity h' ∘ (g' ∘ f') = (h' ∘ g') ∘ f' flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f

 

Applicative로 패러랠 합성

applicatives라고 하는 관련 패턴에 대해서도 이야기해 보겠습니다.
applicatives는 모나드와 유사합니다.
그러나 모나딕 함수를 직렬로 연결하는 대신 applicatives를 사용하면 모나딕 값을 병렬로 결합할 수 있습니다.
예를 들어 유효성 검사를 수행해야 하는 경우 첫 번째 오류만 유지하는 것보다 모든 오류를 결합하기 위해 applicatives 접근 방식을 사용할 것입니다.
 

Async Effect 추가

원래 디자인에서는 오류 효과(Result)만 사용하지 않았습니다.
대부분의 파이프라인에서 Async 효과도 사용했습니다.
일반적으로 효과를 결합하는 것은 까다로울 수 있지만
이 두 효과가 함께 나타나는 경우가 많기 때문에 앞에서 정의한 AsyncResult 유형과 함께 사용할 asyncResult 계산 표현식을 정의합니다.
​ 	​let​ validateOrder : ValidateOrder =
​ 	  ​fun​ checkProductCodeExists checkAddressExists unvalidatedOrder ->
​ 	    asyncResult {
​ 	      ​let​! orderId =
​ 	        unvalidatedOrder.OrderId
​ 	        |> OrderId.create
​ 	        |> Result.mapError ValidationError
​ 	        |> AsyncResult.ofResult   ​// lift a Result to AsyncResult​
​ 	      ​let​! customerInfo =
​ 	        unvalidatedOrder.CustomerInfo
​ 	        |> toCustomerInfo
​ 	        |> AsyncResult.ofResult
​ 	      ​let​! checkedShippingAddress = ​// extract the checked address​
​ 	        unvalidatedOrder.ShippingAddress
​ 	        |> toCheckedAddress checkAddressExists
​ 	      ​let​! shippingAddress =        ​// process checked address​
​ 	        checkedShippingAddress
​ 	        |> toAddress
​ 	        |> AsyncResult.ofResult
​ 	      ​let​! billingAddress = ...
​ 	      ​let​! lines =
​ 	        unvalidatedOrder.Lines
​ 	        |> List.map (toValidatedOrderLine checkProductCodeExists)
​ 	        |> Result.sequence ​// convert list of Results to a single Result​
​ 	        |> AsyncResult.ofResult
​ 	      ​let​ validatedOrder : ValidatedOrder = {
​ 	        OrderId  = orderId
​ 	        CustomerInfo = customerInfo
​ 	        ShippingAddress = shippingAddress
​ 	        BillingAddress = billingAddress
​ 	        Lines = lines
​ 	      }
​ 	      ​return​ validatedOrder
​ 	    }​
 
 
Result를 asyncResult로 바꾸는 것 외에도 이제 모든 것이 AsyncResult인지 확인해야 합니다. 예를 들어 OrderId.create의 출력은Result일 뿐이므로 도우미 함수 AsyncResult.ofResult를 사용하여 AsyncResult로 "리프트"해야 합니다.
(Asnyc안에 Result값을 넣음)
 
 
또한 주소 유효성 검사를 두 부분으로 나누었습니다.
모든 효과를 다시 추가할 때 CheckAddressExists 함수가 AsyncResult를 반환하기 때문입니다.
​ 	​type​ CheckAddressExists =
​ 	  UnvalidatedAddress -> AsyncResult<CheckedAddress,AddressValidationError>​
워크플로에 맞지 않는 오류 타입이 있으므로 해당 결과를 처리하고 서비스별 오류(AddressValidationError)를 자체 ValidationError에 매핑하는 도우미 함수(toCheckedAddress)를 만들어 보겠습니다.
​ 	​/// Call the checkAddressExists and convert the error to a ValidationError​
​ 	​let​ toCheckedAddress (checkAddress:CheckAddressExists) address =
​ 	  address
​ 	  |> checkAddress
​ 	  |> AsyncResult.mapError (​fun​ addrError ->
​ 	    ​match​ addrError ​with​
​ 	    | AddressNotFound -> ValidationError ​"Address not found"​
​ 	    | InvalidFormat -> ValidationError ​"Address has bad format"​
​ 	    )
toCheckedAddress 함수의 출력은 여전히 ​​CheckedAddress를 감싼 AsyncResult를 반환하므로 let!을 사용하여
이를 checkedAddress 값으로 래핑 해제한 다음 일반적인 방식으로 유효성 검사 단계(toAddress)에 전달할 수 있습니다.
​ 	​let​ placeOrder : PlaceOrder =
​ 	  ​fun​ unvalidatedOrder ->
​ 	    asyncResult {
​ 	      ​let​! validatedOrder =
​ 	        validateOrder checkProductExists checkAddressExists unvalidatedOrder
​ 	        |> AsyncResult.mapError PlaceOrderError.Validation
​ 	      ​let​! pricedOrder =
​ 	        priceOrder getProductPrice validatedOrder
​ 	        |> AsyncResult.ofResult
​ 	        |> AsyncResult.mapError PlaceOrderError.Pricing
​ 	      ​let​ acknowledgmentOption = ...
​ 	      ​let​ events = ...
​ 	      ​return​ events
​ 	    }
그리고 나머지 파이프라인 코드는 같은 방식으로 asyncResult를 사용하여 변환할 수 있습니다.
리포지토리에서 전체 구현을 볼 수 있습니다.


지금까지 용어 정리

  • 오류 처리 컨텍스트에서 bind 함수는 Result 생성(Ok Error) 함수를 2트랙 함수로 변환합니다. Result 생성 함수를 "연속적으로" 연결하는 데 사용됩니다. bind 함수는 모나드의 핵심 구성 요소입니다.
  • 오류 처리 컨텍스트에서 map 함수는 1트랙 함수를 2트랙 함수로 변환합니다.
  • 합성에 대한 모나딕 방식은 bind를 사용하여 함수를 직렬로 결합하는 것을 말합니다.
  • 합성에 대한 applicative 접근은 결과를 병렬로 결합하는 것을 말합니다.



반응형