이번 주제는 대화식 자료입력입니다. PCL의 저자는 add-record 함수가 잘 동작하지만 이런한 입력방식은 너무 lispy하다고 말을 합니다. 고로 보다 사용자에게 친숙하기 위하여 대화형 입력방식을 소개합니다. 대화형 입력방식이란 화면에 prompt로 무엇을 입력하여야 하는지를 보여주고 사용자가 그 내용을 입력하면 그 자료가 저장이 되어야 합니다. 물론 아직까지는 저장된다는 의미가 메모리내에 db에 저장되는 것을 뜻합니다.
그런데 처음에 정의를 하지 않고 지나온 부분이 있습니다. c계열로 프로그램을 하거나 실제 데이타베이스를 구축한다면 이렇게 잊어버리고 지나가는 경우가 없겠습니다만…
이 데이타베이스에서 사용하는 데이타필드는 Title, Artist, Rating, Ripped인데 항목만을 명확히 정의하였지 이들의 속성은 정의하지 않고 두리뭉실 넘어 왔습니다. 확실하게 속성을 정의하자면 Title과 Artist는 문자열고 Rating은 숫자, 그리고 Ripped는 진리값 즉 Boolean으로 하겠습니다.
아무튼, 화면에 prompt를 출력하고 입력을 받는 함수는 common lisp에서는 아래와 같습니다.
(defun prompt-read (prompt)
(format *query-io* “~a;” prompt)
(force-output *query-io*)
(read-line *query-io*))
Clojure 코드도 대동소이합니다.
(defn prompt-read [prompt]
(print (format "%s: " prompt))
(flush)
(read-line))
함수정의 첫번째 줄에는 함수이름 prompt-read와 인자로 [prompt]를 정의하였고, 두번째줄은 화면에 prompt를 표시하여 주는 것임은 당근입니다. 세번째줄, (flush) 는 common lisp의 (force-output *query-io*)와 같이, 현재 *out* 값의 outoutput stream을 “비워”줍니다. 그리고 네번째줄은 입력값을 읽어주는 것은 당연하겠지요.
이 함수를 make-cd의 각각 필드를 위한 입력값으로 사용하면 됩니다만, 문제는 이 함수를 사용하면 당연히 단수 문자열를 반환하여 주기 때문에, 이를 Rating을 위하여는 숫자로, Ripped를 위하여는 boolean값으로 변환하여주는 함수가 필요합니다.
우선 Rating을 위한 입력된 문자열을 숫자로 변화하는 common lisp 최종 코드는 다음과 같습니다.
(or ( parse-integer (prompt-read “Rating”) :junk-allowed t) 0)
Clojure 코드가 이번에는 조금 더 복잡하게 보입니다.
(defn parse-integer [str]
(try (Integer/parseInt str)
(catch NumberFormatException nfe 0)))
첫번째 줄을 설명하면 이글을 읽으시는 분들의 수준을 무시하는 것 같아서 생략합니다. 두번째줄 괄호안부터 설명합니다. common lisp에서와 같이 integer로 변환하는 함수같은데 조금 이상합니다. 대문자가 섞여 쓰인 것이 약간은 java 냄새가 납니다. 그런데 “.”가 아닌 “/”가 중간에 있군요. 결론은 java를 호출하였습니다.clojure에 관심을 가지셨다면 대부분 java를 아실터이니
Integer.parseInt(str)
라고 표시한다면 친숙하시겠지요? 예 자바를 불러 사용한 예입니다. Clojure 장점중에 하나겠지요. 자바에서는 바로 위와 같이 표시합니다만 clojure에서의 정식 표기법은 아래와 같습니다
(. Integer parseInt str)
그런데 이 정식표기법보다는 약식으로 사용하는 것이 “조금” 간편하고, 보다 명확하게 어디까지가 클래스명과 맴버명이고 어떤것이 인자인지 알수 있는 것이 바로 "syntactic sugar”형 (저는 그냥 “간이형식”이라 하겠습니다.) 인 아래와 같습니다.
(Integer/parseInt str)
그런데 str이 정수값으로 변환할 수없는 기타문자나 기호인경우에는 에러가 발생합니다. 이 에러를 처리하는 방법이, 잘 아시는 try – catch 입니다. 그래서 결국은
(try (Integer/parseInt str)
(catch NumberFormatException nfe 0)))
즉 에러가 발생하면, 문자나 기호인 경우에는 0를 반환하라는 것입니다. 자세한 설명은 생략합니다. 끝!
다음은 Ripped를 위하여 (사실은 y/n만을 입력받기 위한 모든 곳을 위하여) 입력을 y, Y, n, N 만을 허용하고 이를 clojure의 boolean값인 true / false로 반화하는 함수를 정의 하겠습니다. common lisp에서는 이기능을 y-or-n-p 함수로 제공합니다. 우리는 이것과 같은 함수를 정의하여 사용하겠습니다.
(defn y-or-n-p [prompt]
(= "y"
(loop []
(or
(re-matches #"[yn]" (.toLowerCase (prompt-read prompt)))
(recur)))))
첫째줄은 y-or-n-p 함수를 정의합니다. 인자는 화면상에 prompt를 띄울 문자열을 받아들이고, 반환값은 Boolean 값입니다. 둘째줄은 (loop []…) 반환값이 “y” 와 동일하면 true 를 아니면 false를 반환하는 조건식이고, 세째줄에는 그 조건식의 두번째 인자값을 줄 (loop []…) 표현식이군요. clojure의 loop는 (recur)를 만나면 되돌아 옵니다만, 그렇지 않으면 종료됩니다. 물론 []안에는 바인딩하려 loop안에서 사용할 인자를 사용할 수 있습니다만, 이곳에서는 바인딩하여 사용할 인자가 없기에 공란입니다. (or…)안의 첫번째 표현식은 중첩된 가장 안쪽의 (prompt-read prompt)는 당연히 위에서 정의한 대화식입력 함수입니다. 즉 입력을 받아 이것을 (.toLowerCase…)로 대문자인 경우 소문자로 변경합니다. (.)는 자바함수를 call 한다는 의미이고 toLowerCase는 자바를 사용하신 분들께 설명할 필요가 없겠지요? 다음은 (re-matcjes …)인데 이것은 정규표현식을 사용하여 주어진, 여기서는 입력된 문자열에 yn이 있는 지를 확인하여 있다면 그것을 반환합니다. 만일 없다면 거짓값이 반환되니 (or …)안에 있으므로 다음 표현식이 평가되는데, 다음 표현식이 (recur)이니, loop가 되풀이 됨을 뜻합니다. 즉 y,Y,n 또는 N이 입력되지 않았다면 다시 입력을 기다리는 무한반복이 됩니다.
그러면 이제 준비가 다 되었습니다. 대화형으로 cd 정보를 입력하는 함수를 만들어 봅시다.
(defn prompt-for-cd []
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(parse-integer (prompt-read "Rating"))
(y-or-n-p "Ripped [y/n]")))
설명할 곳이 없군요. 이 함수를 add-record 함수에 사용하여 메모리 db에 입력하고, 사용자가 반복하여 여러 cd정보를 입력하게 할 수 있는 loop- 되풀이 기능을 넣어 add-cds 함수를 아래와 같이 정의합니다.
(defn add-cds []
(loop []
(add-record (prompt-for-cd))
(if (y-or-n-p "Another? [y/n]") (recur) )))
(loop [] ….)표현식을 이해하였다면 별 설명이 필요없습니다.
그러면 한번 실행을 하여 볼까요?
1:3 user=> (add-cds)
Title: Rockin'the Suburbs
Artist: Ben Folds
Rating: 6
Ripped [y/n]: y
Another? [y/n]: y
Title: Give Us a Break
Artist: Limpopo
Rating: 10
Ripped [y/n]: y
Another? [y/n]: y
Title: Lyle Lovett
Artist: Lyle Lovett
Rating: 9
Ripped [y/n]: y
Another? [y/n]: n
nil
1:19 user=> (dump-db)
title: Lyle Lovett
artist: Lyle Lovett
rating: 9
ripped: true
title: Home
artist: Dixie Chicks
rating: 9
ripped: true
title: Roses
artist: kathy Mattea
rating: 7
ripped: true
title: Rockin'the Suburbs
artist: Ben Folds
rating: 6
ripped: true
title: Fly
artist: Dixie Chicks
rating: 8
ripped: true
title: Give Us a Break
artist: Limpopo
rating: 10
ripped: true
nil
1:21 user=>
하하하…잘 동작합니다. 화면의 프롬프트가 1:3에서 3장의 cd정보를 입력후에는 갑자기 1:19로 뛰는 군요. 아마 프롬프트입력의 각라인이 계산되는 모양입니다. 아무튼 이렇게 힘들게 입력한 정보를 “정말” 잘 보관하기 위하여 화일에 저장하는 것은 다음 주제로 하겠습니다.
계속 진행될 실용 간단한 데이타베이스를 위하여 계속 필요한 코드는 아래와 같습니다.
[[code format="lisp"]]
(defstruct cd-field :title :artist :rating :ripped)
(defn make-cd [title artist rating ripped]
(struct cd-field title artist rating ripped))
(def db (ref #{}))
(defn add-record [cd]
(dosync (alter db conj cd )))
(defn dump-db []
(doseq [cd @db]
(doseq [[key value] cd ]
(print (format "%10s: %s \n" (name key) value)))
(println)))
(defn prompt-read [prompt]
(print (format "%s: " prompt))
(flush)
(read-line))
(defn parse-integer [str]
(try (Integer/parseInt str)
(catch NumberFormatException nfe 0)))
(defn y-or-n-p [prompt]
(= "y"
(loop []
(or
(re-matches #"[yn]" (.toLowerCase (prompt-read prompt)))
(recur)))))
(defn prompt-for-cd []
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(parse-integer (prompt-read "Rating"))
(y-or-n-p "Ripped [y/n]")))
(defn add-cds []
(loop []
(add-record (prompt-for-cd))
(if (y-or-n-p "Another? [y/n]") (recur) )))
[[code]]
댓글을 달아 주세요