INDIES

[Learn You a Haskell For Great Good!] 2. 시작하기(1) 본문

Haskell/LYAH

[Learn You a Haskell For Great Good!] 2. 시작하기(1)

jwvg0425 2015. 1. 16. 16:28

Learn You a Haskell For Great Good!


이 게시글은 http://learnyouahaskell.com/chapters 사이트에 올라와있는 글을 한글로 번역한 것입니다.의역이 굉장히 많으니 주의...


2. 시작하기


제자리에, 준비, 땅!


좋아, 이제 시작할 때다! 만약 네가 소개글을 읽지 않고 넘겨버리는 끔찍한 부류의 인간이라해도, 어쨌든 소개글의 마지막 섹션을 읽고 싶어하게 될 거야. 왜냐하면 이 튜토리얼을 따라오기 위해 필요한 것들에 대한 설명과, 함수를 어떻게 하면 불러올 수 있는 지에 대한 설명이 거기에 다 있거든. 우리가 여기서 처음으로 하게 될 것은 ghc의 상호작용 모드를 실행하는 것과, haskell에 대한 기본적인 감각을 느낄 수 있는 몇몇 함수들을 호출해보는 거야. 터미널 창을 열고 ghci라고 쳐봐. 아마 아래와 같은 환영 문구를 볼 수 있을 거야.


  1. GHCi, version 6.8.2: http://www.haskell.org/ghc/  :? for help  
  2. Loading package base ... linking ... done.  
  3. Prelude

축하해. 너는 GHCI 안에 들어왔어! 여기서 프롬프트는 Prelude> 지만 이 이름이 세션 내용에 집중하기 힘들게 만들 수 있기 때문에, 앞으로 우리는 ghci> 라는 이름을 이용할 거야. 만약 네가 똑같은 프롬프트를 사용하기 원한다면, 그저 :set prompt "ghci> "라고 치면 돼.

여기 몇 가지 단순한 산술식 예제가 있어.

  1. ghci> 2 + 15  
  2. 17  
  3. ghci> 49 * 100  
  4. 4900  
  5. ghci> 1892 - 1472  
  6. 420  
  7. ghci> 5 / 2  
  8. 2.5  
  9. ghci> 

 이 건 너무 자명하지. 물론 한 줄에 여러 개의 연산자를 이용할 수도 있고, 일반적인 연산의 우선순위들도 적용돼. 연산 우선순위를 바꾸기 위해서 괄호를 이용할 수 있어.

  1. ghci> (50 * 100) - 4999  
  2. 1  
  3. ghci> 50 * 100 - 4999  
  4. 1  
  5. ghci> 50 * (100 - 4999)  
  6. -244950 

깔끔하지? 흠, 나는 이게 깔끔하지 않다는 걸 알아. 내 말을 주의깊게 봐봐. 여긴 주의해야할 작은 위험이 있는데, 그건 바로 음수야. 음수를 다룰 때에는 항상 음수를 괄호로 싸는게 좋아. 5* -3과 같은 수식은 GHCI가 경고를 할테지만, 5 * (-3)은 정상적으로 동작할거야.

부울 대수(Boolean Algebra)도 굉장히 직관적이야. 네가 이미 알고 있다시피, &&는 and를, ||는 or를 의미해. not은 True 또는 False의 부정을 의미하고.

  1. ghci> True && False  
  2. False  
  3. ghci> True && True  
  4. True  
  5. ghci> False || True  
  6. True   
  7. ghci> not False  
  8. True  
  9. ghci> not (True && True)  
  10. False  

동등성에 대한 비교도 이와 비슷해.

5+"llama" 나 5 ==True와 같은 연산은 어떻게 동작할까? 음, 첫 번째 식을 실행하면 우리는 굉장히 끔찍한 에러 메시지를 보게 될거야!

  1. No instance for (Num [Char])  
  2. arising from a use of `+' at <interactive>:1:0-9  
  3. Possible fix: add an instance declaration for (Num [Char])  
  4. In the expression: 5 + "llama"  
  5. In the definition of `it': it = 5 + "llama"  

 으악! 여기서 GHCI가 말하고자 하는 건 "llama"는 숫자가 아니고, 그래서 이걸 어떻게 5랑 더해야될 지 모르겠다는 거야. 이게 "llama"가 아니라 "four"나 "4"라도 마찬가지야. Haskell은 그게 숫자가 될 수 있는 지 고려하지 않아. + 연산자는 좌변과 우변에 숫자가 올거라고 예측해. True == 5 라는 수식을 실행할 경우에는, GHCI는 우리에게 두 개의 타입이 일치하지 않는다고 말할거야. + 연산자가  숫자로 고려할 수 있는 것들에 대해서만 동작한다면, == 연산자는 서로 비교될 수 있는 두 개체에 대해서만 동작해. 그러나 문제가 되는 건 두 개가 서로 같은 타입의 개체여야만 한다는 거지. 너는 사과와 오렌지를 비교할 수 없어. 우리는 타입에 대해서 조금 더 있다가 살펴보게 될거야. 알아둘건, 5 + 4.0은 가능하다는 거야. 왜냐하면 5는 교활해서 정수처럼도, 부동소수점처럼도 행동할 수 있기 때문이지. 4.0은 정수처럼 행동할 수 없고, 따라서 이 경우 5는 부동소수점으로 다뤄지게 돼.

 굳이 알 필요는 없지만, 우리가 지금까지 써온 것들은 전부 함수야. 예를 들어서, *는 두 개의 숫자를 받아서 그 둘을 곱한 결과를 돌려주는 함수지. 위에서 본 것처럼, 우리는 이걸 두 숫자 사이에 놓음으로써 호출할 수 있어. 우리는 이런걸 중위(infix) 함수라고 부르지. 숫자와 같이 사용하지 않는 대부분의 함수들은 전위(prefix) 함수야. 이제 이것들을 살펴보자고.

 함수는 종종 전위(prefix)적으로 사용되고 따라서 이제부터 따로 명시하지 않는 한 함수는 전위 함수를 말하는 거라고 가정하자. 대부분의 명령형 언어에서 함수는 함수 이름을 쓰고, 콤마로 구분되는 함수의 인자를 이어지는 괄호 안에 쓰는 걸 통해 호출되지. Haskell에서, 함수는 함수 이름을 쓰고, 그 뒤에 공백과 공백으로 구분되는 인자들을 순서대로 씀으로써 호출돼. 처음이니까, 우리는 Haskell에서 가장 지루한 함수 중 하나를 호출해볼거야.



  1. ghci> succ 8  
  2. 9

succ 함수는 successor가 정의된 어떤 것을 받아 그 successor를 돌려줘. 위에서 봤듯이, 단지 공백만으로 함수의 이름과 인자를 구분해. 여러 개의 인자를 가진 함수의 호출도 마찬가지로 단순해. min 함수와 max 함수는 정렬될 수 있는 두 개의 인자를 받지(숫자 같은 것들 말이야!). min은 더 작은 것, max는 더 큰 것을 돌려줘. 직접 확인해봐.

  1. ghci> min 9 10  
  2. 9  
  3. ghci> min 3.4 3.2  
  4. 3.2  
  5. ghci> max 100 101  
  6. 101   

 함수 어플리케이션(함수 이름을 적고 공백과 함께 함수의 인자들을 입력하여 함수를 호출한 것)은 모든 것들 중에서 가장 높은 우선순위를 가져. 그 말은, 아래의 두 문장이 동일한 뜻이라는 거야.

  1. ghci> succ 9 + max 5 4 + 1  
  2. 16  
  3. ghci> (succ 9) + (max 5 4) + 1  
  4. 16  

 하지만, 우리가 9와 10의 곱의 successor를 얻고 싶다면, succ 9*10 이라고 쓰면 안 돼. 왜냐하면 이건 9의 successor를 가져온 다음, 10과 곱해서 100을 돌려주기 때문이지. 91이라는 결과를 얻기 위해서는 succ (9*10)이라고 써야돼.

 만약 함수가 두 개의 인자를 취한다면, 우리는 이걸 backtick(`)으로 감싸는 것을 통해 중위(infix) 함수로 호출할 수 있어. 예를 들어, div 함수는 두 개의 정수를 취하고 그 둘을 나눈 몫을 돌려줘. div 92 10은 결과로 9를 돌려주지. 하지만 이렇게 호출하면, 어떤게 나누는 수이고 어떤게 나눠지는 수인지 약간 혼란이 있을 수가 있어. 그래서 우리는 이걸 92 `div` 10 과 같은 식으로 중위(infix) 함수로 호출할 수 있고, 이게 훨씬 명확하지.

 명령형 언어를 배우다 온 사람들은 함수 호출에서 괄호를 사용하는 표기법에 집착하는 경향이 있어. 예를 들자면, C언어에서는 함수를 호출하기 위해 foo(), bar(1), 또는 baz(3, "haha")와 같이 쓰지. Haskell에선 함수를 쓸 때 단순히 공백만 사용해. 따라서 위와 같은 함수들은 foo, bar 1, 그리고 baz 3 "haha"와 같이 써서 호출되지. 따라서 bar (bar 3)이라고 쓰인 코드는, bar이 bar과 3이라는 두 개의 인수를 이용해 호출되는게 아니라, 먼저 bar과 3을 이용해 함수를 호출해 그 결과로 다시 bar을 호출한다는 거야. C로 따지자면 bar(bar(3))과 같지.


갓난아기의 첫번째 함수


이전 섹션에서 우리는 함수를 호출하는 방법에 대해 기본적인 감을 익혔지. 이제는 우리만의 함수를 만들어 볼 차례야! 네가 좋아하는 텍스트 편집기를 켜고 숫자 하나를 받아 두 배로 늘려서 돌려주는 아래 함수를 타이핑해봐.

  1. doubleMe x = x + x  

함수는 호출하는 방식과 비슷한 방식으로 정의해. 함수의 이름, 그리고 그 뒤에 함수가 받는 인자들이 공백을 기준으로 분리되어 뒤따라 나오지. 하지만 정의를 할 때는, 우리가 정의하고자 하는 함수 뒤에 = 기호가 붙어. 방금 작성한 걸 baby.hs, 혹은 아무거나 마음에 드는 이름으로 저장해봐. 그리고 해당 파일이 저장된 곳에서 ghci를 실행시켜봐. ghci에서 :l baby 라고 타이핑하면 네가 작성한 스크립트가 불러와지고,우린 이제 우리가 정의한 함수를 갖고 놀 수 있지.

  1. ghci> :l baby  
  2. [1 of 1Compiling Main             ( baby.hs, interpreted )  
  3. Ok, modules loaded: Main.  
  4. ghci> doubleMe 9  
  5. 18  
  6. ghci> doubleMe 8.3  
  7. 16.6   

+ 연산자는 정수 뿐만 아니라 부동소수점, 그리고 숫자와 유사하게 다뤄질 수 있는 어떤 개체에 대해서도 잘 동작하기 때문에, 우리의 함수는 어떤 숫자에 대해서도 잘 동작해. 이제 두 개의 숫자를 받아서 두 숫자 각각을 2배로 늘린 뒤 둘을 더하는 함수를 만들어 보자.

  1. doubleUs x y = x*2 + y*2   

 간단하지. 우리는 이걸 doubleUs x y = x + x + y + y 라고 정의할 수도 있어. 함수의 동작 결과는 굉장히 뻔하겠지. 테스트해보기 위해선 위 함수를 baby.hs 파일의 맨 뒤에 덧붙이고, 저장한 다음 GHCI에서 다시 :l baby 명령을 타이핑하면 돼.

  1. ghci> doubleUs 4 9  
  2. 26  
  3. ghci> doubleUs 2.3 34.2  
  4. 73.0  
  5. ghci> doubleUs 28 88 + doubleMe 123  
  6. 478  

 눈치챘겠지만, 만들고자 하는 함수의 정의 부분 내에서도 함수를 호출할 수 있어. 그걸 염두에 두면, 우리는 doubleUs를 아래와 같이 정의할 수 있지.

  1. doubleUs x y = doubleMe x + doubleMe y   

이게 Haskell에서 일반적으로 접할 수 있는 패턴의 가장 간단한 예제야. 명백히 올바른(correct) 기본적인 함수들을 만든 다음, 그것들을 결합해서 더 복잡한 함수들을 만드는 거지. 이 방법을 통해 반복도 피할 수 있어. 만약에, 어떤 수학자가 2랑 3이 알고보니 똑같은 숫자였다는 증명을 해냈다고 치자. 그럼 우리는 우리 프로그램을 어떻게 수정해야할까? 그냥 doubleMe 함수만 x + x + x라는 결과를 돌려주도록 수정하면 돼. 왜냐하면 doubleUs 함수는 doubleMe 함수를 호출해서 동작하고, 따라서 이 2랑 3이 똑같은 이상한 세계에서도 잘 동작하도록 자동으로 수정되거든.

 Haskell에서 함수는 어떤 특정한 순서를 갖고 있지 않아. 그래서 doubleMe 함수를 먼저 선언하고 doubleUs 함수를 선언하나 그 반대로 선언하나 별 상관은 없어.

 이제 숫자에 2를 곱하되, 그 숫자가 100이하일 때만 2를 곱해주는 함수를 만들어볼 차례야. 왜냐하면 100을 넘는 숫자는 이미 춧분히 큰 숫자거든!

  1. doubleSmallNumber x = if x > 100  
  2.                         then x  
  3.                         else x*2   


이제 Haskell의 if 문(if statement)를 소개할 차례야. 아마 다른 언어에서의 if 문과 이미 상당히 친숙하리라고 생각해. Haskell에서의 if문과 명령형 언어에서의 if문의 차이는, Haskell에서는 if문의 else 부분을 생략할 수 없다는 거야. 명령형 언어에서는 조건이 만족되지 않으면 많은 명령을 생략하고 넘어갈 수 있지만, Haskell에서는 모든 표현식과 함수가 반드시 뭔가를 돌려줘야만 해. 또 if문을 한 줄에 모두 다 쓸 수 있고 난 이게 더 가독성이 좋다고 생각해. Haskell의 if문에 대한 또다른 특징은, 이게 표현식(expression)이라는 거야. 표현식은 기본적으로 숫자를 값을 돌려주지. 5는 5라는 값을 돌려주기 때문에 표현식이고, 4+8은 x+y가 x와 y의 합을 돌려주는 표현식이기 때문에 마찬가지로 표현식이야. Haskell의 if문에서 else 부분은 필수적이고, 따라서 if문은 항상 뭔가를 돌려주기 때문에 역시 마찬가지로 표현식이야. 우리가 아까 만든 함수에서 나온 결과에서 항상 1을 더해 돌려주는 함수를 만든다고 해보자. 그럼 아래와 같은 방식으로 정의할 수 있을거야.

  1. doubleSmallNumber' x = (if x > 100 then x else x*2) + 1  

만약 괄호를 빼먹는 다면 x가 100보다 크지 않을 때만 1을 더하게 될거야. 함수 이름의 끝에 '이 붙어있는 걸 봐봐. 아포스트로피(apostrophe, ')  기호는 Haskell의 구문에서 특별한 의미를 갖고 있지 않아. 함수 이름에서 사용가능한 문자지. 보통의 경우 함수의 좀 더 엄격한 버젼(게으르지(lazy) 않은 것)이나 살짝 수정된 버젼의 함수, 변수를 나타내기 위해 ' 기호를 사용해. ' 기호가 함수에서 사용가능한 문자기 때문에 함수를 아래와 같은 방식으로도 만들 수 있어.

  1. conanO'Brien = "It's a-me, Conan O'Brien!"   

 이 함수에서 두가지 명심해둘만한 것이 있어. 첫 번째는 함수의 이름에서 우리는 Conan의 이름을 대문자로 시작할 수 없다는 거야. 왜냐하면 함수의 이름은 대문자로 시작할 수 없거든. 이 점에 대해선 나중에 다시 살펴볼거야. 두 번째는 이 함수는 어떤 인자도 취하지 않는다는 거야. 우리는 이걸 보통 정의(definition) 또는 이름(name)이라고 불러. 이름(과 함수)이 뭘 의미하는 지를 한 번 정의하고 나면 바꿀 수 없기 때문에, conanO'Brien과 "It's a-me, Conan O'Brien!" 문자열은 서로 교환되어 사용될 수 있지.