사용자 도구

사이트 도구


하스켈_대모험

하스켈 대모험

이것은 취직 전까지 할게 없어서 서러운 이민혁이 우연히 함수형 언어에 관심을 갖고 익히기 위한 여정의 한 부분이다.

학습은 여기서 하자! : http://learnyouahaskell.com/starting-out

트리비아에 대한 참고는, https://namu.wiki/w/Haskell

하스켈이 뭐지

순수 함수형 언어. 사이드 이펙트가 적은 코드를 만들어 낼수 있다고 하는 이론상 최강의 언어. 여기서 'side effect'가 없다는 것은, 같은 입력으로 다른 값을 내는 경우가 없음을 의미한다. 진정한 의미의 '함수'? 하지만 실제로는 입출력 명령 등 다양한 기능을 사용하기 때문에 어렵다 함…

  • 느긋(lazy)하다. 필요한 순간이 결정될때 까지 계산을 미루어 둔다는 것.
  • Currying, “모든 이변수 함수는 일변수 함수와 출력이 되는 일변수 함수의 조합으로 분해 가능하다” 라는 논리.
  • Type Inference. 위의 Currying을 도와준다. 즉, 타입추론을 해준다는 이야기…인것 같은데…
  • 강타입 성향. 타입을 코딩시 명시하지 않되 컴파일 시간에 Principal type1)로부터 타입을 추론하며, 런타임에서는 이러한 일을 하지 않으므로 타입 문제로부터 자유로운 코딩을 할 수 있다.
  • 타입 클래스, Polymorphism. 이것 역시 다형성을 지원하는데, 사실 우리가 OOP에서 배운 '다형성'은 Subtype Polymorphism이었다고 한다. 기실, OOP와 Polymorphism은 독립적인 개념. 이녀석은 Parametric Polymorphism을 사용하는 경우가 많다. Type Variable개념을 넣어서, 타입 자체를 입력으로 취하고 새 타입을 출력으로 내놓는 함수처럼 쓰는 것을 의미한다.2)

설치

haskell.org에 가면 자신의 플랫폼에 맞는 설치 도구를 제공한다. 알아서 잘 해보았다.

구성요소들

  • GHC - 하스켈 기본 컴파일 유닛
  • GHCi - 명령형 인터프리터.

기본 함수/명령

  • 주석사용 - '–' 키워드를 활용해 단일 행의 주석처리가 가능하다. '{- <주석 내용> -}'을 활용해 광역 주석처리가 가능하다. 허나 이 프로그래밍 패러다임에서는 주석의 남용을 권하지 않는편.
  • succ - 다음에 오는 수에 대해 1을 추가.
  • min/max - 두개의 수 중 최소값/최대값을 도출
  • div - 두개의 수를 제수/피제수로 보고 나눈다.

전위함수들은 아래와 같은 용법으로 중위 함수 처럼 써먹을 수 있다.

   91 `div` 10
  • :l (파일 이름) 소스코드를 (로드) 한다.
  • 함수의 선언시 '(따옴표) 표시를 사용할 수 있다. 예약어나 문법적 의미를 가지고 있지 않다는 이야기.
  • 함수를 대문자로 시작할 수 없다. CamelCase를 강제하는 것인가….
  • 함수의 선언. - GHCi 내부에서는 let 키워드를 활용해서 함수를 선언한다.
  • '/='연산자는 '같지'않음 을 의미한다.
let numbers = [4, 8, 12, 16, 12]
// GHCi 에서 리스트를 선언한 코드.

주로 리스트에서.

  • 리스트나 문자 등의 연결은 '++'키워드를 사용한다. 예) [1, 3, 5, 7] ++ [12, 14, 15], “hello” ++ “world” 등. 하지만 이때 하스켈은 내부적으로, 좌측의 리스트를 전부 순회하게 된다고 함. 고로 이것이 작은 사이즈의 리스트에서는 문제가 아닌데, 큰 리스트에서 문제가 발생한다.
  • 하지만, 실제로 긴 문자열 등을 통합할 때, 그러니까 긴 리스트간의 결합에는 ':' 키워드를 쓰는 것이 바람직하다고 한다.
    • 그러니까, [1,2,3]은 사실, 1:2:3:[]라고 한다. 즉슨, 이것은 '++'과 달리 앞 리스트의 '맨앞'에 붙이는 형태가 된다.
    • 근데 또 이게 리스트간 결합에는 안되니까 적재적소에 잘 활용하도록 하자.
Prelude> (1,2,3):(2,3,4):[]
[(1,2,3),(2,3,4)]
  • 하스켈에서 문자열은 그냥 '문자'의 리스트 임을 상기할 것.
  • length <리스트> - 함수의 길을 가져온다.
  • null <리스트> - 함수가 비어있는지 여부를 가져온다.
  • reverse <리스트> - 함수를 역순으로 뒤집어준다.
  • take <숫자> <리스트> - 시작점부터 숫자 인덱스 까지 리스트에 있는 목록을 가져온다.
  • drop <숫자> <리스트> - 시작점부터 숫자 인덱스 까지 리스트에 있는 목록을 버린다.
  • minimum / maximum <리스트> - 리스트 내용중 최대/ 최소 를 반환한다.
  • sum <리스트> - 리스트 내의 모든 수를 더한다.
  • product <리스트> - 리스트 내의 모든 수를 곱한다.
  • elem <숫자> <리스트> - 해당 숫자가 리스트 내에 존재하는 지 여부를 boolean 으로 알려준다. 다만, <숫자> `elem` <리스트> 형태로, 중위 함수처럼 많이 쓰임.
  • 범위의 선언 - [1..20]과 같이 선언한다. ['a'..'z'] 형태로 문자열을 쓸 수도 있다. step을 줄 수도 있다. [2, 4..20]과 같이 등차수열로.
리스트의 깊이있는 이해?(i'm a list comprehension)

마치 수학의 집합조건을 정의하는 것 처럼 리스트를 만들 수 있다.

[x*2 | x <- [1..10], x*2 >= 12]
= [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

조건을 대입하는 것 역시 가능하다. 신기해. 원리가 뭘까. 문자를 이용해서도 만들 수 있다.

*Main> let nouns = ["mayer", "dog", "windows"]
*Main> let adjectives = ["irritating", "annoying", "astonishing"]
*Main> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns]
["irritating mayer","irritating dog","irritating windows","annoying mayer","annoying dog","annoying windows","astonishing mayer","astonishing dog","astonishing windows"]

튜플

여러 종류의 항목들을 하나의 '값'처럼 저장하는데 사용한다. 튜플은 리스트와 많이 비슷하긴 한데, 튜플은 여러 종류가 될 수 있다. 리스트가 하나의 종류를 여럿 늘어놓은 것과는 대조적.

(3, 'a', "hello")

괄호를 활용해서 선언하고 사용한다.

  • 페어와 트리플의 차이, 그리고 자료형의 다름. - 가령, [(1, 2), (8, 11, 13), (15, 17)]의 형태는, 페어와 트리플이 섞여있는 형태. 고로, 같은 자료형을 담는 리스트 내에서는 에러가 날 수 밖에 없다.
  • 튜플은 고정된 크기이다. 어느정도의 요소가 필요한지 견적이 딱 나왔을때만 써먹자. 마치 관계형 스키마의 튜플처럼 말이지….
  • fst <튜플> - 페어를 받아서 '첫'요소를 반환한다. 트리플을 받으면 에러가 뜸.
  • snd <튜플> - 페어를 받아서 '두번째'요소를 반환한다.
  • zip <리스트 1> <리스트 2> - 페어의 리스트를 만든다. 두개의 리스트를 활용해 짝을 지어준다. 무한 리스트를 통해 유한 페어를 zip하는 것도 가능하다.

타입과 타입 클래스

하스켈은 정적 타입 시스템을 갖추고 있다. 이것은 타입을 '컴파일'시간에 결정지어서 안정적인 코드를 만들어낸다고 주장한다. JAVA나 Pascal과 구분되는 특징으로 하스켈은 '타입추론;type inference'이 있다. 이것은 :t (자료형)을 통해서 확인 할 수 있다. 이렇게 써먹어서 확인해보자. 웹페이지 내용을 차용함.

ghci> :t 'a'  
'a' :: Char  
ghci> :t True  
True :: Bool  
ghci> :t "HELLO!"  
"HELLO!" :: [Char]  
ghci> :t (True, 'a')  
(True, 'a') :: (Bool, Char)  
ghci> :t 4 == 5  
4 == 5 :: Bool  

위의 내용에서 '::'의 의미는 'has type of'라는 것. 이를 list나 tuple 형태에 맞게 표현해서 보여주고 있다.

함수도 타입이 있다. 함수 작성시, 명시적 타입 형을 우리가 부여한다.

*Main> let addThree x y z = x + y + z
*Main> :t addThree
addThree :: Num a => a -> a -> a -> a

즉, 함수는 특정한 '타입'에서 '타입'으로 Mapping시키고 있는 것이다. 우리가 이것을 굳이 타입을 선언할 필요가 없는 이유는, 컴파일러가 타입추론을 하기 때문에. 함수에서 보면 파라미터가 여러개 있을지언정 딱히 파라미터 타입을 식별할 만한 것은 없는 모습인데, 맨 마지막에 나오는놈이 Return값의 타입이다. 왜, Int, Int, Int → Int 가 아니고 이런형태인지는 후술.

몇가지 타입들이 있다.

  • Int, Integer : Int는 역시 정수. 헌데 32비트 Signed 정수를 표현한다. Integer도 정수이긴 한데, 이건 표현범위 제한이 없다.
  • Float : 단정도 실수.
  • Double : 배정도 실수.
  • Bool : 당연히 불리언 값. True or False
  • Char : 개개의 것은 글자, 리스트집단은 String.
  • Tuple도 타입이긴 한데, 길이에 따라 독립적이다. 두개짜리랑 세개짜리랑은 다르다는 것.

몇가지 타입 클래스 들이 있다. 연산자등등에서 사용된다.

  • Eq. 타입 테스팅 할때 나온다. ==, /=같은것에서나온다.
  • Ord. 순서를 갖는 경우에 쓴다. 비교연산자라던가. Ordering도 타입인데, GT(greater than), LT(Lesser Than), EQ(equal).
  • Show. String으로 표현될 수 있는 값들을 의미한다. 고로 이 연산자를 활용해 타입을 변환하면 전부 String화 됨.
  • Read. Show의 반대되는 개념. 값을 읽어들여서 적절한 형태로 변환 해준다.
    • 헌데 read “4”와 같은 형태로 단독으로 쓰면, GHCI가 어떤식으로 값을 반환할지 몰라서 혼란스러워한다.
  • Enum 만약 순서있게 정렬된 형태는, 가히 열거되었다 할 수있음. Enum타입 클래스의 장점은, 리스트범위에서 써먹을 수 있다는 거임.
  • Bounded 는 최대 최소 제한이 있는 자료형들을 내포하고 있다.
  • Num은 Numeric typeclass. 숫자들을 다 포함한다.
  • Integral 이것도 숫자타입클래스. Integral은 정수들만 이야기 한다.
    • fromIntegral이라는 써먹기 좋은 함수가 있다고 한다. 음수는 안먹네. 이것은 정수를 좀더 활용할 수 있는 형태의 타입으로 변환하는 역할을 한다. 예를들어서 length 연산자는 Int형을 쓰는데 이 것을 통해 Num형태까지 써먹을 수 있음.
Prelude> fromIntegral(length [1,2,3,4]) + 3.3
7.3

이걸 만약 fromIntegral을 안쓰고 length만을 쓰면 타입 불일치로 에러가 난다.

  • Floating 부동실수를 다 포함하는 타입클래스.
함수 활용 기본문법

모나드?

1)
하스켈에서 모든 함수는 Principal Type을 가진다고 한다.
2)
후대에 이것은 C++의 STL 형태로 보급된다. 결국 Generic Programming이 이개념에 들어가는 건가?
하스켈_대모험.txt · 마지막으로 수정됨: 2017/08/19 22:13 (바깥 편집)