nML Manual for homework-_-; v0.99999999999999......

cholonge

2004.12.5. 밤중

0. 서론(잡설)

내가 무슨 nML고수도 아니고, 숙제도 몇 개 엉터리로 낸 몸이건만, 숙제를 처음 시작했을 때 nML문법을 몰라서 혼자 짜증내며 삽질했던 기억이 난다. 조교님의 ppt와 교수님의 링크primer가 훌륭하기는 하지만, 예제 중심이 아니라서 적용을 하는 데 엄청 삽질을 했던 기억은 아직도 선명하다. 그리하여, 전혀 정리되지 않은, 머릿속에서 떠오르는 대로 nML에 관련된 몇 가지 문법 사항과 팁, 디버깅 요령들을 간단히 예제 중심으로(이거 모순이다) 정리해 보고자 한다. 이 글의 정리는 다음에 컴프나 PL을 듣게 될 03 이하 후배들에게 맡기도록 해야겠다.

시작하기 전에, 이 글은 이광근 교수님이 내주시는 숙제를 고통 없이(할 수는 없고, 적은 고통으로) 하게 하기 위한 매뉴얼이라는 점을 밝혀 둔다-_-;

1. 시작.

먼저, nML바로 실행 컴파일러를..... 켜지 말자-_-; 몇 가지 명령을 입력하면 바로 답이 나오지만, 우리는 이걸로 프로그래밍을 할 게 아니다. Putty를 두개 켜고, 마티니에 접속하자.

nml은 한글 변수와 펑션을 지원하는데, 이걸 제대로 보려면 설정을 해 줘야 한다.

(왼쪽 위 구성 아이콘 클릭, change settings, 어피어런스, 폰트세팅 체인지, 픽스드시스, 확인, 어플라이) ;;;;;

거기에 nmlc와 nml을 입력하면 각각 컴파일과 바로 실행 컴파일러를 쓸 수 있다.

vim 1.n 이라고 해서 vim에 들어간 뒤에, I누르고[이런건 안 써도[...]]

간단한 프로그램을 입력하자, 뭔지는 처음에 다 그렇듯이 묻지 말고....

fun merge([],[]) = []

| merge([],h::t) = h::t

| merge(h::t,[]) = h::t

| merge(h1::t1,h2::t2) = if h1<h2 then

h1::merge(t1,h2::t2)

else

h2::merge(h1::t1,t2)

// 이렇게 하면 주석이다.

(* 이렇게 해도 주석이다!

여러줄 주석 말이지~~!!!

*)

여기까지.

저장하고, 다른 쁘띠로 가서, nmlc 1.n 이라고 쳐주자(컴파일인데, 사실 생략 가능하다 왜냐면 아래 단계에서 로드할 때 다 쓸 것이므로) 타이핑을 잘못하지 않았다면-_- 조용히 끝날 것이다. nml쳐서 #이 뜨면,

#  #use "1.n";;

이라고 쳐주자

val merge : 'a list * 'a list -> 'a list = <fun>

이라고 나올텐데, 뭔지는 넘어가고 일단 테스트를 하자

#  merge([1,3],[2,100]);;

val it: int list = [1,2,3,100]

자, 이로서 헬로 월드의 전통을 깨는[....] 첫 시도가 무사히 끝났다!

# #quit ;;

나오자!

2. 리스트 다루기!

방금 쳤던 프로그램은, 달랑 함수 한개다. nML은 정의의 모임; 이기 때문에, 함수를 잘 정의하는 것이 중요...하다기보다는 전부다. 이제 방금 쳤던 프로그램에 대해 이것저것 살펴보자.

fun merge([],[]) = []

| merge([],h::t) = h::t

| merge(h::t,[]) = h::t

| merge(h1::t1,h2::t2) = if h1<h2 then

h1::merge(t1,h2::t2)

else

h2::merge(h1::t1,t2)

fun 은 함수를 정의하는 키워드다. merge는 함수 이름이고. 그런데 보다시피 merge가 4개 있다. 이것은 함수에 들어오는 인자를 패턴 매칭하는 것이다. 그래서 같은 이름이 4개 있는 것이고, 하나의 패턴별로 ~~ 패턴 = 이거 리턴, ~~~~한 패턴 = 이거 리턴. 이런 식으로 나가는 것이다.

| <---- 요 키워드는 그러한 패턴과 패턴을 구분하는 바이다. or정도로 생각하면 되겠고, 수업시간에 이미 봤으리라 믿는다(최소한 공학수학시간에....)

그럼 첫줄.

fun merge([],[]) = []

컴파일러는 여기까지 읽고 생각한다.

‘merge는 뭔가의 리스트와 뭔가의 리스트의 페어를 인자로 받는다.’ 라고.

이 줄의 의미는. 이 함수에 빈 리스트와 빈 리스트의 페어가 들어오면 (정식명칭은 튜플인가....) 빈 리스트를 리턴하라. 라는 것이다. 함수에 써 놓은 값은 그대로 리턴값이 된다.

둘째줄

| merge([],h::t) = h::t

첫 번째 인자가 빈 리스트이고, 두 번째 인자가 빈 리스트는 아닌 리스트일때. 그 비지 않은 리스트를 h와 t로 나누어서 생각하겠다. 라는 뜻이다. 뭔말이냐면-_-....

리스트는.

(a타입의 값) :: a타입 리스트 나머지 부분

이 반복되어 있는 구조이기 때문에, int list라고 할때 head::tail 하면 head는 int type이고 tail은 int list type 이라는 것이다. 스킴의 car와 cdr를 생각하면 되겠다(안 배웠으면... 쩝..)

원소가 하나도 없는 리스트는 []에 매칭되고, 하나 들어있는 리스트는 h::t로 매칭 가능하다.(h가 그 원소, t가 nil list : [] 가 된다)

즉, merge([],h::t) = h::t 는 왼쪽이 비고 오른쪽에 뭔가 있으면, 그 오른쪽 리스트를 그대로 리턴하겠다는 뜻.

셋째줄  | merge(h::t,[]) = h::t  : 둘째줄과 거의 같다.

나머지

| merge(h1::t1,h2::t2) = if h1<h2 then

h1::merge(t1,h2::t2)

else

h2::merge(h1::t1,t2)

if문이 등장한다. 이거 없으면 어떻게 프로그램 짜지-_-;;;

구문은 보다시피 if 조건식 then 프로그램식 else 프로그램식 이다. 특기할 만한 사항 몇 가지는

1. else없으면 안된다-_-!

2. ==이 아니고 =이다

3. and와 or의 연산자는 andalso와 orelse이다.

이정도라 할 수 있겠다.

의미를 보면

이 케이스는 양쪽 모두 뭔가 원소가 있는 리스트일때

만약 그 맨앞 원소 두개를 비교하여 h1이 더 작다면

h1을 앞에다 놓고, 뒤에 merge(t1,h2::t2)라는 인자로 불려져 리턴된 리스트를 붙여서 그 리스트를 리턴해라. 아니면,  h2을 앞에다 놓고, 뒤에 merge(h1::t1,t2)라는 인자로 불려져 리턴된 리스트를 붙여서 그 리스트를 리턴해라.

라는 것.  ....알고 있었다구-.-?

쳇-_-++

리커시브 콜이다. 4번째 경우를 계속 돌다 보면 결국은 두 번째나 세 번째 경우로 들어가면서 프로그램이 종료되겠다. 그럼 첫 번째 경우는 왜 있느냐 하면, 사용자가 ([]

또 한 가지 특기해 두면, 리커시브와 함께 살게 된다. for와 while같은게 있긴 한데, 그냥 없다고 생각하는게 nML인생에 도움이 된다.

저 프로그램이 merge를 올바로 수행한다는 것을. 프로그램을 천천히(특히 4번째 경우를) 보면서 잘 따져두도록 하자. 앞으로 이런 간단한[...]프로그램 보기도 쉽지 않을터이니....

3. let-in-end, sequence

매우 강력한 키워드 let-in-end를 여기에 소개한다.

let val (mem1, v1) = eval (mem, env, e1)

val (mem2, v2) = eval (mem1, env, e2)

in  case v1

of Num n1 => (case v2

of Num n2 => (mem2, Num (n1 + n2))

| _ => raise KIN "숫자 아닌걸 더하고 싶냐? 응?")

| _ => raise KIN "숫자 아닌걸 더하고 싶냐? 응? 응?"

end

이름에서도 알 수 있듯이 let 뒤에 써있는걸 먼저 실행하고, in 뒤에 있는걸 수행한다. end로 닫아줘야. in 과 end사이에 있는 프로그램식의 리턴값 타입을 맞춰줘야 한다.

저 프로그램에서도 할 말이 많~다.

let val (mem1, v1) = eval (mem, env, e1)

val (mem2, v2) = eval (mem1, env, e2)

val 은 밸류의 약자로 값을 정의하는 명령이다. eval함수를 mem,env, e1이라는 인자로 실행하여, 그 결과(여기서 우리는 그 결과가 뭔가의 튜플 형태로 나오게 됨을 짐작할 수 있다)를 mem1과 v1에 저장한다. 그리고 다시 mem1,env,e2라는 인자로 eval을 돌려 그 값을 mem2와 v2에 저장하게 되는 것이다.

in  case v1

of Num n1 => (case v2

of Num n2 => (mem2, Num (n1 + n2))

| _ => raise KIN "숫자 아닌걸 더하고 싶냐? 응?")

| _ => raise KIN "숫자 아닌걸 더하고 싶냐? 응? 응?"

case 문이 등장하였다. case 변수명 of TYPE => ......

| TYPE => ......

이게 구문인데, 중첩해서 쓰면 컴파일러가 헷갈려하므로 case앞뒤에 괄호를 강추한다.

......거기 당신,  (case) 변수명... 이라고 생각하고 있진 않지?

(  case 변수명 of TYPE => ......

| TYPE => ....... )

이런 뜻이다. (앗, 돌이다아~)

해석을 해보면

v1의 타입이 Num n1이면. 즉 v1이 Num타입이면..........

아, 타입 이야기가 빠졌네 oTL

이...일단 넘어가자-.-

어쨌든 Num타입이면, 다시 v2의 타입을 봐서 그것도 Num 타입이면, 그 값 n1과 n2를 더해서 Num타입을 씌워서 리턴 한다는 것이다.

그리고 _ => 라는 구문이 있는데, 이것은 그 외의 경우, 즉 Num타입이 아닌 타입의 경우에 그쪽에 매칭되었다고 생각, 실행하게 된다. 실행문이

raise KIN "숫자 아닌걸 더하고 싶냐? 응?"

인데, raise는 예외를 발생시키는 문장으로 그 전에 이미

exception KIN of string

이라고 선언되어 있어야 한다.

렛인앤드 설명할려다가 딴걸 잔뜩 설명해 버렸다-_-;;;;

그리고, nML도 화면 출력 라이브러리가 있다. print_int 23 하면 23 찍히고, print_string "Rock!" 하면 Rock!찍힌다 print_endline " Mania!" 하면 찍히고 개행문자 찍힌다.

그런데!

이런걸 여러개 찍고 적당한 값을 리턴하려면?

그러니까 “Rock~!" 을 찍고 r값을 리턴해야 하는 경우.

print_endline "Rock~!" ; r

;는 시퀀스로, 앞에껄 하고 그 다음껄 한다.

print문과의 콤보 외에는 사용을 그다지 추천하지 않는다.

4. 타입

이거 1번이나 2번쯤 갔어야 할지도 모르겠다-_-

일부러 살짝 꼬인[?] 타입을 가져왔다. 사실 뭐 꼬인것도 아니다.

type tree = LEAF of string

| NODE of tree list

type zipper = TOP

| HAND of tree list * zipper * tree list

type location = LOC of tree * zipper

타입은 type 키워드로 선언하고 타입이름 = 프로그램 내부에서 사용할 타입이름 ( of 구성물)

이라는 형태를 가진다. 주의할 점은 대소문자인데, 타입이름과 구성물은 소문자로 시작, 프로그램 내부에서 사용할 타입이름은 대문자이다.

type tree = LEAF of string

| NODE of tree list

type zipper = TOP

| HAND of tree list * zipper * tree list

tree타입은 LEAF나 NODE인데 그 구성물은 LEAF일 경우에는 string, NODE일 경우에는 tree(자기자신) list다.

zipper는 TOP(구성물없음)이거나 HAND(트리리스트, 지퍼, 트리리스트)

......아까  프로그램 일부

in  case v1

of Num n1 => (case v2

라는 것은 어디선가 v1으로 가능한 타입 중에

type value = .....

| Num of int

| .....

라고 선언된 부분이 있었다고 생각할 수 있다.

tree타입의 예를 들면

LEAF("초롱이“) 와 같은 형태를 가질 수 있고

NODE( [ LEAF("Alchemist") , LEAF("Black Death"), LEAF("Civil War") ]

와 같은 형태도 가질 수 있다. 심지어는

NODE( [

NODE( [ LEAF "EN", LEAF "N", LEAF "W", LEAF "R", LEAF "RN"] ) ,

NODE( [] ) ,

LEAF( "IJKL....M")

] )

처럼 다양한 형태를 가질 수 있다.

5. 디버깅 팁들

1) 어이없는 구문 에러들

이런걸로 시간보내면 인생이 허무해진다. type과 type형태이름, exception이름, 변수이름의 시작 대소문자를 잘 지키고(순서대로 소문자,대문자,대문자,소문자...) 어이없는 곳에 ;를 찍지는 않았는지[....-_-] 괄호가 꽤 많으므로(스킴보다야 적지만) 어느 괄호가 어디 들어갔는지 잘 살피고, q가 어딘가 있지 않은지 살피고(.....이해안되는 사람은 무시-_-), | 빼먹지 않았나 보고 등등.

2) 타입 에러!

대부분의 컴파일 에러는 이 타입 에러와 함께 춤추게 된다. 이게 컴파일러가 나름대로 열심히 타입 추론을 하다가 모순이 일어나는 데서 에러를 내기 때문에, 꽤나 찾기도 힘들다. 꼭 거기서 에러가 났다는 보장도 없고, 그것을 call한 함수라든가. 지금 막 넘겨주려는 함수라든가 등등 여러 경우가 있는데, 이러면 좀 도움이 된다.

1. 함수를 짤 때 주석으로 타입을 명시하자!

ex) // eval (메모리, 환경, 식) -> (값, 메모리)

2. 컴파일러가 자신이 생각한 타입이랑 다른 거여야 한다고 우기면, 컴파일러한테 확실히 타입을 알려주자. 그 방법은 함수식에 : 타입명 이라고 해주면 된다. 즉

ex)  fun getsame(exp,[]:(map*map) list) = .....

이런 식으로 불확실한 것을 확실하게 이것은 무엇이다! 라고 해주면 컴파일러가 틀린 구문이 그 함수에 있는지, caller에 있는지 callee에 있는지..를 더 정확히 집어주게 된다.

3. 뭐뭐 list타입이어야 하는데 뭐뭐 list list타입입니다

혹시 h::t를 [h::t]로 안 썼는지 보자-_-+

3) 컴파일러와의 전투 (괄호!)

컴파일러, 엄청 똑똑하고 가끔 바보다-.-;;;;;. 아까도 이야기했지만 case문을 써 주는 앞뒤에는 괄호를 강력히 추천한다. 특히 'a -> 'b -> 'c -> 'd 와 같은 함수의 경우에는 필요하지 않다고 생각하는 모든 경우에도 괄호를 쳐 보는 것이 도움이 될 것이다.

4)  실행 에러

컴파일이 되었는데 뭔가 안 돌아간다면, 이것을 추적하려고 하지 말고 논리적으로 상위 레벨에서 자기가 짠 코드를 찬찬히 살펴보자. 그게 훨씬 도움이 된다. 특히 맞다고 생각된 부분도 다시 한번 보자. nML코드 몇줄이나 되는가-_-;;; 또한 컴파일러는 에러를 하나만 알려주기 때문에 중간중간 컴파일 해보는 것을 강추한다.--;

6. 맺음말

뭔가 야심차게 시작했는데 도움이 되었는지, 모르겠-_-다. 마지막으로, 지금 시험공부 안하고 감기 대충 나았다고 책 하나도 없어서 공부도 하기싫고. 해서 쓰는게 아니라, 후배들에 대한 무한한 사랑의 힘으로[......................................]

....미안하다.

여하튼 열심히 해서 좋은 결과 있고

나처럼 숙제 째지 말길-_-;;;

그럼 안녕!