https://itchallenger.tistory.com/428
이전 게시물에서 모나드를 설명했습니다.
type ValidateOrder =
// ignoring additional dependencies for now
UnvalidatedOrder // input
-> Result<ValidatedOrder, ValidationError> // outp
type PriceOrder =
ValidatedOrder // input
-> Result<PricedOrder, PricingError> // output
type AcknowledgeOrder =
PricedOrder // input
-> OrderAcknowledgmentSent option // output
type CreateEvents =
PricedOrder // input
-> OrderAcknowledgmentSent option // input (event from previous step)
-> PlaceOrderEvent list // output
type PlaceOrderError =
| Validation of ValidationError
| Pricing of PricingError
// Adapted to return a PlaceOrderError
let validateOrderAdapted input =
input
|> validateOrder // the original function
|> Result.mapError PlaceOrderError.Validation
// Adapted to return a PlaceOrderError
let priceOrderAdapted input =
input
|> priceOrder // the original function
|> Result.mapError PlaceOrderError.Pricing
(주 : mapError는 2track Result를 반환하므로 Ok면 그대로 Ok를 리턴합니다.)
(bind는 생성자 함수인 Switch 함수와 Result 값을 인자로 받습니다.)
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> validateOrderAdapted // adapted version
|> Result.bind priceOrderAdapted // adapted version
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> validateOrderAdapted
|> Result.bind priceOrderAdapted
|> Result.map acknowledgeOrder // use map to convert to two-track
|> Result.map createEvents // convert to two-track
UnvalidatedOrder -> Result<PlaceOrderEvent list,PlaceOrderError>
- 파이프라인의 각 함수는 오류를 생성할 수 있으며 생성할 수 있는 오류는 서명에 표시됩니다. 우리는 함수를 개별적으로 테스트할 수 있으며, 조립할 때 예기치 않은 동작이 발생하지 않을 것이라고 확신합니다.
- 함수는 여전히 함께 연결되어 있지만 이제 2트랙 모델을 사용합니다. 한 단계에서 오류가 발생하면 파이프라인의 나머지 함수를 건너뜁니다.
- 최상위 placeOrder의 전체 흐름은 여전히 깨끗합니다. 특별한 조건문이나 try/catch 블록은 없습니다.
type AcknowledgeOrder =
PricedOrder // input
-> OrderAcknowledgmentSent option // output
type CreateEvents =
PricedOrder // input
-> OrderAcknowledgmentSent option // input (event from previous step)
-> PlaceOrderEvent list // output
2트랙 모델에 다른 종류의 함수 적용하기
지금까지 파이프라인에서 두 가지 함수 "모양"을 보았습니다. 원 트랙 함수과 "스위치" 함수입니다.
우리는 다른 많은 종류의 함수로 작업해야 할 수도 있습니다.
이제 두 가지를 살펴보겠습니다.
- 예외를 발생시키는 함수 (이전처럼 도메인 오류처럼 예외를 안던지는 경우가 아닌 - 패닉 혹은 인프라 오류)
- 아무것도 반환하지 않는 "막다른 길(dead-end)" 함수
예외 처리
솔루션은 간단합니다. 그림과 같이 예외 발생 함수를 Result 반환 함수로 변환하는 또 다른 "어댑터 블록" 함수를 생성하면 됩니다.
예를 들어 원격 서비스의 시간 초과를 잡아서 RemoteServiceError로 바꾸고 싶다고 가정해 보겠습니다.
우리는 많은 서비스와 함께 일할 것이므로 먼저 ServiceInfo를 정의하여 오류를 일으킨 서비스를 추적합니다.
type ServiceInfo = {
Name : string
Endpoint: Uri
}
그런 다음 이를 기반으로 하는 오류 타입을 정의할 수 있습니다.
type RemoteServiceError = {
Service : ServiceInfo
Exception : System.Exception
}
(전에 다룬 도메인 오류는 Result가 validation 결과였지 예외를 던지는건 아니었음)
/// "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}
가능한 모든 예외를 포착하는 것이 아니라 도메인과 관련된 예외만 포착한다는 점에 유의하십시오.
서비스 함수에 두 개의 매개변수가 있는 경우 해당 경우를 지원하기 위해 다른 어댑터를 정의해야 합니다.
더 많아지면 그때도 마찬가지입니다.
let serviceExceptionAdapter2 serviceInfo serviceFn x y =
try
Ok (serviceFn x y)
with
| :? TimeoutException as ex -> ...
| :? AuthorizationException as ex -> ...
let serviceInfo = {
Name = "AddressCheckingService"
Endpoint = ...
}
// exception-throwing service
let checkAddressExists address =
...
// Result-returning service
let checkAddressExistsR address =
// adapt the service
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
// call the service
adaptedService address
새 함수가 Result를 반환하는 변종(variant)이라는 것을 명확히 하기 위해 이름을 checkAddressExistsR로 지정하고 끝에 R을 붙입니다.
(실제 코드에서는 원래 함수와 같은 이름을 지정하여 "shadowing"할 수 있습니다.)
다시 한 번 서명을 확인하여 원하는 것이 있는지 확인하겠습니다.
원래 함수는 항상 CheckedAddress를 반환했음을 나타냅니다.
checkAddressExists :
UnvalidatedAddress -> CheckedAddress
그러나 우리는 서명이 오해의 소지가 있다는 것을 알고 있습니다. 새로운 "adapted" 함수의 서명을 보면 훨씬 더 설명적이라는 것을 알 수 있습니다. 실패하고 오류를 반환할 수 있음을 나타냅니다.
checkAddressExistsR :
UnvalidatedAddress -> Result<CheckedAddress,RemoteServiceError>
오류 타입은 RemoteServiceError이므로 파이프라인에서 이 함수를 사용하려면 PlaceOrderError 타입에 원격 오류에 대한 사례를 추가해야 합니다.
type PlaceOrderError =
| Validation of ValidationError
| Pricing of PricingError
| RemoteService of RemoteServiceError // new!
이전과 마찬가지로 R 버전의 함수를 생성할 때 RemoteServiceError를 공유 PlaceOrderError로 변환해야 합니다.
let checkAddressExistsR address =
// adapt the service
let adaptedService =
serviceExceptionAdapter serviceInfo checkAddressExists
// call the service
address
|> adaptedService
|> Result.mapError RemoteService // lift to PlaceOrderError
Handling Dead-End Functions (막다른 길 함수 다루기)
// string -> unit
let logError msg =
printfn "ERROR %s" msg
// ('a -> unit) -> ('a -> 'a)
let tee f x =
f x
x
// ('a -> unit) -> (Result<'a,'error> -> Result<'a,'error>)
let adaptDeadEnd f =
Result.map (tee f)
module DeadEnd =
//>DeadEnd1
// string -> unit
let logError msg =
printfn "ERROR %s" msg
//<
//>DeadEnd2
// ('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
//<
logErrorR (Ok "hello")
(주 : 저자 제공 코드는 Result.map으로 되어있어서... 함수명은 logError인데 Error가 아니라 Ok를 로깅한다.
위 코드는 동작함으로 https://try.fsharp.org/ 에서 돌려봐도 된다.)
계산 표현식(Computation Expressions)으로 삶을 더 쉽게 만들기
(주 :해당 기능은 F#의 특수한 문법
모나딕한 값을 일반 값처럼 다룰 수 있게 해줌
js의 async 블럭이랑 비슷하게 실제로는 Wrapped된 값인데 일반 값처럼 쓸 수 있게 해줌
(Result랑 Promise랑 유사)
다른 타입들은 Do notation을 찾아봐야 할듯.)
지금까지 우리는 간단한 오류 처리 논리를 다루었습니다.
- bind - Result와 함께 이미 사용된 함수.
- return - 값의 생성자입니다. Result의 경우 Ok 생성자가 됩니다.
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> validateOrderAdapted
|> Result.bind priceOrderAdapted
|> Result.map acknowledgeOrder
|> Result.map createEvents
let placeOrder unvalidatedOrder =
result {
let! validatedOrder =
validateOrder unvalidatedOrder
|> Result.mapError PlaceOrderError.Validation
let! pricedOrder =
priceOrder validatedOrder
|> Result.mapError PlaceOrderError.Pricing
let acknowledgmentOption =
acknowledgeOrder pricedOrder
let events =
createEvents pricedOrder acknowledgmentOption
return events
}
- Result 계산 표현식은 result라는 단어로 시작하여 중괄호{}로 구분된 블록을 포함합니다.
- let! 키워드는 let처럼 보이지만 사실 내부 값을 얻기 위해 Result를 "unwraps"합니다. let! validatedOrder=...의 validatedOrder 는 priceOrder 함수에 직접 전달할 수 있는 일반 값(Result<'a> 아니고 'a)입니다.
- error type은 블록 전체에서 동일해야 하므로 Result.mapError를 사용하여 이전과 마찬가지로 error type을 일반 type으로 들어 올립니다. 오류는 결과 표현식에 명시적이지 않지만 해당 type은 여전히 일치해야 합니다.
- 내부적으로 Error 케이스를 없는것처럼 사용합니다.
- 블록의 마지막 줄은 블록의 실행 결과를 나타내는 return 키워드를 사용합니다.
acknowledgeOrder와 같이 bind가 필요하지 않은 다른 함수의 경우 일반 구문을 사용하면 됩니다.
type ResultBuilder() =
member this.Return(x) = Ok x
member this.Bind(x,f) = Result.bind f x
let result = ResultBuilder()
계산 표현식 합성하기
계산 표현식은 합성 가능합니다.
let validateOrder input = result {
let! validatedOrder = ... // 실제로는 Result 타입임
return validatedOrder
}
let priceOrder input = result {
let! pricedOrder = ... // 실제로는 Result 타입임
return pricedOrder
}
let placeOrder unvalidatedOrder = result {
let! validatedOrder = validateOrder unvalidatedOrder
let! pricedOrder = priceOrder validatedOrder
...
return ...
}
Validating an Order with Results
let validateOrder : ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
let orderId =
unvalidatedOrder.OrderId
|> OrderId.create
let customerInfo =
unvalidatedOrder.CustomerInfo
|> toCustomerInfo
let shippingAddress =
unvalidatedOrder.ShippingAddress
|> toAddress checkAddressExists
let billingAddress = ...
let lines = ...
let validatedOrder : ValidatedOrder = {
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
Lines = lines
}
validatedOrder
let validateOrder : ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
result {
let! orderId =
unvalidatedOrder.OrderId
|> OrderId.create
|> Result.mapError ValidationError
let! customerInfo =
unvalidatedOrder.CustomerInfo
|> toCustomerInfo
let! shippingAddress = ...
let! billingAddress = ...
let! lines = ...
let validatedOrder : ValidatedOrder = {
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
Lines = lines
}
return validatedOrder
}
이전과 마찬가지로 Result.mapError를 사용하여 모든 오류 타입이 일치하는지 확인해야 합니다.
OrderId.create는 오류의 경우 문자열을 반환하므로 mapError를 사용하여 이를 ValidationError로 들어 올려야 합니다.
다른 헬퍼 함수는 단순 타입을 처리할 때 동일한 작업을 수행해야 합니다.
toCustomerInfo 및 toAddress 함수의 출력이 이미 ValidationError라고 가정하므로 Result.mapError를 사용하지 않습니다.
(즉 헬퍼 함수들은 Result를 반환해야함.
Working with Lists of Results
let validateOrder unvalidatedOrder =
...
// convert each line into an OrderLine domain type
let lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)
// create and return a ValidatedOrder
let validatedOrder : ValidatedOrder = {
...
Lines = lines
// etc
}
validatedOrder
let validateOrder unvalidatedOrder =
...
let lines = // lines is a "list of Result"
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)
let validatedOrder : ValidatedOrder = {
...
Lines = lines // compiler error
// ^ expecting a "Result of list" here
}
...
- 두 매개변수가 모두 Ok이면 내용을 추가하고 결과 목록을 다시 Result로 래핑합니다.
- 매개변수 중 하나가 오류이면 오류를 반환합니다.
/// Prepend a Result<item> to a Result<list>
let prepend firstR restR =
match firstR, restR with
| Ok first, Ok rest -> Ok (first::rest)
| Error err1, Ok _ -> Error err1
| Ok _, Error err2 -> Error err2
| Error err1, Error _ -> Error err1
마지막 항목부터 시작하여 목록을 반복하고(foldBack 사용)
각 Result 요소를 우리가 작성한 리스트 앞에 추가하여 Result<'a> 리스트에서 Result<'a list>를 만들 수 있습니다.
우리는 이 함수를 sequence라 명칭하고 Result 모듈에 또 다른 유용한 함수로 추가할 것입니다. 구현은 다음과 같습니다.
let sequence aListOfResults =
let initialValue = Ok [] // empty list inside Result
// loop through the list in reverse order,
// prepending each element to the initial value
List.foldBack prepend aListOfResults initialValue
type IntOrError = Result<int,string>
let listOfSuccesses : IntOrError list = [Ok 1; Ok 2]
let successResult =
Result.sequence listOfSuccesses // Ok [1; 2]
let listOfErrors : IntOrError list = [ Error "bad"; Error "terrible" ]
let errorResult =
Result.sequence listOfErrors // Error "bad"
실패 예에서는 첫 번째 오류만 반환됩니다. 그러나 많은 경우 특히 유효성 검사를 수행할 때 모든 오류를 유지하려고 합니다.
이를 위한 함수형 프로그래밍 기법을 applicative 라고 합니다.
다음 섹션에서 간략하게 언급하겠지만 자세한 구현에 대해서는 다루지 않을 것입니다.
module ValidateOrderLines =
open Lists
type CheckProductCodeExists = DotDotDot
type CheckAddressExists = DotDotDot
type UnvalidatedOrder = {
OrderId : DotDotDot
CustomerInfo : DotDotDot
ShippingAddress : DotDotDot
BillingAddress : DotDotDot
Lines : DotDotDot list
}
type ValidatedOrder = UnvalidatedOrder
module OrderId =
let create _ = dotDotDot()
let toCustomerInfo _ = dotDotDot()
let toAddress _ = dotDotDot()
type ValidationError = ValidationError of string
type ValidateOrder =
CheckProductCodeExists // dependency
-> CheckAddressExists // dependency
-> UnvalidatedOrder // input
-> Result<ValidatedOrder,ValidationError> // output
let checkProductCodeExists _ = dotDotDot()
let toValidatedOrderLine _ _ = dotDotDot()
//>ValidateOrder_OrderLines
let validateOrder : ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
result {
let! orderId = dotDotDot()
let! customerInfo = dotDotDot()
let! shippingAddress = dotDotDot()
let! billingAddress = dotDotDot()
let! lines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)
|> Result.sequence // convert list of Results to a single Result
let validatedOrder : ValidatedOrder = {
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
Lines = lines
}
return validatedOrder
}
//<
그러나 기본 파이프라인에서는 오류 사례가 PlaceOrderError여야 합니다.
이제 placeOrder 함수에서 Result<ValidatedOrder,ValidationError> 타입을
module ValidateOrderLines2 =
open ValidateOrderLines
type PlaceOrderEvent = DotDotDot
type PlaceOrderError =
| Validation of ValidationError
| Pricing of DotDotDot
type PlaceOrder =
UnvalidatedOrder -> Result<PlaceOrderEvent list,PlaceOrderError>
let checkProductExists _ = dotDotDot()
let checkAddressExists _ = dotDotDot()
let getProductPrice _ = dotDotDot()
let validateOrder _ _ _ = dotDotDot()
let priceOrder _ _ = dotDotDot()
//>placeOrder
let placeOrder : PlaceOrder = // definition of function
fun unvalidatedOrder ->
result {
let! validatedOrder =
validateOrder checkProductExists checkAddressExists unvalidatedOrder
|> Result.mapError PlaceOrderError.Validation
let! pricedOrder =
priceOrder getProductPrice validatedOrder
|> Result.mapError PlaceOrderError.Pricing
let acknowledgmentOption = dotDotDot()
let events = dotDotDot()
return events
}
//<
'BackEnd' 카테고리의 다른 글
Serialization (0) | 2022.03.21 |
---|---|
DDD 구현 : 모나드와 Async (0) | 2022.03.21 |
DDD 구현 : 모나드로 Result 생성 함수 연결 (0) | 2022.03.20 |
DDD 구현 : 오류 모델링 (0) | 2022.03.20 |
함수 파이프라인 합성하기 with F# 2. 작은 파이프라인 조합하기 (0) | 2022.03.20 |