관리 메뉴

HAMA 블로그

Haskell 에서 Sum 타입이란? [번역] 본문

Haskell

Haskell 에서 Sum 타입이란? [번역]

[하마] 이승현 (wowlsh93@gmail.com) 2018. 3. 26. 10:35

다음 글은 Why Sum Types Matter in Haskell  글을 번역한 글임을 밝힙니다.

Why Sum Types Matter in Haskell

이 글에서는 Haskell의 Algebraic Data Types 자세히 살펴볼 것 입니다. Algebraic Data Types를 이해하는 열쇠는 기존 타입을 결합하여 새로운 타입을 생성하는 방법을 이해하는 것입니다. 고맙게도 두 가지 방법 밖에 없는데요. 다른 타입을 'and'과 결합하여 만든 타입을 Product types라고합니다. 'or'을 사용하여 결합 된 타입을 Sum types이라고합니다. 대부분의 프로그래밍 언어는 Product 타입을 사용합니다. Sum types은 개념적으로 단순하지만 모델링 데이터의 경우 매우 강력한 도구임을 입증합니다. 이 글의 끝 부분에서는 Sum types을 사용하여 복잡한 데이터의 디자인을 극적으로 단순화하는 방법을 살펴 보겠습니다.

Product Types — Combing types with ‘and’

Product types은 두 개 이상의 기존 타입을 'and'로 결합하여 생성됩니다. 몇 가지 일반적인 예는 다음과 같습니다.

  • 분수는 분자 (정수)와 분모 (또 다른 정수)로 정의 할 수 있습니다.
  • 거리 주소는 숫자 (Int) 및 거리 이름 (String) 일 수 있습니다.
  • 우편 주소는 거리 주소 및 도시 (문자열) 및 상태 (문자열) 및 우편 번호 (Int) 일 수 있습니다.

"Product type"이라는 이름은 좀 복잡하게 들릴 수 있지만 타입을 정의하는 가장 일반적인 방법입니다. 거의 모든 프로그래밍 언어가 Product type을 지원합니다. 가장 간단한 예제는 C의 구조체입니다. 다음은 author_name에 다른 구조체를 사용 하는 책의 C 구조체 예제입니다 .

struct author_name {
char *first_name;
char *last_name;
};
struct book {
author_name author;
char *isbn;
char *title;
int year_published;
double price;
};

이 예제에서는 author_name 타입이 두 개의 String을 결합하여 만들어 짐을 알 수 있습니다. 그 다음에 있는 book 타입은AUTHOR_NAME , 두 개의 문자열, int 형과 double 형을 결합합니다. author_name 과 book 은 모두 'and'로 다른 유형을 결합하여 만듭니다. C의 구조체는 클래스 및 JSON을 비롯한 거의 모든 언어에서 유사한 타입들의 조상이죠. Haskell에서 우리의 book 예제는 다음과 같이 보일 것입니다.

data AuthorName = AuthorName String String
data Book = Author String String Int

Record Syntax를 사용하여 C 구조체를 더 연상시키는 Book 버전을 작성하겠습니다.

data Book = Book 
{author :: AuthorName
,isbn :: String
,title :: String
,year :: Int
,price :: Double }

Book 과 AuthorName 은 모두 product types의 예이며 거의 모든 최신 프로그래밍 언어에서 analog를 사용합니다. 매혹적인 것은 대부분의 프로그래밍 언어에서 타입을 'and'로 결합하는 것이 종종 새로운 타입을 만드는 유일한 방법이라는 것입니다.

The curse of product types: Hierarchical design.

"and"를 사용하여 기존 타입을 결합하여 새로운 타입을 만드는 것은 소프트웨어 설계의 흥미로운 모델로 연결됩니다. 아이디어를 추가하여 확장 할 수 있다는 제한 때문에 우리는 상상할 수 있는 타입의 가장 추상적인 표현부터 시작하여 상향식 디자인으로 제한됩니다. 이것은 클래스 계층의 관점에서 소프트웨어를 설계하기위한 기초입니다.

예를 들어 Java를 작성 중이며 Book Store에 대한 데이터 모델링을 시작한다고 가정합니다. 의 Book 예제로 시작 합니다 ( Author 클래스가 이미 있다고 가정 )

public class Book {
Author author;
String isbn;
String title;
int yearPublished;
double price;
}

이것은 우리가 서점에서 레코드판을 판매하기를 원한다는 것을 알기 전까지 훌륭합니다. VinylRecord 의 기본 구현은 다음과 같습니다.

public class VinylRecord {
String artist;
String title;
int yearPublished;
double price;
}

VinylRecord은 book과 비슷하지만 몇가지 문제를 야기 시킵니다처음에는 Author 타입을 다시 사용할 수 없습니다 그 이유는 일부 아티스트는 이름이 없기 때문입니다. 우리는 "Elliott Smith" 의 저자 타입을 사용할 수 있지만 "The Smiths" 는 사용할 수 없습니다. 전통적인 계층적 디자인에서는 제작자 와 아티스트의 불일치 에 대해 이 문제에 대한 좋은 해답이 없습니다 또 다른 문제는 VinylRecords 에 ISBN 번호가 없다는 것입니다.

디자인 관점에서 우리는 검색 가능한 인벤토리를 지원할 VinylRecords 및 Books 를 나타내는 단일 타입을 원합니다 "and" 만을 사용하여 타입을 작성해야 하는 필요성 때문에 우리는 레코드와 서적이 공통으로 가지고있는 모든 것을 설명하는 추상화를 개발해야합니다. 그런 다음 개별 클래스 의 차이점 만 구현합니다 이것이 상속 의 기본 아이디어이죠. 다음으로 VinylRecord 와 Book 의 수퍼클래스 인 StoreItem 클래스를 만듭니다 . 다음은 리팩터링된 Java 클래스입니다.

public class StoreItem {
String title;
int yearPublished;
double price;
}
public class Book extends StoreItem{
Author author;
String isbn;
}
public class VinylRecord extends StoreItem{
String artist;
}

이 솔루션은 작동은 합니다. StoreItems 로 작업 할 코드를 생성 한 다음 조건문을 사용하여 Book 및 VinylRecord 를 처리 할 수 ​​있습니다 .

이제 판매 할 수 있는 다양한 종류의 장난감 인형을 주문 했다고 가정합니다. 다음은 기본적인 CollectibleToy 클래스입니다.

public class CollectibleToy {
String name;
String description;
double price;
}

모든 것을 작동 시키려면 모든 코드를 다시 리팩토링해야합니다! 이제 StoreItem 은 모든 항목이 공통으로 공유하는 유일한 값이기 때문에 가격 속성 만 공통적으로 가질 수 있습니다 즉, VinylRecords와 Books 사이의 일반적인 속성은 해당 클래스로 돌아 가야합니다. 또는 StoreItem 에서 상속받은 새로운 클래스를 만들 수 있겠지요. 즉 VinylRecord 및 Book 의 수퍼 클래스말 입니다 근데 CollectibleToy 의 name 속성은 title 과 무엇이 다른가요? 어쩌면 우리는 인터페이스를 만들어야 할 것입니다. 대신 우리의 모든 항목! 핵심은 비교적 단순한 경우임에도 엄격하게 제품 타입을 설계하는 것이 꽤나 복잡해질 수 있다는 것입니다.

이론적으로 클래스 계층 구조를 생성하는 것은 우아하고 세계의 모든 것이 어떻게 상호 연관되어 있는지에 대한 추상을 포착합니다. 실제로는 사소한 클래스 계층 구조를 만드는 일은 디자인 문제로 가득합니다. 이러한 모든 도전의 근원은 대부분의 언어에서 타입을 결합하는 유일한 방법은 "and" 이라는것 입니다. 이로 인해 우리는 극단적인 추상화에서 시작하여 아래로 이동해야합니다. 불행히도, 실생활은 우리가 원하는 것보다 더 복잡한 이상한 요소들로 가득차 있습니다

(역주: 누구나 저런 경우를 만나왔을 것이고 고민 또한 했을 거 같습니다. 제 경우 자바나 파이썬 할 때 저런 지경이면 먼가 깨름칙 한 기분을 앉고서는 그냥 다 따로 만듭니다. 특히 요즘은 마이크로서비스로 작은 사이즈의 서버에서 돌아가는 프로그램을 짜 왔기 때문인지 별 고민없이 따로 만들었던거 같습니다.  계층적 설계는 많은 경우에 오버스펙이 될 여지가 크기 때문이죠.) 

Sum types: Combining Types with ‘or’

Sum 타입 은 두 가지 타입을 결합한다는 점에서 놀랍도록 강력한 도구입니다. 타입을 "or" ( 둘 중 하나 !!!) 로써 결합하는 예는 다음과 같습니다. 

  • 주사위는 6면 다이 또는 20면 다이입니다.
  • 논문은 사람 (String) 또는 사람 그룹 ([String])으로 authored 됩니다.
  • 목록은 빈 목록 ([])이거나 다른 목록 (a : [a])에 추가 된 항목입니다.

가장 간단한 Sum 타입은 Bool입니다 .

data Bool = False | True

BOOL 의 인스턴스는 False 데이터 생성자 또는 True  데이터 생성자 둘 중 하나입니다. 이것은 Sum타입이 많은 다른 프로그래밍 언어에 존재 하는 열거 타입 생성 방법의 하스켈 버전이라는 잘못된 인상을 줄 수도 있습니다. 다음은 Sum타입을 사용하여 Name 타입을 생성 할 수있는 예입니다 중간 이름을 가진 이름의 가능성과 그렇지 않은 이름의 가능성을 모델링 할 수 있음을 주목하십시오. 대부분의 다른 언어 에서는 가운데 이름이 없는 경우 Null 값을 지정해야 합니다 . Sum 타입을 사용하여 각 사례를 명확하게 모델링 할 수 있습니다.

type FirstName = String
type LastName = String
type MiddleName = String
data Name = Name FirstName LastName 
| NameWithMiddle FirstName MiddleName LastName

이 예제에서는 두 개의 String으로 구성된 Name 또는 세 개의 String으로 구성된 NameWithMiddle 중 하나가 될 수있는 두 개의 다른 타입 생성자를 사용할 수 있습니다 여기서 두 가지 타입 중 "or"을 사용하면 타입의 의미를 표현할 수 있습니다. 우리가 타입을 결합하는 데 사용하는 도구에 "or"을 추가함으로써, Sum 타입이 없는 다른 프로그래밍 언어에서는 사용할 수 없는 가능성의 세계를 하스켈에서는 해내지 말입니다. 이전 섹션의 몇 가지 문제를 해결할 수 있는 강력한 Sum 타입을 확인 하십시오.

흥미로운 지점은 author와 artist의 차이입니다. 이 예제에서는 책 저자의 이름을 이름과 성으로 가정 할 수 있기 때문에 두 가지 타입이 필요했지만 레코드를 만드는 아티스트는 사람의 이름이나 밴드 이름이 될 수 있습니다. Product 타입만으로 이 문제를 해결하는 것은 까다로운 일입니다. Sum타입을 사용하면 이 문제를 쉽게 처리 할 수 ​​있습니다. 우리는 Author 또는 Artist 이기도 한 Creator 타입을 만들 수 있습니다.

data Creator = AuthorCreator Author | ArtistCreator Artist

우리는 이미 Author를 이름으로 정의 할 수 있는 Name 형식을 가지고 있습니다.

data Author = Author Name

아티스트는 사람의 이름이나 밴드 이름이 될 수 있기 때문에 아티스트가 더 까다 롭습니다. 이 문제를 해결하기 위해 다른 합계타입을 사용합니다.

data Artist = Person Name | Band String

이것은 좋은 해결책이지만 실생활에서 나타나는 더 까다로운 경우는 어떨까요? 예를 들어, H.P. Lovecraft와 같은 저자를 잊었습니다! 우리는 "하워드 필립스 러브 크래프트"를 사용하도록 강요 할 수 있지만, 왜 우리 자신이 우리의 데이터 모델에 의해 강요를 당해야 하는지요. 유연해야합니다. Name에 또 다른 데이터 생성자를 추가하여 쉽게 이 문제를 해결할 수 있습니다.

data Name = Name FirstName LastName
| NameWithMiddle FirstName MiddleName LastName
| TwoInitialsWithLast Char Char LastName

이제 다른 종류의 방법에 대해 걱정 할 필요가 없는 타입들이 정의되어 있습니다 Artist 및 Author 타입 모두에서 이름을 한 곳에서 정의 함으로써 이익을 얻으므로 동시에 코드 재사용의 이점을 누릴 수 있습니다 이 모든 것의 예로 HP Lovecraft Creator가 있습니다.

hpLovecraft :: Creator
hpLovecraft = AuthorCreator(Author
(TwoInitialsWithLast
'H' 'P' "Lovecraft"))

우리의 데이터 생성자는 약간 장황 할 수 있지만 실제로는 이 부분을 '추상화'하는 기능을 사용하게 될 것입니다. 이 솔루션이 제품 타입별로 강제된 계층 적 디자인을 사용하여 생성하는 솔루션과 어떻게 비교되는지 생각해보십시오. 계층적 디자인 관점에서 이름 속성이 세 개인 이름 수퍼 클래스가 있어야합니다. 세 가지 타입의 이름 만 공유 할 수있는 유일한 속성이기 때문입니다. 사용된 세 가지 데이터 생성자 각각에 대해 별도의 하위 클래스가 필요합니다. 그때도 성 (姓)이 "Louis CK"와 같은 이름이 우리 모델을 완전히 깨뜨릴 것입니다. Sum 타입을 사용하면 쉽게 해결할 수 있습니다.

data Name = Name FirstName LastName
| NameWithMiddle FirstName MiddleName LastName
| TwoInitialsWithLast Char Char LastName
| FirstNameWithTwoInits FirstName Char Char

Product 타입 관점에서의 유일한 솔루션은 사용되지 않는 특성 목록이 늘어나는 한심한 Name 클래스 를 만드는 것입니다 .

public class Name {
String firstName;
String lastName;
String middleName;
char firstInitial;
char middleInitial;
char lastInitial;
}

이것은 모든 것이 올바르게 작동하도록 하기 위해 많은 추가 코드가 필요합니다. 또한, 우리는 이름 이 유효한 상태에 있음을 보장하지 않습니다. 이러한 모든 속성이 값을 가지고 있으면 어떻게 될까요? 자바 타입 검사기는 Name 객체가 이름에 대해 지정한 제약 조건을 충족하는지 확인할 수 없습니다 하스켈에서는 정의한 명시적 타입만 존재한다는 것을 알고 있습니다.

Putting together our book store

이제 서점 문제를 다시 살펴보고 Sum 타입으로 생각하는 것이 어떻게 도움이되는지 확인해 봅시다. 강력한 Creator 타입을 사용하여 책을 다시 쓸 수 있습니다.

data Book = Book 
{author :: Creator
, isbn :: String
, bookTitle :: String
, bookYear :: Int
, bookPrice :: Double}

VinylRecord 타입 도 정의 할 수 있습니다.

data VinylRecord = VinylRecord 
{ artist :: Creator
, recordTitle :: String
, recordYear :: Int
, recordPrice :: Double}

이제 우리는 쉽게 StoreItem 타입을 생성 할 수 있습니다.

data StoreItem = BookItem Book | RecordItem VinylRecord

다시 한번 우리는 CollectibleToy 에 대해 잊어 버렸습니다 Sum 타입으로 인해이 데이터 타입을 추가하고 StoreItem 타입을 확장하여 쉽게 포함 할 수 있습니다.

data CollectibleToy = CollectibleToy 
{ name :: String
, descrption :: String
, toyPrice :: Double}

우리는 쉽게 할 수 리팩토링 STOREITEM을 하나 더 '또는'추가

data StoreItem = BookItem Book
| RecordItem VinylRecord
| ToyItem CollectibleToy

마지막으로, 우리는 모든 항목의 가격을 가져 오는 가격 함수를 작성하여 이러한 타입 모두에서 작동하는 함수를 빌드하는 방법을 보여줍니다.

price :: StoreItem -> Double
price (BookItem book) = bookPrice book
price (RecordItem record) = recordPrice record
price (ToyItem toy) = toyPrice toy

합계 타입을 사용하면 타입에 따라 표현을 크게 표현할 수 있지만 유사한 타입의 그룹을 만드는 편리한 방법을 제공합니다.

Comments