[하스켈 기초][CIS194] 대수적 데이터 타입(Algebraic Data Type)


CIS194 2강을 정리/번역합니다. 다행히 이번 강의는 짧네요. 원문은 여기.

열거타입

하스켈도 다른 여러 언어처럼 여러 미리 정해진 값을 열거하는 방식으로 열거타입을 지정할 수 있다. 이때 data 타입이름으로 타입 정의를 시작하고 다음과 같이 여러 값을 |로 구분해 나열하는 방식으로 타입을 정의할 수 있다.

-- thing.hs
data Thing = Shoe
           | Ship
           | SealingWax
           | Cabbage
           | King
  deriving Show

이렇게 정의한 타입의 값을 ghci에서 마음대로 사용할 수 있다.

Prelude> :load thing.hs
[1 of 1] Compiling Main             ( thing.hs, interpreted )
Ok, modules loaded: Main.
*Main> shoe = Shoe
*Main> shoe
Shoe
*Main> :type shoe
shoe :: Thing

하스켈에서는 이런 Shoe, Ship, SAealingWax, Cabbage, King을 데이터 생성자(data constructor)라고 부른다. 이들을 사용하면 Thing이라는 타입의 값을 만들어낼 수 있기 때문에 데이터 생성자라는 이름이 붙었다.

thing.hs에서 deriving Show는 현 시점에서는 하스켈에게 Thing타입의 값을 문자열로 변환하는 함수를 자동으로 생성하라고 알려주는 지시문이라고 생각하면 된다.

1강에서 배운 패턴매칭이나 가드 등을 사용해 Thing 타입의 값을 다루는 함수를 작성할 수 있다.

-- thing.hs 뒷부분
isSmall :: Thing -> Bool
isSmall Ship = False
isSmall King = False
isSmall _    = True

여기서 True가 되는 값에 대한 패턴을 모두 작성할 수도 있지만 패턴매칭이 위(프로그램의 앞)에서 아래로 차례대로 시도된다는 사실을 사용해 이런식으로 False가 아닌 모든 경우에 대한 처리를 뭉뚱그릴 수 있다.

열거 타입을 넘어서 - 대수적 데이터 타입

Thing은 다른 언어의 열거 타입과 비슷하지만 실제로는 더 일반적인 대수적 데이터 타입(algebraic data type)의 일종이다. 열거 타입에서는 모든 데이터 생성자가 인자가 없는 상수 형태였지만, 다른 타입의 값을 인자로 받는 데이터 생성자를 정의할 수도 있다. 다음을 보자.

-- failableDouble.hs
data FailableDouble = Failure
                    | OK Double
  deriving Show

여기서 Failure는 앞의 Thing에서와 마찬가지로 단순한 데이터 생성자지만, OKDouble 타입의 값을 받아서 Failure 타입의 값을 만들어내는 데이터 생성자다. 따라서 OK 자체는 FailableDouble 타입의 값이 아니지만 OK에 실수를 넘겨서 만들어진 OK 1.0이나 OK 3.4등은 FailableDouble의 값이다.

그렇다면 다음과 같이 함수를 정의할 수 있을까? 그렇다. safe Div x y 패턴의 OK (x/y)라는 식은 FailableDouble 타입의 값이 된다.

-- failableDouble.hs의 뒷부분
safeDiv :: Double -> Double -> FailableDouble
safeDiv _ 0 = Failure
safeDiv x y = OK (x / y)

역으로 패턴매칭에 변수를 사용해 대수적 타입에 들어있는 값을 꺼내올 수도 있다. 다음 예의 마지막 줄의 (OK d)failureToZero에 전달된 FailableDouble 타입의 값이 OK 무엇형태의 값인 경우 매치되며, d라는 변수에 무엇을 넣어준다.

-- failableDouble.hs의 뒷부분
failureToZero :: FailableDouble -> Double
failureToZero Failure = 0
failureToZero (OK d)  = d

데이터 생성자가 2개 이상의 인자를 받을 수도 있다.

-- person.hs
data Person = Person String Int Thing
  deriving Show

brent :: Person
brent = Person "Brent" 31 SealingWax

stan :: Person
stan  = Person "Stan" 94 Cabbage

getAge :: Person -> Int
getAge (Person _ a _) = a

여기서 타입 이름 Person과 데이터 생성자 Person이 같다는 점에 유의하라. 하스켈에서는 타입 생성자(단순한 타입이름은 아무 타입도 인자로 받지 않는 타입 생성자라고 생각할 수 있다)와 데이터 생성자는 서로 다른 네임스페이스에 위치하기 때문에 이런식으로 사용할 수 있다. 이런 식으로 대수적 데이터 타입의 이름과 데이터 생성자의 이름이 같은 경우가 흔히 있는데 혼동하지 않기 바란다.

대수적 데이터 타입

일반적으로 정리하면, 대수적 데이터 타입은 1개 이상의 데이터 생성자가 있고, 각 데이터 생성자는 0개 이상의 인자를 받을 수 있다.

data AlgDataType = Constr1 Type11 Type12
                 | Constr2 Type21
                 | Constr3 Type31 Type32 Type33
                 | Constr4

AlgDataType 타입의 값을 구축하는 방법은 4가지가 있다.

  • Constr1Type11 타입의 값과 Type12 타입의 값을 넘긴다
  • Constr2Type21의 값을 넘긴다
  • Constr3Type31, Type32, Type33 타입의 값을 넘긴다
  • Constr4를 단독 사용한다
노트

타입 생성자나 데이터 생성자 이름은 항상 대문자로 시작하고, 변수(함수이름 포함) 이름은 항상 소문자로 시작하라.

패턴매칭

패턴매칭을 이미 1강에서 살펴봤지만, 일반적으로 다시 설명한다. 패턴 매칭은 어떤 데이터 생성자를 통해 값이 만들어졌는지 알아내서 값을 분해하는 과정이다. 실제로 하스켈에서는 패턴 매칭이 유일한 조건 선택 방법이다.

앞의 AlgDataType 타입의 값에 대해 어떠 작업을 해야 할지 결정하려면 다음과 같은 코드를 작성해야 한다.

foo (Constr1 a b)   = ...
foo (Constr2 a)     = ...
foo (Constr3 a b c) = ...
foo Constr4         = ...

이때 인자가 있는 생성자 패턴을 괄호로 감싸야 한다는 사실에 유의하라. 데이터 생성자를 판단한다는 요소 외에 패턴 매칭에 대해 알아둬야 할 내용은 다음과 같다.

  1. _와일드카드 패턴이다. _는 어떤 값과도 매치 가능하다.
  2. x@패턴 형태의 패턴은 패턴이라는 패턴과 값을 매치시키되 패턴과 매치된 값 전체에 x라는 이름을 붙여준다.
  3. 패턴을 내포시킬 수 있다.
-- 2번의 예
baz :: Person -> String
baz p@(Person n _ _) = "The name field of (" ++ show p ++ ") is " ++ n

*Main> baz brent
"The name field of (Person \"Brent\" 31 SealingWax) is Brent"

-- 3번의 예
checkFav :: Person -> String
checkFav (Person n _ SealingWax) = n ++ ", you're my kind of person!"
checkFav (Person n _ _)          = n ++ ", your favorite thing is lame."

*Main> checkFav brent
"Brent, you're my kind of person!"
*Main> checkFav stan
"Stan, your favorite thing is lame."

일반적으로 패턴 문법은 다음과 같은 형태라 할 수 있다.

pat ::= _                      -- 와일드카드 패턴
     |  변수                    -- 변수 패턴: 모든 값과 일치하되 var라는 이름을 부여함
     |  변수 @ ( 패턴 )          -- @패턴: 패턴과 일치하는 경우 그 값에 var라는 이름을 부여함
     |  ( 생성자 패턴1 패턴2 ... 패턴n ) -- 생성자 패턴

여기서 패턴이나 패턴1, …, 패턴n 에는 이 문법을 사용한 패턴이 재귀적으로 내포될 수 있다.

여기에 설명하지 않은 다른 패턴 문법과 기능이 있지만 그에 대해서는 나중에 필요할 때 설명할 것이다.

2'c'와 같은 리터럴 값은 인자가 없는 생성자 패턴으로 간주할 수 있다. 따라서 마치 다음과 같이 IntChar가 정의된 것처럼 작동한다. 물론 IntChar가 정말 이런식으로 정의된 것은 아니다.

data Int  = 0 | 1 | -1 | 2 | -2 | ...
data Char = 'a' | 'b' | 'c' | ...

case

case식을 쓰면 패턴 매칭을 사용할 수 있다. 다음 예제는 문자열(=Char의 리스트)에 대해 case 식을 사용한 예다.

ex03 = case "Hello" of
           []      -> 3
           ('H':s) -> length s
           _       -> 7

"Hello"'H'로 시작하므로 ex03의 값은 "ello"의 길이인 4가 된다.

실제로 1강에서 본 함수 정의 패턴매칭은 case식 정의를 더 쉽게 사용할 수 있게 해주는 구문적 편의(syntatic sugar)일 뿐이다. failureToZerocase 식으로 다시 정의한 failureToZero'는 다음과 같다.

failureToZero' :: FailableDouble -> Double
failureToZero' x = case x of
                     Failure -> 0
                     OK d    -> d

재귀적 데이터 타입

대수적 타입을 재귀적으로 만들 수도 있다. 이미 그런 재귀적 데이터 타입을 하나 살펴봤다. 바로 리스트가 재귀적 데이터 타입이다. 리스트를 정의하면 다음과 같다.

data IntList = Empty | Cons Int IntList

하스켈의 리스트 정의도 이와 비슷하지만 빈 리스트에 []를, 콘스에 :를 사용한다는 점이 다를 뿐이다(물론 하스켈 리스트는 임의의 타입에 작용할 수 있다는 점에서도 IntList와 다르다. 그에 대해선 다음 강의에서 다룬다).

재귀적 데이터 타입을 처리하기 위해 재귀 함수를 작성하는 경우도 흔하다.

intListProd :: IntList -> Int
intListProd Empty      = 1
intListProd (Cons x l) = x * intListProd l

이진 트리도 재귀적으로 정의할 수 있다. 쓸모는 잘 모르겠지만 리프에 Char 값을, 내부 노드에 Int 값을 저장하는 트리는 다음과 같이 정의할 수 있다(예제니까 ^^).

data Tree = Leaf Char
          | Node Tree Int Tree
  deriving Show

tree :: Tree
tree = Node (Leaf 'x') 1 (Node (Leaf 'y') 2 (Leaf 'z'))

[사족] 대수적 데이터 타입이라는 용어 소개

원 강의에서는 대수적 데이터 타입이라는 용어에 대해 따로 자세히 설명하지는 않았다(참고로 ADT는 보통 추상 데이터 타입(abstract data type)이라는 다른 용어의 줄임말로 쓰이기 때문에 대수적 데이터 타입의 줄임말로 ADT를 사용할 경우에는 조심해 사용해야 한다)를 . 대수적 데이터 타입이란 용어에 대해 잠깐 생각해보자.

먼저 대수라는 말은 어떤 뜻일까? 대수라는 말은 어떤 대상들로 이뤄진 집합과 그 집합에 속한 원소에 대해 적용할 수 있는 연산을 통해 대상 사이의 관계나 성질, 계산 법칙 따위를 연구하는 수학 분야다.

예를 들어 덧셈과 곱셈이라는 2가지 연산을 수집합에 대해 사용할 때 어떤 값들이 만들어지는지 그 법칙을 연구하는 것을 일컬어 기초적인 대수학이라 할 수 있다. 선형대수학은 벡터공간 상의 연산 구조에 대해 공부하는 것이다.

데이터베이스 관련 교재나 수업을 들었다면 마찬가지로 관계 대수(relational algebra)에 대해 들어봤을 것이다. 관계 대수는 기본 집합 연산(집합의 합, 집합의 차, 집합의 데카르트 곱(cartesian product)), 프로젝션(π), 셀렉션(σ) 등을 사용해 관계형 데이터베이스에 대한 연산의 성질을 연구한다.

이런 대수에 대한 정의를 따라 정리하자면, 대수적 데이터 타입의 대상은 데이터 타입이 되고, 데이터 타입에 대한 연산은 다음 두가지가 있다.

  • 합: 둘 이상의 데이터 타입의 합집합을 만든다
  • 곱: 둘 이상의 데이터 타입의 데카르트 곱을 만든다.

이런 두가지 연산으로 정의되는 모든 데이터 타입을 대수적 데이터 타입이라고 할 수 있다. 일반적인 언어의 경우 대표적인 합 타입은 태그드 유니언(tagged union) 또는 베리언트 레코드(variant record)라고 부르는 타입이다. C++의 union이나 하스켈 등의 베리언트 레코드가 대표적이다. 반면 곱 타입의 대표적인 예로는 n-튜플이나 객체지향언어의 객체, C나 C++의 구조체 등을 들 수 있다.

하스켈 data 타입의 경우 합 연산은 데이터 생성자를 |로 구분해 나열하는 것에 의해 이뤄진다. 반면 곱 연산은 데이터 생성자가 둘 이상의 데이터 타입을 인자로 받는 형태로 이뤄진다.

대수적 데이터 타입의 원소 개수

한편 대수적 데이터 타입이 가질 수 있는 값의 개수(원소 개수)와 관련해 다음과 같이 정리할 수 있다.

  • 합연산으로 만들어진 데이터 타입의 원소 개수는 데이터 생성자 인자 타입의 원소개수의 합계와 같다. 예를 들어 다음과 같은 data Union 타입이 있다면,
data Union = CHR Char | INT Int

Union 타입에 속하는 원소의 개수는 Char 타입에 속하는 원소(값)의 개수와 Int 집합에 속하는 원소 개수를 합한 것이다. Union타입의 값은 항상 CHR cINT i 중 한 형태일 것이고, ci에는 각각 Char 타입의 값과 Int 타입의 값이 들어갈 수 있음을 알고 있으므로 이를 쉽게 이해할 수 있다.

  • 인자를 받지 않는 데이터 생성자의 경우에는 그 자체만을 값으로 가지는 집합을 구성한다고 생각하면 된다. 따라서 다음과 같은 열거 타입의 경우
data Enum = Const1 | Const2 

가능한 값의 개수는 2가 된다.

  • 곱연산의 경우 각 인자 타입의 원소 개수를 모두 곱한 개수가 곱 타입이 가질 수 있는 원소의 개수다. 다음을 보자.
data T = T Int Int

T 데이터 생성자로 만들 수 있는 값은 T i1 i2 형태이고, i1i2에는 서로 독립적으로 Int 값이 들어간다. 따라서 전체 개수는 Int 타입의 모든 값의 개수를 곱한 것과 같고, T i1 i2의 모든 값을 나열하는 것은 Int x Int라는 데카르트 곱에 속하는 각 원소 (i1, i2)에 대해 Int 를 덧붙이는 것이라 생각할 수 있다.

한편 앞에서 본 리스트를 생각해 본다면, 재귀적인 추상 데이터 타입의 크기에 대한 힌트를 얻을 수 있다.

data IntList = Empty | Cons Int IntList

데이터 생성자의 개수는 2개로 유한하지만, Cons 데이터 생성자가 IntIntList의 데카르트 곱에 해당하는 개수의 원소를 만들어낼 수 있고, IntList의 원소 개수가 다시 Cons에 쓰이기 때문에 무한히 많은 원소가 정의되고, 이 IntList정수x정수x정수…의 무한한 데카르트 곱에 해당하기 때문에 셀 수 없는(uncountable) 집합임을 알 수 있다. 따라서 정수 리스트의 원소 개수는 정수 집합보다 더 크기(cardinality)가 크다. 물론 Int가 아니라 원소 개수가 한정된 집합을 가지고 List를 정의한다면 크기가 달라질 수도 있을 것이다.

연습문제 - 로그 파일 파싱

연습문제는 원문은 여기

시스템이 무언가 잘못됐다. error.log에 있는 로그를 분석해야 한다. 로그 구조는 다음과 같다.

  • 각 줄의 첫번째 필드는 로그 유형을 표현하는 문자로 시작한다.

    • 'I'는 정보(information)다.
    • 'W'는 경고(warning)다.
    • 'E'는 오류(error)다.
  • 오류 로그의 경우에만 두번째 필드에 오류의 심각도(severity)를 나타내는 정수가 온다. 1은 경미한 오류로 여유가 될때 해결해도 되고, 100은 경천동지할만큼 심각한 오류다.

  • 로그 유형 뒤에는 타임스탬프(정수값)가 온다. 오류 로그의 경우 타임스탬프 앞에 심각도가 온다는 사실에 유의하라.

  • 타임스탬프 뒤부터 줄 맨 끝까지 실제 로그 메시지가 온다.

다음 예를 보자.

I 147 mice in the air, I’m afraid, but you might catch a bat, and
E 2 148 #56k istereadeat lo d200ff] BOOTMEM

이런 처리를 돕기 위해 Log.hs에 다음과 같이 데이터 타입을 정의해 뒀다.

-- Log.hs
data MessageType = Info
                 | Warning
                 | Error Int
                 deriving (Show, Eq)
type TimeStamp = Int
data LogMessage = LogMessage MessageType TimeStamp String
                | Unknown String
                deriving (Show, Eq)

LogMessage는 로그 파일에 담겨있는 각 줄을 분석한 결과를 표현한다. 앞에서 설명한 형식을 따르지 않아 분석할 수 없는 로그 파일은 Unknown에 내용을 담아둔다.

연습문제 코드를 다음과 같이 시작하는 LogAnalysis.hs에 작성하라.

{-# OPTIONS_GHC -Wall #-}
module LogAnalysis where

import Log

이렇게 코드를 시작하면 LogAnalysis라는 모듈을 정의한다. 이때 LogAnalysis 모듈 안에서 Log 모듈에 있는 정의를 임포트했기 때문에, Log.hs 안에 있는 함수나 타입을 LogAnalysis 안에서 사용할 수 있다.

연습문제 1

첫 단계로 각 메시지를 분석하는 parseMessage 함수를 만들라.

parseMessage :: String -> LogMessage

예를 들면 다음과 같다.

parseMessage "E 2 562 help help"
    == LogMessage (Error 2) 562 "help help"

parseMessage "I 29 la la la"
    == LogMessage Info 29 "la la la"

parseMessage "This is not in the right format"
    == Unknown "This is not in the right format

로그 메시지 한 줄을 파싱할 줄 안다면 전체 로그 파일을 분석할 수 있다. 다음 함수를 정의하라.

parse :: String -> [LogMessage]

parse의 입력은 전체 로그 파일을 한꺼번에 읽어서 문자열로 만든 것이다. Log.hs에 보면 이 parse 함수를 테스트하기 위한 testParse 함수가 있다. testParse 함수에 여러분이 만든 parse함수와 분석할 로그의 줄 수, 파일이름을 전달하면 된다. 예를 들어 다음과 같이 하면 error.log 파일의 맨 앞 10 줄을 분석할 수 있다.

testParse parse 10 "error.log"

(원 문제에서는 don’t reinvent wheel이라고 했지만 나는 그냥 잘 모르겠는건 일반 재미삼아 구현해 보고, 프렐류드 함수를 나중에 사용할 것이다.)

바퀴를 재발명하지 말라. StringInt로 바꾸고 싶다면 read를 사용하면 된다. 아마도 lines, words, unwords, take, drop, (.) 등을 사용하게 될 것이다.

로그 한데 모으기

분석 대상 로그는 세계 곳곳의 여러 서버에서 생성되기 때문에 번개, 디스크 실패 등등 다양한 문제로 인해 로그 형식이 틀릴 수 있다. 로그 메시지를 체계적으로 관리하기 위해 LogMessage를 2진 검색 트리(binary search tree)로 관리하기 위한 데이터 구조를 만들자.

data MessageTree = Leaf
                 | Node MessageTree LogMessage MessageTree

MessageTree는 재귀적인 데이터 타입이다. Node는 왼쪽 하위트리, 로그메시지, 오른쪽 하위트리를 자식으로 하며, Leaf는 빈 트리를 표현한다.

MessageTree를 타임스탬프에 따라 정렬하자. 어떤 Node에 있는 LogMessage의 타임스탬프는 항상 그 Node 왼쪽 하위 트리에 속한 모든 노드에 있는 LogMessage의 타임스탬프보다 더 크거나 같아야 하고, 오른쪽 하위 트리에 속한 모든 노드에 있는 LogMessage의 타임스탬프보다는 작거나 같아야 한다.

타임스탬프가 없는 Unknown 로그 메시지는 MessageTree에 넣어서는 안된다.

연습문제 2

다음과 같은 함수를 만들라. 이 함수는 LogMessage를 기존 MessageTree에 추가한 새 MessageTree를 반환한다. 이때 이 insert 함수가 두번째 인자로 받는 MessageTree는 이미 잘 정렬된 상태라고 가정하라. 반환하는 MessageTree는 앞에서 말한 Node의 가정(타임스탬프의 대소관계)을 따르면서 첫번째 인자인 LogMessage와 두번째 인자인 MessageTree에 있는 모든 LogMessage들이 들어있는 트리여야 한다.

만약 이 insert 함수에 Unknwon 로그메시지를 전달하면 원래의 로그트리를 그대로 반환하면 된다.

insert :: LogMessage -> MessageTree -> MessageTree

연습문제 3

LogMessage 하나를 MessageTree에 추가하는 함수가 있으므로 메시지의 리스트를 MessageTree로 변환하는 함수를 구현할 수 있다. Leaf 트리로부터 시작해 insert를 반복 호출하면서 트리를 구성하는 식으로 구현하면 된다. 다음과 같은 타입의 build 함수를 만들라.

build :: [LogMessage] -> MessageTree

연습문제 4

이제는 정렬된 트리로부터 정렬된 LogMessage의 리스트를 만드는 다음과 같은 함수를 만들 수 있다.

inOrder :: MessageTree -> [LogMessage]

이와 같이 어떤 이진 검색 트리에서 정렬 대상 키(우리 예제에서는 타임스탬프)를 바탕으로 각 원소를 작은 것부터 큰 것 순으로 오름차순(ascending) 정렬한 리스트를 만들어내는 과정을 중위 순화(in-order traversal)라고 말한다. 2진 트리 검색에 대한 중위순회 알고리즘은 다음과 같은 재귀적 알고리즘이다.

  1. 루트 노드의 왼쪽 트리에 대해 중위 순회 알고리즘을 적용한다.
  2. 루트 노드를 출력한다.
  3. 루트 노드의 오른쪽 트리에 대해 중위 순회 알고리즘을 적용한다.

로그 파일 해부

연습문제 5

이제 로그 메시지를 타임스탬프 순으로 정렬할 수 있다. 마지막으로 의미있는 정보를 추출하는 과정이 필요하다. “의미 있는”이라는 말은 “심각도가 최소 50이상”이라는 뜻으로 정의하자.

다음과 같은 함수를 만들라.

whatWentWrong :: [LogMessage] -> [String]

이 함수는 정렬되지 않은 LogMessage의 리스트를 받아서 심각도가 50 이상인 오류 메시지의 목록을 타임스탬프 순으로 정렬해 반환한다.

예를 들어 sample.log라는 로그 파일이 다음과 같다면,

I 6 Completed armadillo processing
I 1 Nothing to report
E 99 10 Flange failed!
I 4 Everything normal
I 11 Initiating self-destruct sequence
E 70 3 Way too many pickles
E 65 8 Bad pickle-flange interaction detected
W 5 Flange is due for a check-up
I 7 Out for lunch, back in two time steps
E 20 2 Too many pickles
I 9 Back from lunch

whatWentWrongsample.log에 적용한 출력은 다음과 같아야 한다.

[ "Way too many pickles"
, "Bad pickle-flange interaction detected"
, "Flange failed!"
]

whatWentWrong을 테스트하기 위해 testWhatWentWrong 함수를 사용할 수 있다. Log 모듈에 testWhatWentWrong가 들어있다. testWhatWentWrong에 여러분이 만든 parse, whatWentWrong 함수를 전달하고 분석할 로그 파일의 경로를 넘겨야 한다.

(선택문제) 연습문제 6

여러가지 상황을 고려해 볼 때, 최근의 여러 문제가 자기 중심적인 해커 한명 때문에 발생한 것으로 여겨진다. 범인이 누군지 알아낼 수 있나?

Related Posts

[하스켈 기초][CIS194] 고차 프로그래밍과 타입 추론 연습문제 풀이

CIS194 4강 고차 프로그래밍과 타입 추론 연습문제 풀이

[하스켈 기초][CIS194] 4강 - 고차 프로그래밍과 타입 추론

무명 함수(람다) 정의 방법을 알려주고, 함수 합성, 커링, 부분 적용, 폴드와 같은 고차 함수 프로그래밍에 대해 설명한다.

[하스켈 기초][CIS194] 재귀 연습문제 풀이

CIS194 3강 재귀 관련 연습문제 풀이

[하스켈 기초][CIS194] 재귀 패턴, 다형성, 프렐류드

실용적인 재귀 패턴을 설명하고 재귀 패턴을 추상화한 몇몇 함수를 정리한 다음, 하스켈 프렐류드에 대해 설명한다

[하스켈][팁] 하스켈 연산자 검색

하스켈 연산자를 검색하고 싶을 때 사용할 수 있는 도구 hoogle을 소개한다.

[하스켈 기초][CIS194] 대수적 데이터 타입(Algebraic Data Type) 연습문제 풀이

대수적 데이터 타입 소개와 재귀적 데이터 구조 소개 연습문제 풀이

[하스켈 기초][CIS194] 1강 하스켈 소개 - 연습문제 풀이

CIS194 1강 연습문제 풀이

[하스켈 기초] 하스켈 공부법

하스켈 익스퍼트 비기너가 되기 위한 여정을 시작합니다!

[하스켈 기초][CIS194] 하스켈 소개

하스켈을 소개하고 기본 타입, 산술연산, `if`식, 함수정의, 순서쌍, 인자가 여럿 있는 함수, 리스트 등에 대해 설명한다.

[seed] 코루틴과 컨티뉴에이션

코루틴과 컨티뉴에이션에 대해. 나중에 정식 글을 쓰기 위한 씨앗.