본문 바로가기

BackEnd

함수 파이프라인 합성하기 with F# 2. 작은 파이프라인 조합하기

반응형
이제 서브스텝의 구현을 하나의 파이프라인으로 합성하여 워크플로를 완료할 준비가 되었습니다.
​ 	​let​ placeOrder : PlaceOrderWorkflow =
​ 	  ​fun​ unvalidatedOrder ->
​ 	    unvalidatedOrder
​ 	    |> validateOrder
​ 	    |> priceOrder
​ 	    |> acknowledgeOrder
​ 	    |> createEvents
그러나 validateOrder에는 UnvalidatedOrder 외에 두 개의 추가 입력(의존성)이 있다는 문제가 있습니다.
현재로서는 PlaceOrder 워크플로의 입력을 validateOrder 함수에 연결하는 쉬운 방법이 없습니다.
입력과 출력이 일치하지 않기 때문입니다.
validateOrder에는 UnvalidatedOrder 외에 두 개의 추가 입력(의존성)이 있다는 문제가 있습니다.

priceOrder에는 두 개의 입력이 있으므로 validateOrder의 출력에 연결할 수 없습니다.

 

다양한 "모양"의 함수를 합성하는 것은 함수형 프로그래밍의 주요 과제 중 하나이며
이 문제를 해결하기 위해 많은 기술이 개발되었습니다.
대부분의 솔루션에는 두려운 "모나드"가 포함되므로 지금은 부분 적용을 사용하는
매우 간단한 접근 방식을 사용합니다.
3개의 매개변수 중 2개만 validateOrder(2개의 종속성)에 적용하여 입력이 하나만 있는 새 함수를 제공하는 것입니다.
 
3개의 매개변수 중 2개만 validateOrder(2개의 종속성)에 적용하여 입력이 하나만 있는 새 함수를 제공합니다.
​ 	​let​ validateOrderWithDependenciesBakedIn =
​ 	  validateOrder checkProductCodeExists checkAddressExists
​ 	
​ 	​// new function signature after partial application:​
​ 	​// UnvalidatedOrder -> ValidatedOrder​
이름이 이상하네요. 운 좋게도 F#에서는 새 함수에 로컬에서 동일한 이름(validateOrder)을 사용할 수 있습니다.
이를 "섀도잉"이라고 합니다.
​ 	​let​ validateOrder =
​ 	  validateOrder checkProductCodeExists checkAddressExists
우리는 같은 방식으로 priceOrder 및acknowledgeOrder에 대한 종속성을 베이크하여
단일 입력 매개변수를 사용하는 함수가 되도록 할 수 있습니다.

같은 방식으로 priceOrder 및acknowledgeOrder에 대한 종속성을 베이크하여 단일 입력 매개변수가 있는 함수가 되도록 합니다.

기본 워크플로 기능인 placeOrder는 이제 다음과 같이 보일 것입니다.
​ 	​let​ placeOrder : PlaceOrderWorkflow =
​ 	
​ 	  ​// set up local versions of the pipeline stages​
​ 	  ​// using partial application to bake in the dependencies​
​ 	  ​let​ validateOrder =
​ 	    validateOrder checkProductCodeExists checkAddressExists
​ 	  ​let​ priceOrder =
​ 	    priceOrder getProductPrice
​ 	  ​let​ acknowledgeOrder =
​ 	    acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment
​ 	
​ 	  ​// return the workflow function​
​ 	  ​fun​ unvalidatedOrder ->
​ 	
​ 	    ​// compose the pipeline from the new one-parameter functions​
​ 	    unvalidatedOrder
​ 	    |> validateOrder
​ 	    |> priceOrder
​ 	    |> acknowledgeOrder
​ 	    |> createEvents
이렇게 해도 함수가 맞지 않는 경우가 있습니다.
위 경우 acknowledgeOrder의 출력은 pricedOrder가 아닌 이벤트일 뿐이므로
createEvents의 입력과 일치하지 않습니다.
이를 위해 어댑터를 작성하거나
다음과 같이 각 단계의 출력이 값에 명시적으로 할당되는 보다 명령적인(절차적인) 코드 스타일로 간단히 전환할 수 있습니다.
​ 	​let​ placeOrder : PlaceOrderWorkflow =
​ 	  ​// return the workflow function​
​ 	  ​fun​ unvalidatedOrder ->
​ 	    ​let​ validatedOrder =
​ 	      unvalidatedOrder
​ 	      |> validateOrder checkProductCodeExists checkAddressExists
​ 	    ​let​ pricedOrder =
​ 	      validatedOrder
​ 	      |> priceOrder getProductPrice
​ 	    ​let​ acknowledgmentOption =
​ 	      pricedOrder
​ 	      |> acknowledgeOrder createAcknowledgmentLetter sendAcknowledgment
​ 	    ​let​ events =
​ 	      createEvents pricedOrder acknowledgmentOption
​ 	    events​
파이프라인만큼 우아하지는 않지만 여전히 이해하고 유지 관리하기 쉽습니다.

checkProductCodeExists 및 checkAddressExists 및 priceOrder 및 기타 종속성은
전역적으로 정의할 필요가 없으므로 이러한 종속성을 "주입"하는 방법을 살펴보겠습니다. 
 
 

의존성 주입

서비스를 의미하는, 함수를 파라미터로 받는 toValidProductCode와 같은 여러 하위 수준 도우미 함수가 있습니다.

이것들은 디자인 면에서 매우 저수준에 존재합니다.

따라서 최상위 수준에서 이를 필요로 하는 함수까지 종속성을 어떻게 전달할 수 있을까요?

객체 지향 프로그래밍을 하고 있다면 의존성 주입과 IoC 컨테이너를 사용할 것입니다.

하지만 우리는 종속성을 암시적으로 전달하고 싶지 않습니다.

 

우리는 항상 종속성을 명시적 매개변수로 전달하기를 원하므로 종속성이 분명합니다.

"Reader Monad" 및 "Free Monad"와 같이 함수형 프로그래밍에서 이러한 종류의 작업을 수행하는 기술이 많이 있지만

이 책은 입문서이므로 가장 간단한 접근 방식을 고수합니다.

모든 종속성을 최상위 함수로 전달하면 내부 함수로 전달되고, 다시 내부 함수로 전달되는 식입니다.

 

이전에 정의한 대로 도우미 함수를 구현했다고 가정해 보겠습니다.

​ 	​// low-level helper functions​
​ 	​let​ toAddress checkAddressExists unvalidatedAddress =
​ 	  ...
​ 	
​ 	​let​ toProductCode checkProductCodeExists productCode =
​ 	  ...
둘 다 종속성에 대한 명시적 매개변수가 있습니다.
OrderLine을 생성하기 위해, ProductCode를 생성해야 합니다.
즉, toValidatedOrderLine은 toProductCode를 사용해야 하며,
이는 toValidatedOrderLine에도 checkProductCodeExists 매개변수가 있어야 함을 의미합니다.
​ 	​// helper function​
​ 	​let​ toValidatedOrderLine checkProductExists unvalidatedOrderLine =
​ 	​//                       ^ needed for toProductCode, below​
​ 	
​ 	  ​// create the components of the line​
​ 	  ​let​ orderLineId = ...
​ 	  ​let​ productCode =
​ 	    unvalidatedOrderLine.ProductCode
​ 	    |> toProductCode checkProductExists ​//use service​
​ 	
​ 	  ...​

한 수준 위로 이동하면 validateOrder 함수는 toAddress와 toValidatedOrderLine을 모두 사용해야 하므로

두 서비스를 추가 매개변수로 전달해야 합니다.

​ 	​let​ validateOrder : ValidateOrder =
​ 	  ​fun​ checkProductExists ​// dependency for toValidatedOrderLine​
​ 	    checkAddressExists   ​// dependency for toAddress​
​ 	    unvalidatedOrder ->
​ 	
​ 	      ​// build the validated address using the dependency​
​ 	      ​let​ shippingAddress =
​ 	        unvalidatedOrder.ShippingAddress
​ 	        |> toAddress checkAddressExists
​ 	
​ 	      ...
​ 	
​ 	      ​// build the validated order lines using the dependency​
​ 	      ​let​ lines =
​ 	        unvalidatedOrder.Lines
​ 	        |> List.map (toValidatedOrderLine checkProductExists)
​ 	
​ 	      ...​
그리고 모든 서비스와 기타 종속성을 설정하는 최상위 함수에 도달할 때까지 계속됩니다.
객체 지향 디자인에서 이 최상위 함수는 일반적으로 composition root라고 하므로 여기에서 그 용어를 사용하겠습니다.
 
placeOrder 워크플로 함수가 컴포지션 루트인가요?
아니요, 서비스 설정에는 일반적으로 설정 파일 접근 등이 포함되기 때문입니다.
다음과 같이 placeOrder 워크플로 자체에 매개변수로 필요한 서비스가 제공되는 것이 좋습니다.
​ 	​let​ placeOrder
​ 	  checkProductExists               ​// dependency​
​ 	  checkAddressExists               ​// dependency​
​ 	  getProductPrice                  ​// dependency​
​ 	  createOrderAcknowledgmentLetter  ​// dependency​
​ 	  sendOrderAcknowledgment          ​// dependency​
​ 	  : PlaceOrderWorkflow =           ​// function definition​
​ 	
​ 	    ​fun​ unvalidatedOrder ->
​ 	      ...
 
모든 종속성에 가짜 객체를 사용 가능하기 때문에 전체 워크플로를 쉽게 테스트할 수 있다는 추가 이점이 있습니다.

실제로 컴포지션 루트 함수는 콘솔 앱의 경우 main 함수
웹 서비스와 같이 오래 실행되는 앱의 경우 OnStartup/Application_Start 핸들러와 같은 애플리케이션의 진입점에 최대한 가까이 있어야 합니다.
 
다음은 Suave 프레임워크를 사용하는 웹 서비스에 대한 컴포지션 루트의 예입니다.
먼저 서비스가 설정된 다음 워크플로에 모든 종속성이 전달되고
마지막으로 입력을 적절한 워크플로로 보내기 위해 라우팅이 설정됩니다.​
​ 	​let​ app : WebPart =
​ 	
​ 	  ​// set up the services used by the workflow​
​ 	  ​let​ checkProductExists = ...
​ 	  ​let​ checkAddressExists = ...
​ 	  ​let​ getProductPrice = ...
​ 	  ​let​ createOrderAcknowledgmentLetter = ...
​ 	  ​let​ sendOrderAcknowledgment = ...
​ 	  ​let​ toHttpResponse = ...
​ 	
​ 	  ​// set up the "placeOrder" workflow​
​ 	  ​// by partially applying the services to it​
​ 	  ​let​ placeOrder =
​ 	    placeOrder
​ 	      checkProductExists
​ 	      checkAddressExists
​ 	      getProductPrice
​ 	      createOrderAcknowledgmentLetter
​ 	      sendOrderAcknowledgment
​ 	
​ 	  ​// set up the other workflows​
​ 	  ​let​ changeOrder = ...
​ 	  ​let​ cancelOrder = ...
​ 	
​ 	  ​// set up the routing​
​ 	  choose
​ 	    [ POST >=> choose
​ 	      [ path ​"/placeOrder"​
​ 	          >=> deserializeOrder ​// convert JSON to UnvalidatedOrder​
​ 	          >=> placeOrder       ​// do the workflow​
​ 	          >=> postEvents       ​// post the events onto queues​
​ 	          >=> toHttpResponse   ​// return 200/400/etc based on the output​
​ 	        path ​"/changeOrder"​
​ 	          >=> ...
​ 	        path ​"/cancelOrder"​
​ 	          >=> ...
​ 	      ]
​ 	    ]


val ( >=> ):
   x: ('a -> 'b) ->
   y: ('b -> 'c)
   -> 'a -> 'c

경로가 /placeOrder이면 "주문하기" 프로세스를 시작하는 것을 볼 수 있습니다.
입력을 역직렬화한 다음
기본 placeOrder 파이프라인을 호출한 다음
이벤트를 게시한 다음
출력을 HTTP 응답으로 변환하여 클라이언트에 리턴합니다.
placeOrder 이외의 기능에 대해 논의할 공간이 없지만
직렬화 해제 기술은 직렬화 장에서 논의됩니다.

종속성이 너무 많은가요?

validateOrder에는 두 가지 종속성이 있습니다.
4개, 5개 또는 그 이상이 필요하면 어떻게 됩니까?
각 단계의 의존성에 또 다른 많은 종속성이 필요한 경우 폭발적으로 증가할 수 있습니다.
 
첫째, 당신의 기능이 너무 많은 일을 하고 있을 수 있습니다. 더 작은 조각으로 나눌 수 있습니까?
그것이 가능하지 않다면 종속성을 단일 레코드 구조로 그룹화하고 이를 하나의 매개변수로 전달할 수 있습니다.
 
일반적인 상황은 자식 함수에 대한 종속성이 차례로 특히 복잡해지는 경우입니다.
예를 들어, checkAddressExists 함수가 URI 엔드포인트와 자격 증명이 필요한 웹 서비스와 통신한다고 가정해 보겠습니다.​
 
​ 	​let​ checkAddressExists endPoint credentials =
​ 	  ...
이렇게 두 개의 추가 매개변수를 toAddress의 호출자에게도 전달해야 할까요?
​ 	​let​ toAddress checkAddressExists endPoint credentials unvalidatedAddress =
​ 	​//                           only ^ needed ^ for checkAddressExists​
​ 	
​ 	  ​// call the remote service​
​ 	  ​let​ checkedAddress = checkAddressExists endPoint credentials unvalidatedAddress
​ 	  ​//                     2 extra parameters ^ passed in ^​
​ 	  ...​
그러면 우리는 이러한 추가 매개변수를 toAddress의 호출자 체인을 따라 맨 위로 올려야 합니다.
 	​let​ validateOrder
​ 	  checkProductExists
​ 	  checkAddressExists
​ 	  endPoint    ​// only needed for checkAddressExists​
​ 	  credentials ​// only needed for checkAddressExists​
​ 	  unvalidatedOrder =
​ 	    ...
물론 이렇게 해서는 안됩니다.
이러한 중간 함수는 checkAddressExists 함수의 종속성에 대해 아무 것도 알 필요가 없습니다.
훨씬 더 나은 접근 방식은 최상위 함수 외부에서 저수준 함수(종속성)를 설정하여
모든 종속성이 이미 전달된 자식 함수를 전달하는 것입니다.
예를 들어 앱 초기화 단계에 URI와 자격 증명을 checkAddressExists 함수에 미리 전달하여 이후 하나의 매개변수 함수로 사용할 수 있습니다.
이 단순화된 함수는 이전과 마찬가지로 모든 곳으로 전달할 수 있습니다.
​ 	​let​ placeOrder : PlaceOrderWorkflow =
​ 	
​ 	  ​// initialize information (e.g from configuration)​
​ 	  ​let​ endPoint = ...
​ 	  ​let​ credentials = ...
​ 	
​ 	  ​// make a new version of checkAddressExists​
​ 	  ​// with the credentials baked in​
​ 	  ​let​ checkAddressExists = checkAddressExists endPoint credentials
​ 	  ​// etc​
​ 	
​ 	  ​// set up the steps in the workflow​
​ 	  ​let​ validateOrder =
​ 	    validateOrder checkProductCodeExists checkAddressExists
​ 	    ​//               the new checkAddressExists ^​
​ 	    ​//               is a one parameter function​
​ 	  ​// etc​
​ 	
​ 	  ​// return the workflow function​
​ 	  ​fun​ unvalidatedOrder ->
​ 	    ​// compose the pipeline from the steps​
​ 	    ...
"미리 빌드된" 도우미 함수를 전달하여 매개변수를 줄이는 이 접근 방식은 복잡성을 숨기는 데 도움이 되는 일반적인 기술입니다.
한 함수가 다른 함수로 전달될 때 "인터페이스"(함수 유형)는 가능한 한 최소화되어야 하며 모든 종속성(저수준)이 숨어 있어야 합니다.

의존성 테스트하기

종속성을 전달의 한 가지 장점은 특별한 모킹 라이브러리 없이도 가짜 종속성을 제공하기 쉽기 때문에 핵심 기능을 테스트하기가 매우 쉽다는 것입니다.

예를 들어 유효성 검사의 제품 코드 측면이 작동하는지 여부를 테스트하려고 한다고 가정해 보겠습니다.
하나의 테스트는 checkProductCodeExists가 성공하면 전체 유효성 검사가 성공하는지 확인해야 합니다.
그리고 또 다른 테스트는 checkProductCodeExists가 실패하면 전체 유효성 검사가 실패하는지 확인해야 합니다.
이제 이러한 테스트를 작성하는 방법을 살펴보겠습니다.
시작하기 전에 팁이 있습니다. F#을 사용하면 이중 백틱으로 묶인 경우 공백과 구두점이 있는 식별자를 만들 수 있습니다.
일반 코드에 대해 이렇게 하는 것은 좋은 생각이 아니지만
테스트 기능의 경우 테스트 출력을 훨씬 더 읽기 쉽게 만들기 때문에 허용됩니다.
다음은 Arrange/Act/Assert 테스트 모델을 사용하여 "성공" 사례에 대한 몇 가지 샘플 코드입니다.
​ 	​open​ NUnit.Framework
​ 	
​ 	[<Test>]
​ 	​let​ ​``​If product exists, validation succeeds​``​() =
​ 	  ​// arrange: set up stub versions of service dependencies​
​ 	  ​let​ checkAddressExists address =
​ 	    CheckedAddress address ​// succeed​
​ 	  ​let​ checkProductCodeExists productCode =
​ 	    true                   ​// succeed​
​ 	
​ 	  ​// arrange: set up input​
​ 	  ​let​ unvalidatedOrder = ...
​ 	
​ 	  ​// act: call validateOrder​
​ 	  ​let​ result = validateOrder checkProductCodeExists checkAddressExists ...
​ 	
​ 	  ​// assert: check that result is a ValidatedOrder, not an error​
​ 	  ...​

checkAddressExists 및 checkProductCodeExists 함수(서비스를 나타냄)의 스텁 버전이 작성하기 쉽고

테스트에서 바로 정의할 수 있음을 알 수 있습니다.

NUnit 프레임워크를 사용하고 있지만 FsUnit, Unquote, Expecto 또는 FsCheck와 같은 F# 친화적인 라이브러리 또는 더 나은 .NET 테스트 프레임워크 중 하나를 사용할 수 있습니다.
실패 사례에 대한 코드를 작성하려면 모든 제품 코드에 대해 실패하도록 checkProductCodeExists 함수를 변경하기만 하면 됩니다.
​ 	​let​ checkProductCodeExists productCode =
​ 	  false  ​// fail
​ 	[<Test>]
​ 	​let​ ​``​If product doesn't exist, validation fails​``​() =
​ 	  ​// arrange: set up stub versions of service dependencies​
​ 	  ​let​ checkAddressExists address = ...
​ 	  ​let​ checkProductCodeExists productCode =
​ 	    false ​// fail​
​ 	
​ 	  ​// arrange: set up input​
​ 	  ​let​ unvalidatedOrder = ...
​ 	
​ 	  ​// act: call validateOrder​
​ 	  ​let​ result = validateOrder checkProductCodeExists checkAddressExists ...
​ 	
​ 	  ​// assert: check that result is a failure​
​ 	  ...
물론 이 장에서는 서비스 실패가 예외를 throw하여 표시될 것이라고 말했는데, 이는 우리가 피하고 싶은 것입니다.
우리는 다음 장에서 이것을 고칠 것입니다.
이것은 작은 예이지만 이미 테스트에 함수형 프로그래밍 원칙을 사용하는 실질적인 이점을 볼 수 있습니다.
  • validateOrder 함수는 stateless합니다. 아무 것도 변경하지 않으며 동일한 입력으로 호출하면 동일한 출력을 얻습니다.  이것은 함수를 테스트하기 쉽게 만듭니다.
  • 모든 종속성은 명시적으로 전달되므로 작동 방식을 쉽게 이해할 수 있습니다.
  • 모든 사이드 이펙트는 함수 자체가 아니라 매개변수에 캡슐화됩니다. 함수를 테스트하기 쉽게 하고, 부작용이 무엇인지 제어하기 쉽게 만듭니다.

테스트는 큰 주제입니다. 다음은 조사할 가치가 있는 몇 가지 인기 있는 F# 친화적 테스트 도구입니다.

  • FsUnit은 NUnit 및 XUnit과 같은 표준 테스트 프레임워크를 F# 친화적인 구문으로 래핑합니다.
  • Unquote는 테스트 실패(말하자면 "스택 풀기")로 이어지는 모든 값들을 보여줍니다.
  • NUnit과 같은 "example-based" 테스트 접근 방식에만 익숙하다면 테스트에 대한 "property-based" 접근 방식을 반드시 살펴봐야 합니다. FsCheck는 F#의 기본 속성 기반 테스트 라이브러리입니다.
  • Expecto는 [<Test>]와 같은 특수 attribute를 요구하는 대신 표준 함수를 테스트 픽스처로 사용하는 경량 F# 테스트 프레임워크입니다.

조립된 파이프라인

우리는 이 장 전체에 걸쳐 흩어져 있는 코드를 보았습니다.

모든 것을 함께 모아서 전체 파이프라인이 어떻게 조립되는지 봅시다.

  • 특정 워크플로를 구현하는 모든 코드를 워크플로의 이름을 따서 명명된 동일한 모듈에 넣습니다 (예: PlaceOrderWorkflow.fs).
  • 파일 맨 위에 타입 정의를 넣습니다.
  • 그 후, 우리는 각 단계에 대한 구현을 넣습니다.
  • 맨 아래에서 주요 워크플로 함수로 단계를 조합합니다. 

종속성은 컴포지션 루트 함수에서 전달될 것입니다!

 module CompletePipeline = 

    open SimpleTypes
    open CommonTypes

    module API = ()

    //>CompletePipeline1 
    module PlaceOrderWorkflow =
    
        // make the shared simple types (such as 
        // String50 and ProductCode) available.
        open SimpleTypes    

        // make the public types exposed to the
        // callers available
        open API 

        // ==============================
        // Part 1: Design
        // ==============================

        // NOTE: the public parts of the workflow -- the API -- 
        // such as the `PlaceOrderWorkflow` function and its
        // input `UnvalidatedOrder`, are defined elsewhere.
        // The types below are private to the workflow implementation.

        // ----- Validate Order ----- 

        type CheckProductCodeExists = 
            ProductCode -> bool
        type CheckedAddress = 
            CheckedAddress of UnvalidatedAddress
        type CheckAddressExists = 
            UnvalidatedAddress -> CheckedAddress
        type ValidateOrder = 
            CheckProductCodeExists    // dependency
              -> CheckAddressExists   // dependency
              -> UnvalidatedOrder     // input
              -> ValidatedOrder       // output


        // ----- Price order ----- 

        type GetProductPrice = DotDotDot
        type PriceOrder = DotDotDot
        // etc
    //< 


        //>CompletePipeline2 
        // ==============================
        // Part 2: Implementation 
        // ==============================

        // ------------------------------
        // ValidateOrder implementation
        // ------------------------------

        let toCustomerInfo (unvalidatedCustomerInfo: UnvalidatedCustomerInfo) = 
            dotDotDot()

        let toAddress (checkAddressExists:CheckAddressExists) unvalidatedAddress =
            dotDotDot()

        let predicateToPassthru _ = dotDotDot()

        let toProductCode (checkProductCodeExists:CheckProductCodeExists) productCode = 
            dotDotDot()

        let toOrderQuantity productCode quantity = 
            dotDotDot()
                
        let toValidatedOrderLine checkProductExists (unvalidatedOrderLine:UnvalidatedOrderLine) = 
            dotDotDot()

        /// Implementation of ValidateOrder step
        let validateOrder : ValidateOrder = 
            fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
                let orderId = 
                    unvalidatedOrder.OrderId 
                    |> OrderId.create
                let customerInfo = dotDotDot()
                let shippingAddress = dotDotDot()
                let billingAddress = dotDotDot()
                let lines = 
                    unvalidatedOrder.Lines 
                    |> List.map (toValidatedOrderLine checkProductCodeExists) 
                let validatedOrder : ValidatedOrder = {
                    OrderId  = orderId 
                    CustomerInfo = customerInfo 
                    ShippingAddress = shippingAddress 
                    BillingAddress = billingAddress  
                    Lines = lines 
                }
                validatedOrder 
        //< 

        // mask previous implementation
        let validateOrder _ = dotDotDot()
        let priceOrder _ = dotDotDot()
        let acknowledgeOrder _ = dotDotDot()
        let createEvents _ = dotDotDot()

        type PlaceOrderWorkflow = DotDotDot -> unit

        //>CompletePipeline3
        // ------------------------------
        // The complete workflow
        // ------------------------------
        let placeOrder 
            checkProductExists              // dependency
            checkAddressExists              // dependency
            getProductPrice                 // dependency
            createOrderAcknowledgmentLetter // dependency
            sendOrderAcknowledgment         // dependency
            : PlaceOrderWorkflow =          // definition of function

            fun unvalidatedOrder -> 
                let validatedOrder = 
                    unvalidatedOrder 
                    |> validateOrder checkProductExists checkAddressExists 
                let pricedOrder = 
                    validatedOrder 
                    |> priceOrder getProductPrice 
                let acknowledgmentOption = 
                    pricedOrder 
                    |> acknowledgeOrder createOrderAcknowledgmentLetter sendOrderAcknowledgment 
                let events = 
                    createEvents pricedOrder acknowledgmentOption 
                events
        //<
반응형