본문 바로가기

BackEnd

타입으로 코드 문서화하기 With F# - 타입 기초

반응형

https://fsharpforfunandprofit.com/books/

 

Books | F# for fun and profit

This book starts with a discussion of Domain Driven Design, and then shows to how to model a design using types. The last part shows how to implement the design using functional programming with F# (composition of functions, “railway-oriented programming

fsharpforfunandprofit.com

함수형 프로그래밍 언어의 대수적 타입 시스템을 이용하여 도메인 모델의 개념을 코드에 명시적으로 나타낼 수 있습니다.

함수 이해하기

학교에서 함수는 입출력이 있는 블랙박스와 갖다 배운 적이 있을 겁니다.

터널과 트랙으로 생각해보면, 무언가 들어갔다가, 변환되어 나오는 트랙으로 생각 가능합니다.
우리는 모든 사과를 바나나로 바꾸는 함수를 생각해볼 수 있습니다.

타입 시그니처 with F#

(주 : 타입스크립트의 타입은 Type Alias일 뿐입니다. F#과 Haskell은 Data Type을 따로 제공합니다. 타입클래스로 해당 데이터 타입을 사용하기 위해선, 생성자를 따로 정의하거나 리터럴 타입을 사용해야 합니다!)

apple->banana는 타입(클래스) 간의 관계입니다. 즉 apple 클래스에 속하는 값을 banana클래스의 값으로 바꾸어 줍니다.

클래스는 종류, 값은 각각의 케이스(인스턴스) 입니다.

 

예를들어 int에 숫자를 더하는 x+1함수와, 숫자 두개의 합을 계산하는 함수가 있다 칩시다.

 

F#는 C# 혹은 haskell, scala와 유사합니다.

즉, 자동으로 커링이 지원되며, 타입 추론이 지원됩니다.

 

(연산자와 값을 이용해 명시적으로 데이터 타입을 드러내지 않아도 컴파일러가 자동으로 추론해줍니다.)

또한, F#의 모든 식은 표현식입니다. 즉 값은 반드시 평가됩니다.

let 키워드는 함수를 정의하는 데 사용됩니다. 매개변수는 괄호나 쉼표 없이 공백으로 구분됩니다.

C#이나 Java와 달리 return 키워드가 없습니다.

함수 정의의 마지막 표현식은 함수의 출력입니다.

​ 	​let​ add1 x = x + 1   ​// signature is: int -> int​
​ 	
​ 	​let​ add x y = x + y  ​// signature is: int -> int -> int
둘 이상의 행은 파이썬처럼 들여쓰기를 사용합니다.
보시다시피 왼쪽에 파라미터가 없으면 오른쪽은 평가된 값이 대입됩니다.
(우항을 항등함수에 apply 한 것으로 생각해도 됩니다.)
​ 	​// squarePlusOne : int -> int​
​ 	​let​ squarePlusOne x =
​ 	  ​let​ square = x * x
​ 	  square + 1

함수의 제네릭 타입

이전에 F#이 add 함수의 타입을 어떻게 int->int로 추론했을까요?

이는 + 함수가 덧셈에만 사용할 수 있기 때문입니다.

다양한 타입에서 사용할 수 있는 연산자는 어떨까요? (ex =)

 

=같은 연산자를 지원하기 위해선, haskell과 같은 순수함수 언어에선, 타입변수 a로 추론할 수 있는 타입이 Eq(==)라는 타입 클래스의 인스턴스 여야 합니다. 즉 eqality를 따질 수 있는 값이면 a의 위치에 파라미터로 들어올 수 있다는 것입니다.

자바스크립트로 따지면 number와 number, string과 string이며, 다른 케이스의 경우 할당할 수 없도록 합니다.

 

아래의 경우도 명시적으로 타입은 없지만, equality를 비교할 수 있는 타입이면 전부 사용할 수 있습니다.

즉 객체지향의 제네릭이 해당 클래스 내에서 사용되기 위한 타입 변수라면,

함수형 프로그래밍의 제네릭은, 해당 타입 클래스가 해당 연산을 지원하는지 판단하기 위해 사용됩니다.

​ 	​// areEqual : 'a -> 'a -> bool​
​ 	​let​ areEqual x y =
​ 	  (x = y)

F#에서 'a는 제네릭 타입을 나타냅니다. 즉, x와 y는 같은 타입이여야 하며, =가 ==의 역할을 하며, 리턴 타입은 bool인 것을 알 수 있습니다.

C#으로 보면 다음과 같습니다.

​ 	​static​ ​bool​ AreEqual<T>(T x, T y)
​ 	{
​ 	  ​return​ (x == y);
​ 	}

타입과 함수

사람들이 함수형 프로그래밍을 생각할때 타입의 중요성을 간과하지만, 함수형 프로그래밍의 가장 중요한 개념중 하나는 카테고리 이론입니다.

(lisp계열이나 js계열은 좀 덜중요한것 같긴 합니다만, ML계열에서는 아주 중요합니다.)

 

함수형 프로그래밍의 타입은 객체 지향 프로그래밍의 타입보다 훨씬 간단합니다.

함수의 입출력에 사용할수 있는 값의 집합에 지정된 이름(타입)일 뿐입니다.

타입은 함수의 입출력에 사용할수 있는 값의 집합에 지정된 이름입니다.

함수의 입출력에 사용할수 있는 값의 집합에 지정된 이름(타입)일 뿐입니다 !!!
예를 들어 -32768에서 +32767 사이의 숫자 집합을 가져와 레이블 int16을 지정할 수 있습니다.

시그니처 : int16 -> someOutputType

다음은 가능한 모든 문자열 세트로 구성된 출력을 가진 함수의 예입니다. 이를 string type이라고 합니다.

시그니처는 이런 식으로 적을 수 있습니다

​ someInputType -> ​string

타입은 primitive일 필요는 없습니다.

개념적 관점에서 타입의 사물은 실제 또는 가상의 모든 종류의 사물이 될 수 있습니다. 

그림은 "Fruit"과 함께 작동하는 함수를 보여줍니다. 이것이 실제 과일이든 가상의 표현이든 지금은 중요하지 않습니다.

마지막으로 함수도 사물(무언가)이므로 함수 집합을 유형으로 사용할 수도 있습니다. 아래 함수는 Fruit-to-Fruit 함수를 출력합니다.
someInputType -> (Fruit -> Fruit)

Jargon Alert: “Values” vs. “Objects” vs. “Variables”

함수형 프로그래밍 언어에서는 대부분의 것을 "값"이라고 합니다.
객체 지향 언어에서는 대부분의 무언가(사물)을 "객체"라고 합니다.
 
그렇다면 "값"와 "객체"의 차이점은 무엇입니까?
 
값은 입력 또는 출력으로 사용할 수 있는 타입의 구성원일 뿐입니다. 예를 들어 1은 int 타입의 값이고 "abc"는 string 타입의 값입니다.
함수도 값이 될 수 있습니다. let add1 x = x + 1과 같은 간단한 함수를 정의하면 add1은 int->int 타입의 (함수이자) 값입니다.
값은 변경할 수 없습니다("변수"라고 하지 않는 이유입니다 - 불변 -). 그리고 값에는 어떤 메서드도 연결되어 있지 않으며 단지 데이터일 뿐입니다.

 

객체는 데이터 구조 및 관련 동작(메서드)의 캡슐화입니다.
일반적으로 객체에는 상태가 있어야 하며(즉, 변경 가능해야 함) 내부 상태를 변경하는 모든 작업은 객체 자체에서 제공해야 합니다("점" 표기법 - dot notation-을 통해).
따라서 함수형 프로그래밍의 세계(객체가 존재하지 않는 곳)에서는 "변수" 또는 "객체"보다는 "값"이라는 용어를 사용해야 합니다. 

타입 합성하기

함수형 설계의 기본은 함수의 합성입니다.

레고블록처럼 작은 함수들을 조립하여 큰 함수를 만듭니다.

비슷하게 타입을 조립하여 새로운 타입을 만듭니다.

이전의 타입으로부터 새로운 타입을 만듭니다.

 

F#에서 새 타입은 다음 두 가지 방법으로 더 작은 타입에서 빌드됩니다.
  • 함께 _AND_함으로써
  • 함께 _OR_함으로써

"AND" Types

AND type

F#에서 해당 개념은 레코드로 표현됩니다. 

중괄호는 레코드 유형임을 나타내며 세 필드는 Apple, Banana 및 Cherries입니다.
 	​type​ FruitSalad = {
​ 	  Apple: AppleVariety
​ 	  Banana: BananaVariety
​ 	  Cherries: CherryVariety
​ 	}

“OR” Types

과일 스낵에는 사과나 바나나 또는 체리가 필요하다고 말할 수 있습니다.

이러한 종류의 "초이스-choice-" 유형은 모델링에 매우 유용합니다.
​ 	​type​ FruitSnack =
​ 	  | Apple ​of​ AppleVariety
​ 	  | Banana ​of​ BananaVariety
​ 	  | Cherries ​of​ CherryVariety
이와 같은 초이스 유형을 F#에서는 discriminated union이라 합니다. 다음과 같이 읽을 수 있습니다.
FruitSnack은 AppleVariety(Apple 태그) 또는 BananaVariety(Banana 태그) 또는 CherryVariety(Cherry 태그) 중 하나입니다.
수직 막대는 각 선택 항목을 구분하고 태그(예: Apple 및 Banana)가 필요한 이유는 두 개 이상의 선택 항목이 동일한 유형을 가질 수 있으므로 이를 구별하기 위해 태그가 필요한 경우가 있기 때문입니다.
(ex A와 B가 전부 String 생성자로 만들어질 수 있음.)
과일의 품종 자체는 OR 유형으로 정의되며 이 경우 다른 언어의 Enum과 유사하게 사용됩니다.
​ 	​type​ AppleVariety =
​ 	  | GoldenDelicious
​ 	  | GrannySmith
​ 	  | Fuji
​ 	
​ 	​type​ BananaVariety =
​ 	  | Cavendish
​ 	  | GrosMichel
​ 	  | Manzano
​ 	
​ 	​type​ CherryVariety =
​ 	  | Montmorency
​ 	  | Bing
이것은 다음과 같이 읽을 수 있습니다. AppleVariety는 GoldenDelicious 또는 GrannySmith 또는 Fuji 타입이다.

Product 타입과 Sum Type

AND를 사용하여 작성된 유형을 Product Type이라고 합니다. (레코드 타입)
OR을 사용하여 빌드된 형식을 sum type
또는 tagged union이라고 하며, F# 용어로 discriminated union이라고 합니다. 
DDD에서는 OR 타입을 초이스 타입이라고 하는게 좋은 것 같습니다

간단한 타입

다음과 같이 하나의 선택으로만 초이스 타입을 정의하는 경우가 많습니다.
​ 	​type​ ProductCode =
​ 	  | ProductCode ​of​ ​string
/// or
​type​ ProductCode = ProductCode ​of​ ​string​

단순히 Primitive Type을 Wrapping하는 타입입니다.

필드값을 통해 해당 값에 접근하는 js, java 개발자들에겐 익숙하지 않은 방법이지만,

함수형 프로그래밍에서는 패턴 매칭, 구조분해을 통해 값을 가져옵니다. 즉 객체지만 해당 필드명을 명시적으로 지정할 필요가 없습니다.

(ex : js로 컴파일되는 리스크립트는 해당 문법을 지원하는데, 내부적으로 index를 이용해 처리합니다.)

/// Constrained to be a decimal between 0.0 and 1000.00 
type Price = private Price of decimal

module Price =
    /// Return the value inside a Price 
    /// Price -> decimal
    let value (Price v) = v

우리는 도메인 모델링을 할 때 이러한 종류의 유형을 많이 보게 될 것입니다.

단일 케이스 union를 레코드 및 초이스 타입과 같은 복합 유형과 반대되는 "Simple types"으로 레이블을 지정합니다.

 

대수적 타입 시스템 (Algebraic Type Systems)

대수 유형 시스템은 단순히 모든 복합 타입이 더 작은 타입에서 AND 또는 OR로 구성되는 시스템입니다.
F#은 대부분의 함수형 언어와 마찬가지로(OO 언어와 달리) 대수 타입 시스템이 내장되어 있습니다.
AND와 OR을 사용하여 새로운 데이터 유형을 구축합니다.
우리는 도메인을 문서화하기 위해 동일한 종류의 AND 및 OR을 사용했습니다.
대수적 타입 시스템이이 실제로 도메인 모델링을 위한 훌륭한 도구라는 것을 곧 알게 될 것입니다.

F# 타입 좀 더 알아보기

F#에서 타입이 정의되는 방식과 값이 생성되는 방식은 매우 유사합니다.
 
주의 : Discriminated Union의 각 Case는 서브 클래스와 동일하지 않습니다.
UnitQuantity 및 KilogramQuantity는 타입이 아니며 OrderQuantity 유형의 고유한 케이스일 뿐입니다.
위의 예에서 두 값 모두 동일한 타입인 OrderQuantity를 갖습니다.
(ex Maybe a의 Nothing과 Something a은 타입이 아니라 케이스입니다.)
module WorkingWithTypes =

    //>WorkingRecord1
    // 예를 들어, 레코드 타입을 정의하려면 다음과 같이 중괄호를 사용한 다음 
    // 각 필드에 대해 name:type 정의를 사용합니다.
    type Person = {First:string; Last:string}
    //<

    //>WorkingRecord2

    // 이 타입의 값을 생성하려면 동일한 중괄호 및 =를 사용하여 다음과 같이 필드에 값을 할당합니다.
    let aPerson = {First="Alex"; Last="Adams"}
    //<

    //>WorkingRecord3
    // 패턴 매칭을 통해 값을 분해할 수 있습니다!
    // first와 last에 해당 값이 대입됩니다.
    let {First=first; Last=last} = aPerson 
    //<
    
    // >WorkingRecord3 와 같은 결과.
    let first = aPerson.First
    let last = aPerson.Last

    
    module AlternatePropertySyntax =
        //>WorkingRecord4
        let first = aPerson.First
        let last = aPerson.Last
        //<
        

    //>WorkingUnion1
    type OrderQuantity =
        | UnitQuantity of int
        | KilogramQuantity of decimal
    //<

    //>WorkingUnion2
    // 초이스 타입은 다음과 같이 매개변수로 전달된 관련 정보와 함께 케이스 레이블 중 하나를 생성자 함수로 사용하여 구성됩니다.
    let anOrderQtyInUnits = UnitQuantity 10
    let anOrderQtyInKg = KilogramQuantity 2.5M
    //<

	// 초이스 타입을 아래와 같이 패턴 매치로 분해하여 값을 사용할 수 있습니다.
    //>WorkingUnion3
    let printQuantity aOrderQty =
       match aOrderQty with
       | UnitQuantity uQty -> 
          printfn "%i units" uQty 
       | KilogramQuantity kgQty -> 
          printfn "%g kg" kgQty
    //<

    //>WorkingUnion4
    printQuantity anOrderQtyInUnits // "10 units"
    printQuantity anOrderQtyInKg    // "2.5 kg"
    //<
패턴 매칭은 해당 데이터 구조와 일치하는지를 판별하며, 일치하면 해당 패턴의 값을 사용할 수 있습니다.
입력이 UnitQuantity 케이스와 일치하면 uQty 값이 설정됩니다.

 

반응형