48시간 안에 나만의 스킴 만들기/첫 번째 단계
먼저 GHC를 설치하자. GHC는 리눅스 배포판에 따라 다르지만 미리 설치되어 있을 수도 있고 없다면 GHCup으로 GHC를 설치하자.
이맥스 사용자라면 이맥스 모드에 문법 강조와 자동 들여쓰기 같은 좋은 기능이 꽤 있으니 사용해보자. 윈도우 사용자는 메모장이나 다른 텍스트 에디터를 사용하면 된다. 하스켈 문법은 메모장에서 입력하기 편하지만 들여쓰기는 조심해야 한다. 이클립스 사용자는 eclipsefp 플러그인을 사용하는 게 좋다.
이제 첫 번째 하스켈 프로그램을 만들어 보자. 이 프로그램은 명령줄에서 이름을 입력 받고 화면에 인사말을 출력한다. 파일 확장자를 .hs
로 정하고 다음 코드를 적어보자. 들여쓰기를 틀리면 컴파일이 안 될 수 있다.
module Main where
import System.Environment
main :: IO ()
main = do
args <- getArgs
putStrLn ("Hello, " ++ args !! 0)
위 코드를 살펴보자. 처음 두 줄은 이름이 Main
인 모듈을 만들고 System
모듈을 가져온다는 의미이다. 모든 하스켈 프로그램은 Main
모듈의 main
이라는 액션으로 시작한다. 모듈은 다른 모듈을 가져올 수 있다. 컴파일러가 실행 파일을 만들려면 Main
모듈이 반드시 있어야 한다. 하스켈은 대소문자를 구분한다. 모듈 이름은 대문자로 시작해야 하고 정의는 소문자로 시작해야 한다.
다음 코드는 타입 선언이다.
main :: IO ()
main
의 타입은 IO ()
이라는 의미이다. IO ()
은 타입이 ()
인 값을 지닌 IO 액션이라는 뜻이다. 유닛 타입은 값이 한 개만 있는데 ()
로 적는다. 유닛 타입의 값은 아무런 정보가 없다. 타입 선언은 옵션이다. 적어도 되고 안 적어도 된다. 컴파일러가 타입을 자동으로 확인한다. 프로그래머가 적은 타입과 컴파일러가 생각한 타입이 다를 때는 에러가 난다. 이 튜토리얼에서는 명확하게 하기 위해 모든 타입 선언을 명시적으로 적는다. 실습을 따라할 때는 굳이 타입을 전부 따라 적지 않아도 된다.
IO 타입은 모나드 클래스의 인스턴스이다. 어떤 값이 모나드 클래스의 타입이라고 한다면 다음과 같은 조건을 만족해야 한다.
- 값에 어떤 추가 정보가 있다.
- 대부분의 함수는 이 추가 정보가 뭔지 몰라도 된다.
예를 들어 위 예제 코드에서 모나드를 설명하면 다음과 같다.
- 추가 정보는 IO 액션이다. 이 IO 액션은 들고 다니는 값을 이용해서 실행된다.
- 추가 정보가 들고 다니는 정보가 비어 있을 때는
()
으로 표시한다.
IO [String]
과 IO ()
은 둘 다 IO
모나드 타입이지만 들고 있는 기본 타입이 다르다. 두 모나드는 각각 다른 타입을 들고 액션을 수행하거나 값을 넘긴다.
“어떤 (숨겨진) 추가 정보”를 들고 있는 값을 “모나딕 값”이라고 한다.
“모나딕 값”을 “액션”이라고 부르기도 한다. IO 모나드를 프로그램 바깥 세계에 영향을 미치는 일련의 액션이라고 생각하면 쉽다. 일련의 액션은 기본 값을 넘긴다. 각 액션은 넘겨 받은 값을 다룬다.
하스켈은 함수형 언어이다. 하스켈은 컴퓨터에게 실행할 일련의 명령을 주지 않고 정의를 준다. 이 정의는 필요한 모든 함수를 어떻게 실행할지 알려준다. 이 정의는 액션과 함수의 다양한 합성을 사용한다. 컴파일러는 모든 것을 조합해서 실행 방법을 결정한다.
정의를 작성하려면 등호를 사용해야 한다. 등호의 왼쪽에 이름을 적는다. 하나 이상의 ‘패턴’(나중에 설명한다.)을 적을 수도 있다. 이 패턴은 변수를 연결할 때 쓴다. 등호 오른쪽에 다른 여러 정의를 적는다. 이 정의는 컴퓨터가 이름을 만났을 때 어떻게 해야할지 알려준다. 등호는 대수학의 등호와 같은 방식으로 동작한다. 등호 오른쪽에 적힌 것은 등호 왼쪽에 적힌 것으로 언제나 대체할 수 있다. 같은 정의는 항상 같은 값으로 평가된다. 이런 성질을 “참조 투명성”이라고 한다. 참조 투명성은 다른 언어보다 하스켈로 생각하는 걸 더 쉽게 해준다.
main
액션은 어떻게 정의할까? main
은 IO ()
액션이어야 한다. 이 액션은 명령줄 인자를 읽고 뭔가를 화면에 출력하고 결과로 아무 것도 아닌 값인 ()
이 나온다.
IO 액션을 만드는 방법은 두 가지이다.(직접 만들거나 액션을 수행하는 함수를 호출한다.)
return
함수를 이용해서 그냥 값을 IO 모나드로 만든다.- 이미 있는 IO 액션 두 개를 합친다.
여기서는 명령줄 입력을 읽고 내용을 화면에 출력하는 두 가지 일을 해야 하기 때문에 이미 있는 IO 액션 두 개를 합치는 방법을 사용한다. 내장 액션 getArgs
는 명령줄 인자를 읽어서 문자열 리스트로 만들어 준다. 내장 함수 putStrLn
은 문자열을 받아서 화면에 출력하는 액션을 만든다.
액션 두 개를 합치려면 do블록을 사용한다. do블록은 여러 줄로 쓸 수 있고 do 다음에 적는 줄은 모두 들여쓰기를 해야 한다. 각 줄은 아래 형태 중 하나로 적는다.
name <- action1
action2
첫 번째 형식은 action1
의 결과를 name
에 연결한다. name
을 다음 액션에서 재사용할 수 있다. 예를 들어 action1
의 타입이 IO [String]
(getArgs
처럼 문자열 리스트를 리턴하는 IO 액션이다.)일 때 name
이 문자열 리스트에 연결되고 name
을 뒤에 나올 액션에서 사용할 수 있게 된다.(“바인드(bind)”라는 연산자 덕분에 가능하다. >>=
로 적는다.)
두 번째 형식은 action2
를 그냥 실행하고 바로(있어야 할) 다음 줄로 연결한다.(이번에도 >>=
연산자 덕분에 가능하다.)
바인드 연산자는 각 모나드마다 의미론이 다르다. IO 모나드에서 바인드 연산자는 액션이 프로그램 바깥에 어떤 부작용을 일으키든 상관 없이 액션을 차례로 실행한다. 모나드마다 의미론이 다르기 때문에 IO 모나드만 쓸 수 있는 do블록 안에서 서로 다른 모나드 타입을 섞어 쓸 수 없다.
액션 안에서도 함수나 복잡한 표현식을 호출해서 결과를 다음 액션으로 전달하기도 한다.(return
함수를 호출해서 그렇게 할 수 있다.)
위 예제에서는 먼저 인자 리스트의 첫 번째 원소를 받는다.(인덱스 0번에 있는 요소이다. args !! 0
이렇게 한다.) 이 원소를 문자열 "Hello, "
끝에 연결한다.("Hello, " ++
) 마지막으로 문자열을 putStrLn
에 넘긴다. putStrLn
은 do블록 안에서 새 IO 액션을 만든다.
이렇게 해서 위에서 설명한 일련의 액션을 연결해서 새 액션을 만들었다. 이 액션은 타입이 IO ()
인 이름 main
에 저장된다. 하스켈 시스템은 이 정의를 인식하고 그 안에서 액션을 실행한다.
하스켈에서 문자열은 문자 리스트이다. 따라서 아무 리스트 함수나 연산자를 문자열에 사용할 수 있다. 표준 연산자와 연산자 우선순위는 다음 표와 같다.
연산자 | 우선순위 | 결합방향 | 설명 |
---|---|---|---|
. |
9 | 오른쪽 | 함수 합성 |
!! |
왼쪽 | 리스트 인덱싱 | |
^ , ^^ , ** |
8 | 오른쪽 | 거듭제곱(정수, 분수, 실수) |
* , / |
7 | 왼쪽 | 곱셈, 나눗셈 |
+ , - |
6 | 왼쪽 | 덧셈, 뺄셈 |
: |
5 | 오른쪽 | cons (리스트 생성)
|
++ | 오른쪽 | 리스트 연결 | |
`elem` , `notElem` |
4 | 왼쪽 | 리스트 원소 여부 확인 |
== , /= , < , <= , >= |
왼쪽 | 같음, 다름, 대소 비교 | |
&& |
3 | 오른쪽 | 논리 and |
|| |
2 | 오른쪽 | 논리 or |
>> , >>= |
1 | 왼쪽 | 리턴 값을 무시하는 모나딕 바인드, 다음 함수로 값을 넘겨주는 모나딕 바인드 |
=<< |
오른쪽 | 역방향 모나딕 바인드(위의 바인드와 같은데 인자 순서가 반대이다.) | |
$ |
0 | 오른쪽 | 중위 함수 적용(f $ x 는 f x 와 같지만 좌결합이 아니라 우결합이다.)
|
프로그램을 컴파일하고 실행하려면 다음과 같이 해보자.
# ghc -o hello_you --make listing2.hs # ./hello_you Jonathan Hello, Jonathan
위와 같이 -o
옵션 다음에 생성할 실행 파일의 이름을 적고 이어서 하스켈 소스 파일 이름을 적는다.
연습 문제
[+/-]- 명렬줄에서 인자 두 개를 받도록 프로그램을 바꿔보자. 입력 받은 두 인자를 모두 출력해보자.
- 인자 두 개를 받아 산술 연산하고 결과를 화면에 출력하도록 프로그램을 바꿔보자. 문자열을 숫자로 바꾸려면
read
를 사용하고 숫자를 다시 문자열로 바꾸려면show
를 사용하면 된다. 여러 연산을 지원해보자. getLine
은 터미널에서 행을 입력 받아 문자열로 리턴하는 액션이다. 명령줄로 이름을 받지 않고 프로그램에서 이름을 입력 받아 출력하도록 바꿔보자.