본문 바로가기

BackEnd

DDD 기능 구현을 위한 함수 이해하기 with F#

반응형

https://pragprog.com/titles/swdddf/domain-modeling-made-functional/

 

Domain Modeling Made Functional

Use domain-driven design to effectively model your business domain, and implement that model with F#.

pragprog.com

해당 시리즈는 Domain Modeling Made Functional 책의 번역 및 정리 내용입니다.

Functions, Functions, Everywhere

먼저 함수형 프로그래밍이 객체 지향 프로그래밍과 왜 다른지 살펴보겠습니다. 

  • 함수형 프로그래밍은 함수를 가장 중요한 것으로 다루며 프로그래밍하는 것입니다.
대부분의 모던 언어에서 함수는 일급 객체이지만 때때로 함수(또는 람다)를 사용한다고 해서 함수형 프로그래밍을 "하고 있다"는 의미는 아닙니다.
함수형 프로그래밍 패러다임의 핵심은 함수가 모든 곳에 사용된다는 것입니다.

 

예를 들어 더 작은 조각(컴포넌트)으로 구성된 큰 프로그램이 있다고 가정해 보겠습니다.
  • 객체 지향 접근 방식에서 이러한 조각은 클래스와 객체가 됩니다.
  • 함수형 접근 방식에서 이러한 부분은 함수가 됩니다.
"DRY" 원칙을 따르고 많은 컴포넌트 간에 코드를 재사용하고 싶다고 가정해 보겠습니다.
  • 객체 지향 접근 방식에서는 상속이나 Decorator 패턴과 같은 기술을 사용할 수 있습니다.
  • 함수형 프로그래밍은 재사용 가능한 모든 코드를 함수에 넣고 합성을 사용하여 함께 붙입니다.
따라서 함수형 프로그래밍은 스타일의 차이가 아니라 프로그래밍에 대한 완전히 다른 사고 방식이라는 것을 이해하는 것이 중요합니다.
FP가 처음이라면 초보자의 마음으로 FP 학습에 접근해야 합니다.
즉, "컬렉션을 어떻게 반복합니까?" 또는 "전략 패턴을 어떻게 구현합니까?"와 같은 다른 패러다임의 질문을 하는 것보다, 
좀 더 근본적인 문제("컬렉션의 각 요소에 대해 작업을 수행하려면 어떻게 해야 하나요?" 또는 "동작을 매개변수화하려면 어떻게 해야 하나요?")의 해결 방법을 묻는 것이 좋습니다
프로그래머로서 직면하는 문제는 동일하지만 함수형 프로그래밍에서 사용되는 솔루션은 객체 지향 프로그래밍에서 사용되는 솔루션과 매우 다릅니다.

Functions Are Things (함수는 값이다.)

함수형 프로그래밍 패러다임에서 함수는 그 자체로 값입니다.
그리고 함수가 값이면 다른 함수에 대한 입력으로 전달할 수 있습니다.
입력으로서의 함수

출력으로서의 함수

함수를 사물로 취급하면 가능성의 세계가 열립니다.
처음에는 이해하기 어렵지만 이 기본 원칙만 있어도 복잡한 시스템을 매우 빠르게 구축할 수 있다는 것을 이미 알 수 있습니다.

고차 함수 :

다른 함수를 입력 또는 출력하거나 함수를 매개변수로 취하는 함수를 고차 함수라고 하며 종종 HOF로 축약됩니다.

F#에서 함수를 값으로 취급하기

F#에서 "값로서의 함수"가 어떻게 작동하는지 살펴보겠습니다. 다음은 네 가지 함수 정의입니다.
​ 	​let​ plus3 x = x + 3            ​// plus3 : x:int -> int​
​ 	​let​ times2 x = x * 2           ​// times2 : x:int -> int​
​ 	​let​ square = (​fun​ x -> x * x)  ​// square : x:int -> int​
​ 	​let​ addThree = plus3           ​// addThree : (int -> int)
처음 두 정의는 이전에 본 것과 같습니다.
세 번째 정의에서 let 키워드는 람다 식(lambda expression)이라고도 하는 익명 함수에 이름(square)을 할당하는 데 사용됩니다.
네 번째 정의에서 let 키워드는 이전에 정의된 함수(plus3)에 이름(addThree)을 할당하는 데 사용됩니다.
이러한 각 함수는 int를 입력으로 받아 새로운 int를 출력하는 int -> int 함수입니다.
함수는 값이기에 리스트에도 넣을 수 있습니다.
​ 	​// listOfFunctions : (int -> int) list​
​ 	​let​ listOfFunctions =
​ 	  [addThree; times2; square]
F#에서 리스트 리터럴은 대괄호를 구분 기호로 사용하고 세미콜론(쉼표 아님!)을 요소 구분 기호로 사용합니다.
리스트를 반복하고 각 함수를 차례로 평가할 수 있습니다.
​ 	​for​ fn ​in​ listOfFunctions ​do​
​ 	  ​let​ result = fn 100   ​// call the function​
​ 	  printfn ​"If 100 is the input, the output is %i"​ result
​ 	
​ 	​// Result =>​
​ 	​// If 100 is the input, the output is 103​
​ 	​// If 100 is the input, the output is 200​
​ 	​// If 100 is the input, the output is 10000
let 키워드는 함수 정의에만 사용되는 것이 아니라 일반적으로 값에 이름을 할당하는 데 사용됩니다.
예를 들어 다음은 "hello" 문자열에 이름을 지정하는 데 사용됩니다.
​ 	​// myString : string​
​ 	​let​ myString = ​"hello"​
동일한 키워드(let)가 함수와 값을 모두 정의하는 데 사용된다는 사실은 우연이 아닙니다.
그 이유를 알아보기 위해 예를 살펴보겠습니다. 이 첫 번째 스니펫에서는 square라는 함수를 정의합니다.
​ 	​// square : x:int -> int​
​ 	​let​ square x = x * x
그리고 이 두 번째 스니펫에서는 이름 square를 익명 함수에 할당합니다. let은 여기에 간단한 값을 정의합니까 아니면 함수를 정의합니까?
​ 	​// square : x:int -> int​
​ 	​let​ square = (​fun​ x -> x * x)
정답은 둘 다 입니다! 함수는 값이며 이름을 지정할 수 있습니다.
따라서 제곱의 두 번째 정의는 본질적으로 첫 번째 정의와 동일하며 서로 바꿔서 사용할 수 있습니다.

입력으로서의 함수

"Function as things"는 입력과 출력에 사용할 수 있다는 것을 의미한다고 했습니다.
먼저 함수를 입력 매개변수로 사용하는 방법을 살펴보겠습니다.
여기 evalWith5ThenAdd2라는 함수가 있습니다. 이 함수는 fn 함수를 받아서 5로 호출한 다음 결과에 2를 더합니다.​
​ 	​let​ evalWith5ThenAdd2 fn =
​ 	  fn(5) + 2
​ 	
​ 	​// evalWith5ThenAdd2 : fn:(int -> int) -> int
맨 아래에 있는 형식 서명을 보면 컴파일러에서 fn이 (int -> int) 함수여야 한다고 추론했음을 알 수 있습니다.
이제 테스트해 보겠습니다. 먼저 (int -> int) 함수인 add1을 정의한 다음 전달합니다.
​ 	​let​ add1 x = x + 1     ​// an int -> int function​
​ 	evalWith5ThenAdd2 add1 ​// fn(5) + 2 becomes add1(5) + 2​
​ 	​//                     // so output is 8
결과는 예상대로 8입니다. 모든 (int -> int) 함수를 매개변수로 사용할 수 있습니다.
​ 	​let​ square x = x * x     ​// an int -> int function​
​ 	evalWith5ThenAdd2 square ​// fn(5) + 2 becomes square(5) + 2​
​ 	​//                       // so output is 27

출력으로서 함수

이제 출력으로서의 함수를 살펴보겠습니다.
함수를 반환하는 매우 중요한 이유 중 하나는 특정 매개변수를 함수에 "bake in(바인딩)"할 수 있다는 것입니다.
예를 들어 다음과 같이 정수를 추가하는 세 가지 다른 함수가 있다고 가정해 보겠습니다.
​ 	​let​ add1 x = x + 1
​ 	​let​ add2 x = x + 2
​ 	​let​ add3 x = x + 3​
분명히, 우리는 중복을 제거하고 싶습니다. 어떻게 할 수 있습니까? 대답은 "adder generator"를 만드는 것입니다.
추가할 숫자가 포함된 "추가" 함수를 반환하는 함수입니다. 코드는 다음과 같습니다.
​ 	​let​ adderGenerator numberToAdd =
​ 	  ​// return a lambda​
​ 	  ​fun​ x -> numberToAdd + x
​ 	
​ 	​// val adderGenerator :​
​ 	​//    int -> (int -> int)​
타입 시그니처 보면 int를 입력으로 사용하고 (int -> int) 함수를 출력으로 내보낸다는 것을 명확하게 보여줍니다.
다음과 같이 익명 함수 대신 명명된 함수를 반환하여 adderGenerator를 구현할 수도 있습니다.
​ 	​let​ adderGenerator numberToAdd =
​ 	  ​// define a nested inner function​
​ 	  ​let​ innerFn x =
​ 	    numberToAdd + x
​ 	
​ 	  ​// return the inner function​
​ 	  innerFn

 

두 구현은 사실상 동일합니다.
마지막으로 adderGenerator를 실제로 사용하는 방법은 다음과 같습니다.
​ 	​// test​
​ 	​let​ add1 = adderGenerator 1
​ 	add1 2     ​// result => 3​
​ 	
​ 	​let​ add100 = adderGenerator 100
​ 	add100 2   ​// result => 102

Currying

함수를 반환하는 이 트릭을 사용하면 모든 다중 매개변수 함수를 일련의 단일 매개변수 함수로 변환할 수 있습니다.
이 방법을 커링이라고 합니다. 예를 들어 다음과 같은 두 매개변수 함수 add는 다음과 같습니다.
​ 	​// int -> int -> int​
​ 	​let​ add x y = x + y
위에서 본 것처럼 새 함수를 반환하여 매개변수가 하나인 함수로 변환할 수 있습니다.
​ 	​// int -> (int -> int)​
​ 	​let​ adderGenerator x = ​fun​ y -> x + y
F#에서는 명시적으로 이 작업을 수행할 필요가 없습니다.
모든 함수는 커리 함수입니다.
즉, 시그니처가 'a -> 'b -> 'c인 모든 2개 매개변수 함수는 'a를 취하고 함수('b -> 'c)를 반환하는 1개 매개변수 함수로 해석될 수 있으며 더 많은 매개변수가 있는 함수의 경우에도 마찬가지입니다.
(즉 add x y 는 인자 2개가 전달되기 전까지 평가되지 않습니다. let middle = let add 4 (y->y+4), let result = middle 5 (9) )
 

Partial Application

모든 함수가 커리된 경우 다중 매개변수 함수를 사용하고 하나의 인수만 전달할 수 있으며
해당 매개변수가 베이크인된 새 함수를 다시 얻을 수 있지만 다른 모든 매개변수는 여전히 필요합니다.
예를 들어, 아래의 sayGreeting 함수에는 두 개의 매개변수가 있습니다.
​ 	​// sayGreeting: string -> string -> unit​
​ 	​let​ sayGreeting greeting name =
​ 	  printfn ​"%s %s"​ greeting name
하나의 매개변수만 전달하여 인사말이 포함된 몇 가지 새로운 함수를 생성할 수 있습니다.
​ 	​// sayHello: string -> unit​
​ 	​let​ sayHello = sayGreeting ​"Hello"​
​ 	
​ 	​// sayGoodbye: string -> unit​
​ 	​let​ sayGoodbye = sayGreeting ​"Goodbye"
이 함수에는 이제 이름이라는 매개변수가 하나 남아 있습니다. 이를 제공하면 최종 출력을 얻습니다.
​ 	sayHello ​"Alex"​
​ 	​// output: "Hello Alex"​
​ 	
​ 	sayGoodbye ​"Alex"​
​ 	​// output: "Goodbye Alex"
이러한 매개변수 "베이킹" 접근 방식을 Partial Application 이라고 하며 매우 중요한 함수형 패턴입니다.
예를 들어 종속성 주입을 수행하는 데 사용합니다.

Total Functions

수학 함수는 가능한 각 입력을 출력에 연결합니다.
함수형 프로그래밍에서 우리는 모든 입력이 상응하는 출력을 가지도록 같은 방식으로 함수를 설계하려고 합니다. 이러한 종류의 함수를 전체 함수라고 합니다.
귀찮게 왜그럴까요? 타입 서명에 문서화된 모든 이펙트를 사용하여 가능한 한 많은 것을 명시적으로 만들고 싶기 때문입니다.
정수 나눗셈을 사용하여 12를 입력으로 나눈 결과를 반환하는 다소 어리석은 함수인 twelveDividedBy로 개념을 설명하겠습니다.
의사 코드에서는 다음과 같이 사례 테이블을 사용하여 이를 구현할 수 있습니다.
​ 	​let​ twelveDividedBy n =
​ 	  ​match​ n ​with​
​ 	  | 6 -> 2
​ 	  | 5 -> 2
​ 	  | 4 -> 3
​ 	  | 3 -> 4
​ 	  | 2 -> 6
​ 	  | 1 -> 12
​ 	  | 0 -> ???
이제 입력이 0일 때 대답은 무엇이어야 합니까? 12를 0으로 나눈 값은 정의되지 않습니다.
가능한 모든 입력에 해당하는 출력이 있는지 신경 쓰지 않는다면 다음과 같이 0의 경우 예외를 던질 수 있습니다.
​ 	​let​ twelveDividedBy n =
​ 	  ​match​ n ​with​
​ 	  | 6 -> 2
​ 	  ...
​ 	  | 0 -> failwith ​"Can't divide by zero"
그러나 다음과 같이 정의된 함수의 서명을 살펴보겠습니다.

(예외는 타입 시그니처에서 숨어버립니다)

twelveDividedBy : int -> int
이 서명은 int를 전달하고 int를 다시 가져올 수 있다고 합니다.
거짓말입니다! 항상 int를 반환하는 것은 아닙니다.
때로는 예외가 발생합니다. 그러나 이는 형식 서명에 명시적으로 나타나지 않습니다.
 
타입 시그니처가 거짓말을 하지 않는다면 좋을 것입니다.
이 경우 함수에 대한 모든 입력은 예외 없이 유효한 출력을 갖게 됩니다(exn도 없으며 int->int 시그니처를 보장한다는 의미).

한 가지 방법은 입력을 제한하여 잘못된 값을 제거하는 것입니다.

 
이 예에서는 새로운 제약 타입 NonZeroInteger를 만들고 전달할 수 있습니다.
Zero는 입력 값 집합에 전혀 포함되지 않으므로 처리할 필요가 없습니다.
​ 	​type​ NonZeroInteger =
​ 	  ​// Defined to be constrained to non-zero ints.​
​ 	  ​// Add smart constructor, etc​
​ 	  ​private​ NonZeroInteger ​of​ ​int​
​ 	
​ 	​/// Uses restricted input​
​ 	​let​ twelveDividedBy (NonZeroInteger n) =
​ 	  ​match​ n ​with​
​ 	  | 6 -> 2
​ 	  ...
​ 	  ​// 0 can't be in the input​
​ 	  ​// so doesn't need to be handled
이 새 버전의 시그니처는 다음과 같습니다.
twelveDividedBy : NonZeroInteger -> int
이것은 이전 버전보다 훨씬 좋습니다.
문서를 읽거나 소스를 볼 필요 없이 입력에 대한 요구 사항이 무엇인지 즉시 확인할 수 있습니다.
이 함수는 거짓말을 하지 않습니다. 모든 것이 명시적입니다.

또 다른 기술은 아웃풋 타입을 확장하는 것입니다.

이 접근 방식에서는 0을 입력으로 받아도 괜찮지만 유효한 int와 정의되지 않은 값 사이에서 선택하도록 출력 타입을 확장합니다.
Option 타입을 사용하여 "something"과 "nothing" 사이의 선택을 나타냅니다. 구현은 다음과 같습니다.
​ 	​/// Uses extended output​
​ 	​let​ twelveDividedBy n =
​ 	  ​match​ n ​with​
​ 	  | 6 -> Some 2 ​// valid​
​ 	  | 5 -> Some 2 ​// valid​
​ 	  | 4 -> Some 3 ​// valid​
​ 	  ...
​ 	  | 0 -> None   ​// undefined
새 버전의 타입 서명은 다음과 같습니다.
twelveDividedBy : int -> int option

유효한 int를 입력하면 int를 줄 수도 있다는 것을 의미합니다.

 
이런 간단한 total 함수를 사용하면서도 이점을 볼 수 있습니다.
두 예제 모두 타입 시그니처는 가능한 모든 입력 및 출력이 무엇인지에 대해 명시적입니다.
오류 처리에 관한 장에서 우리는 가능한 모든 출력을 문서화하기 위한 타입 시그니처의 실제 사용을 좀 더 살펴봅니다.
 

Composition

다른 타입을 결합하여 새로운 타입을 생성하는 타입 합성에 대해 논의했습니다.
이제 첫 번째 출력을 두 번째 입력에 연결하여 함수를 결합하는 함수 합성에 대해 이야기해 보겠습니다.
 
예를 들어, 여기에 두 가지 함수가 있습니다.
첫 번째는 사과를 입력으로 받고 바나나를 출력합니다.
두 번째는 바나나를 입력으로 사용하고 체리를 출력합니다.
첫 번째의 출력은 두 번째의 입력과 동일한 타입이므로 다음과 같이 함께 합성할 수 있습니다.
(주 : 함수형 언어에서 타입은 클래스가 아니라 집합과 유사한 개념입니다. 값은 해당 집합에 포함되는 element입니다.)
바나나를 파라미터로 받아 바나나를 리턴, 바나나를 파라미터로 받아 체리를 리턴

함께 합성한 후에는 새로운 함수가 있습니다.

사과를 파라미터로 받아 체리를 리턴
이러한 합의 중요한 측면은 정보 은닉입니다. (Encapsulation, Information Hiding)
함수가 더 작은 함수로 구성되어 있는지, 그리고 더 작은 함수가 어떤 작업을 수행했는지 알 수 없습니다.
바나나는 어디로 갔나요?
이 새로운 함수의 사용자는 바나나가 있다는 사실조차 알지 못합니다.
우리는 최종적으로 합성한 함수의 사용자로부터 해당 정보를 성공적으로 숨겼습니다.
 

F#에서 함수의 합성

F#에서 함수 합성은 어떻게 동작합니까?

F#에서 첫 번째 함수의 출력 형식이 두 번째 함수의 입력 형식과 같기만 하면 두 함수를 함께 사용할 수 있습니다.

이것은 일반적으로 "piping"이라는 접근 방식을 사용하여 수행됩니다.

 

F#의 파이핑은 Unix의 파이핑과 매우 유사합니다.
값으로 시작하여 첫 번째 함수에 입력하고 첫 번째 함수의 출력을 가져와 다음 함수에 입력하는 식입니다.
시리즈의 마지막 함수의 출력은 전체 파이프라인의 출력입니다.
 
F#의 파이프 오퍼레이터는 |>입니다.
첫 번째 예에서 파이프를 사용하면 코드가 다음과 같이 보일 것입니다.
​ 	​let​ add1 x = x + 1     ​// an int -> int function​
​ 	​let​ square x = x * x   ​// an int -> int function​
​ 	
​ 	​let​ add1ThenSquare x =
​ 	  x |> add1 |> square
​ 	
​ 	​// test​
​ 	add1ThenSquare 5       ​// result is 36

add1ThenSquare에 대한 파라미터 x를 정의했습니다.

구현에서 해당 매개변수는 파이프라인을 통한 데이터 흐름을 시작하기 위해 첫 번째 함수(추가)에 제공됩니다.

일반 프로그래밍 언어에선 square(add1(x))가 됩니다.

다른 예가 있습니다. 첫 번째 함수는 int->bool 함수이고 두 번째 함수는 bool->string 함수이며 결합된 함수는 int->string입니다.

​ 	​let​ isEven x =
​ 	  (x % 2) = 0              ​// an int -> bool function​
​ 	
​ 	​let​ printBool x =
​ 	  sprintf ​"value is %b"​ x  ​// a bool -> string function​
​ 	
​ 	​let​ isEvenThenPrint x =
​ 	  x |> isEven |> printBool
​ 	
​ 	​// test​
​ 	isEvenThenPrint 2          ​// result is "value is true"

js의 const isEvenThenPrint = (x)=>(isEven(printBool(x));와 같습니다.

전체 애플리케이션을 함수로 빌드

함수 합성 법칙은 완전한 애플리케이션을 구축하는 데 사용할 수 있습니다.

예를 들어 애플리케이션의 저수준에서 기본 기능으로 시작합니다.

저수준 기능

다음 서비스 함수를 만들기 위해 저수준 오퍼레이션을 합성합니다.

저수준 함수의 합성 >> 서비스

그런 다음 이러한 서비스 함수를 사용 및 결합하여 전체 워크플로를 처리하는 함수를 만들 수 있습니다.

서비스 함수의 합성 >> 워크플로
복습 : 워크플로는 1인 혹은 팀이 수행할수 있는 업무 단위입니다.
마지막으로 이러한 워크플로를 병렬로 구성하고
그림과 같이 입력을 기반으로 호출할 특정 워크플로를 선택하는 컨트롤러/디스패처를 생성하여 애플리케이션을 구축할 수 있습니다.

 

그림과 같이 입력을 기반으로 호출할 특정 워크플로를 선택하는 컨트롤러/디스패처를 생성하여 애플리케이션을 구축할 수 있습니다.

파이프라인 합성 시 이러한 아이디어가 실제로 어떻게 작동하는지 살펴보겠습니다.
일련의 작은 함수들을 조합하여 주문 워크플로를 위한 파이프라인을 구현합니다.

함수 합성의 과제 (모나드)

하나의 출력이 다른 입력과 일치할 때 함수를 합성하는 것은 쉽습니다.
그러나 합성이 그렇게 쉽게 일치하지 않으면 어떻게 될까요?
 
일반적인 경우는 wapper type의 내부 값의 타입은 함수 합성이 가능하지만,
실제론 함수의 "모양"이 다른 경우입니다.

 

1번 예제의 첫번째 함수는 Option<int>를 출력하지만 두 번째 함수는 입력으로 primitive int가 필요합니다.

2번 함수의 첫 함수는 primitive int를 출력하지만 두 번째 함수는 입력으로 Option<int>이 필요합니다.

1번 예제의 첫번째 함수는 Option&lt;int&gt;를 출력하지만 두 번째 함수는 입력으로 primitive int가 필요합니다. 2번 함수의 첫 함수는 primitive int를 출력하지만 두 번째 함수는 입력으로 Option&lt;int&gt;이 필요합니다.

lists, the success/failure Result type, async 등 Wrapper type 에서 발생합니다.
컴포지션을 사용할 때의 많은 문제는 입력과 출력을 조정하여 함수를 함께 연결할 수 있도록 일치하도록 조정하는 것과 관련됩니다.
널리 사용되는 접근 방식은 양쪽을 동일한 타입, 즉 양쪽의 "최소 공배수"로 변환하는 것입니다.
 
예를 들어 출력이 int이고 입력이 Option<int>인 경우 둘 다 포함할 수 있는 "가장 작은" 타입(최소 공배수)은 Option입니다.
Some을 사용하여 functionA의 출력을 Option으로 변환하면 이제 조정된 값을 functionB의 입력으로 사용할 수 있으며 합성이 가능합니다.
최소 공배수 접근법

해당 예제 코드입니다.

​ 	​// a function that has an int as output​
​ 	​let​ add1 x = x + 1
​ 	
​ 	​// a function that has an Option<int> as input​
​ 	​let​ printOption x =
​ 	  ​match​ x ​with​
​ 	  | Some i -> printfn ​"The int is %i"​ i
​ 	  | None -> printfn ​"No value"

Some 생성자를 사용하여 add1의 출력을 Option으로 변환한 다음 printOption의 입력으로 파이프할 수 있습니다.
​ 	5 |> add1 |> Some |> printOption
자바스크립트 예시 : const printOption =  (x)=>printOption(Some(add1(x)));
타입 불일치 문제의 매우 간단한 예입니다. 주문 배치 워크플로를 모델링한 다음 합성하려고 할 때 이미 더 복잡한 예를 보았습니다. 앞으로 우리는 함수를 합성할 수 있도록 균일한 모양으로 함수를 얻는 데 상당한 시간을 할애할 것입니다.
 

추가로 읽어보기 : 책에서 다루지 않는 예제 코드

아래 페이지에서 직접 돌려보는것도 도움이 됩니다.

https://try.fsharp.org/

 

Try F#

 

try.fsharp.org

// 문법 리뷰
module Syntax = 
    //>SyntaxValues
    // single line comments use a double slash
    // 더블 슬래시는 주석

    // The "let" keyword defines an (immutable) value
    // let 키워드는 불변 변수 선언
    let myInt = 5
    let myFloat = 3.14
    let myString = "hello"   // note that no types needed
    //<

    // ======== Lists ============
    //>lists
    // 리스트는 ; 딜리미터
    let twoToFive = [2;3;4;5]        // Square brackets create a list with
                                     // semicolon delimiters.
    //let twoToFive = 2::3::4::[5]
    // ::는 하스켈의 cons 오퍼레이터. [1;2;3;4;5]는 해당 문법의 문법적 설탕에 불과함
    let oneToFive = 1 :: twoToFive   // :: creates list with new 1st element
    // The result is [1;2;3;4;5]
    // @는 concat Operator. 대부분 함수형 언어에서는 오퍼레이션이 infix와 prefix 전부 지원.
    let zeroToFive = [0;1] @ twoToFive   // @ concats two lists

    // IMPORTANT: commas are never used as delimiters, only semicolons!
    //<

    // ======== Functions ========
    //>SyntaxFunctions
    // The "let" keyword also defines a named function.
    // let을 통해 함수 선언 가능
    let square x = x * x          // Note that no parens are used.

    // In F# returns are implicit -- no "return" needed. A function always
    // returns the result of the last expression.
    
    // F#에는 return 키워드가 없음. 리턴이 암시적임. 마지막 표현식의 평가 결과를 리턴함.
    let add x y = x + y           // don't use add (x,y)! It means something
                                  // completely different.
    // 함수 호출 예제.
    square 3                      // Run the function. No parens!
    add 2 3                       // Run the function. No parens!

    // to define a multiline function, just use indents. No semicolons needed.
    // 멀티라인 표현식에도 ; 필요 없음
    let evens list =
       let isEven x = x%2 = 0     // Define "isEven" as an inner ("nested") function
       List.filter isEven list    // List.filter is a library function
                                  // with two parameters: a boolean function
                                  // and a list to work on

    evens oneToFive               // Now run the function
    //<

    //>SyntaxRecords
    // Record types have named fields. Semicolons are separators.
    // 레코드 문법. 타입 선언
    type Person = {First:string; Last:string}

    // To create a value of a record type, use similar syntax to the definition
    // and assign each field to a value
    // 레코드 문법. 값 생성
    let person1 = {First="john"; Last="Doe"}
    //<

    //>SyntaxUnions
    // Union types have choices. Vertical bars are separators.
    // 초이스 타입. 왼쪽이 타입 오른쪽은 Case 생성자.
    type Temp = 
        | DegreesC of float
        | DegreesF of int

    // To create a value of a union type, use the case tag as the constructor
    // 타입 Temp는 Case를 통해 Wrapper 타입을 생성함.
    // 내부적으로는 배열처럼 처리하거나, 리스트 객체처럼 처리함.
    // 사용자는 Wrapping, unWrapping만 하면 됨.
    let tempInC = DegreesC 37.1
    let tempInF = DegreesF 98
    //<

    //>SyntaxComplex
    // Types can be combined recursively in complex ways.
    // E.g. here is a union type that contains a list of the same type:
    type Employee = 
      | Worker of Person
      | Manager of Employee list
    let jdoe = {First="John";Last="Doe"}
    let worker = Worker jdoe
    //<

    // ========= Printing =========
    //>SyntaxPrinting
    // The printf/printfn functions are similar to the
    // Console.Write/WriteLine functions in C#.
    printfn "Printing an int %i, a float %f, a bool %b" 1 2.0 true
    printfn "A string %s, and something generic %A" "hello" [1;2;3;4]

    // all complex types have pretty printing built in (using %A)
    // 알아서 Wrapper type 안의 값을 까서 프린트해줌.
    // Person={First = john;Last = Doe},Temp=DegreesC 37.1,Employee=Worker {First = John; Last = Doe}
    printfn "Person=%A,\nTemp=%A,\nEmployee=%A" 
             person1 tempInC worker
    //<





    // ======== Pattern Matching ========
    // Match..with.. is a supercharged case/switch statement.
    let simplePatternMatch =
       let x = "a"
       match x with
        | "a" -> printfn "x is a"
        | "b" -> printfn "x is b"
        | _ -> printfn "x is something else"   // underscore matches anything

    // Some(..) and None are roughly analogous to Nullable wrappers
    let validValue = Some(99)
    let invalidValue = None

    // In this example, match..with matches the "Some" and the "None",
    // and also unpacks the value in the "Some" at the same time.
    let optionPatternMatch input =
       match input with
        | Some i -> printfn "input is an int=%d" i
        | None -> printfn "input is missing"

    optionPatternMatch validValue
    optionPatternMatch invalidValue
반응형