LISP으로 함수형 프로그래밍 맛보기

4.6 ~ 4.8

 

4.6 입출력을 어떻게할까

 

{{{

(defun readlist (&rest args)

  (values (read-from-string

       (concatenate 'string "("

            (apply #'read-line args)

            ")"))))

 

(defun prompt (&rest args)

  (apply #'format *query-io* args)

  (read *query-io*))

 

(defun break-loop (fn quit &rest args)

  (format *query-io* "Entering break-loop.~%")

  (loop

   (let ((in (apply #'prompt args)))

     (if (funcall quit in)

     (return)

       (format *query-io* "~A~%" (funcall fn in))))))

}}}

Figure 4.7: I/O functions. 입출력 함수들

 

그림 4.7은 3개의 입출력용 유틸리티를 보여줍니다. 입출력 유틸리티가 필요한 이유는 프로그램마다 다릅니다. 그림 4.7에 나오는 것들은 대표적인 예제들일 뿐입니다. 첫번째 유틸리티는 사용자가 괄호를 쓰지않고 뭔가를 입력할 수 있도록 만들 때 씁니다. 사용자의 입력을 한 줄 읽어서 리스트로 만들어서 반환합니다.

 

{{{

> (readlist)

Call me "Ed"

(CALL ME "Ed")

}}}

 

values를 호출하면 항상 첫번째 값 하나만 받게됩니다. (이런 경우와 달리 read-from-string은 두번째 값을 반환합니다.)

prompt함수는 사용자에게 질문을 출력하는 것과 동시에 사용자의 응답도 읽을 수 있습니다. 출력할 질문의 포맷을 인자로 받는데, 출력 방향은 지정할 수 없습니다.(역자주: 출력방향을 stream을 번역한 것인데 *standard-input*이나 *standard-output*같은 출력 스트림 지정을 의미합니다.)

 

{{{

> (prompt "Enter a number between ~A and ~A.~%>> " 1 10)

Enter a number between 1 and 10.

>> 3

3

}}}

 

마지막으로 설명할 break-loop는 리습의 REPL(역자주: 이 책에서는 toplevel이라고도 부르는데 Read-Evaluate-Print Loop의 약자입니다. 보통 리습 구현체를 설치하고 실행하면 나타나는 사용자 입력 환경을 말합니다.)을 흉내내고 싶을 때 사용합니다. 두개의 함수와 &rest 인자를 받습니다. rest 인자는 프롬프트로 출력할 문자열입니다. 두번째 함수가 사용자 입력을 받아서 거짓을 반환하면 첫번째 함수가 사용자 입력을 받아서 실행합니다. 다음처럼 실제 리습의 REPL을 흉내낼 수 있습니다.

 

{{{

> (break-loop #’eval #’(lambda (x) (eq x :q)) ">> ")

Entering break-loop.

>> (+ 2 3)

5

>> :q

:Q

}}}

 

이게바로 커먼 리습 개발회사들이 주로 런타임 라이센스를 고집하는 이유입니다. 런타임에 eval을 호출하는 프로그램만 만들면 리습을 그대로 실행할 수 있으니까요.

 

4.7 심볼과 문자열은 어떤 차이가 있을까

 

 

심볼과 문자열은 매우 밀접한 관계가 있습니다. 읽고 출력하는 함수들을 쓰면 이 두가지 표현을 오갈 수 있습니다. 그림 4.8에 이렇게 두가지 표현을 오갈 수 있는 유틸리티의 예제들이 있습니다. 첫번째 mkstr은 임의의 갯수만큼 인자를 받아서 각각의 출력값을 하나의 문자열로 만들어줍니다.

 

{{{

> (mkstr pi " pieces of " ’pi)

"3.141592653589793 pieces of PI"

}}}

 

mkstr로 만든게 symb입니다. 심볼을 만들 때마다 거의 항상 사용하게 될 함수입니다. 하나 이상의 인자를 받아서 각각의 출력값을 하나의 심볼(필요에 따라 새로 생성된)로 만들어서 반환합니다. 심볼, 문자열, 숫자나 리스트 등등 출력될 수 있는 모든 객체를 인자로 받을 수 있습니다. (역자주: |ARMadiLL0| 에 있는 | 표시는 심볼이라는 표시입니다.)

 

{{{

> (symb ’ar "Madi" #\L #\L 0)

|ARMadiLL0|

}}}

 

 

{{{

(defun mkstr (&rest args)

  (with-output-to-string (s)

             (dolist (a args) (princ a s))))

 

(defun symb (&rest args)

  (values (intern (apply #'mkstr args))))

 

(defun reread (&rest args)

  (values (read-from-string (apply #'mkstr args))))

 

(defun explode (sym)

  (map 'list #'(lambda (c)

         (intern (make-string 1

                      :initial-element c)))

       (symbol-name sym)))

}}}

Figure 4.8: Functions which operate on symbols and strings. 심볼과 문자열을 처리하는 함수들

 

symb는 mkstr을 이용해서 모든 인자들을 하나의 문자열로 연결하고, intern에 넘깁니다. intern 함수는 리습에서 심볼을 만들 때 전통적으로 많이 쓰는 함수입니다. 문자열을 받아서 심볼을 출력합니다. 이때 이전에 이미 같은 심볼이 사용되었는지를 조사해서 이전의 심볼을 반환하거나, 그렇지 않을 때는 새로운 심볼을 만들어서 반환합니다. (역자주: 리습의 심볼은 객체입니다. 따라서 각 심볼마다 메모리를 차지합니다. 출력하면 같은 문자열이 되는 사실상 같은 심볼을 굳이 따로 만들 필요는 없겠지요.)

심볼은 어떤 형태의 문자열로든지 출력될 수 있습니다. 소문자나 괄호같은 특수 문자도 심볼에 들어갈 수 있습니다. 심볼의 이름에 그런 특수 문자가 들어가면 위에서 보듯이 세로막대(역자주: |의 정식 한글 명칭이 없는것 같습니다.)로 표시됩니다. 소스 코드에서도 그런식의 이름을 가지는 심볼들은 세로막대로 표시되야합니다. 아니면 역슬레시로 표시할 수도 있습니다.

 

{{{

> (let ((s (symb ’(a b))))

(and (eq s ’|(A B)|) (eq s ’\(A\ B\))))

T

}}}

 

 

다음 함수는 symb를 좀더 범용적으로만든 reread함수입니다. 여러개의 객체를 인자로 받아서 값을 출력하고 출력된 값을 다시 읽습니다. symb와 마찬가지로 심볼을 반환하지만, read 함수의 인자로 넘길 수 있는 것들도 반환할 수 있습니다. reread에 a:b를 전달하면 인자를 바로 심볼로 처리하지않고 read로 먼저 처리합니다. 즉 |a:b|를 전달하면 현재 페키지에 있는 심볼|a:b|의 이름이라고 생각하지않고 페키지 a에 있는 심볼 b라고 읽습니다. (저자주1) 더 범용적인 함수로는 pickier가 있습니다. reread는 인자가 리습의 문법에 맞지 않으면 에러를 반환합니다.

(저자주1: 페키지에 대한 소개는 원서 381페이지에서 시작하는 부록을 참고하시기 바랍니다.)

그림 4.8에서 마지막 함수는 explode입니다. 몇몇 예전 구현체들은 자체적으로 제공하고있는 함수입니다. explode함수는 심볼을 받고 그 이름을 구성하는 각 문자들을 심볼의 리스트로 반환하는 함수입니다.

 

 

{{{

> (explode ’bomb)

(B O M B)

}}}

 

이 함수가 커먼 리습에 포함되지 못한 이유가 있습니다. 만약에 이 함수에서처럼 심볼을 따로따로 분리하고 싶어진다면 뭔가 비효율적인 것을 만들고있을 가능성이 큰겁니다. 하지만 제품에 들어가기전 프로토타입을 만들 때 등 필요할 때가 있기도 합니다.

 

 

4.8 프로그램의 밀집도를 생각해봅시다

 

코드에 유틸리티를 많이 쓰면 그 코드를 읽는 사람 중에 이해하기 어렵다고 불평할 사람도 생길겁니다. 아직 리습에 익숙하지 못한 사람들은 기본 리습 코드를 읽는데만 적응이 되어있을겁니다. 사실 그런 사람들은 아직 언어자체를 확장한다는 개념에 대해 익숙하지 않을 것입니다. 그런 사람들은 유틸리티를 매우 많이 쓰는 프로그램을 보게되면 프로그램 개발자가 마치 자신만의 언어로 프로그램을 만들려한것처럼 생각할 수도 있습니다.

이렇게 새로 만들어진 연산자들은 어쨌든 프로그램을 읽기 어렵게 만듭니다. 프로그램을 읽고 이해하기 이전에 그것들부터 이해해야하니까요. 꼭 그렇지만은 안다는걸 알려면 페이지 41에 설명된 경우를 생각해봐야합니다. 가장 가까운 책방을 찾으려고 합니다. find2를 써서 프로그램을 만들면 프로그램을 읽기전에 새로운 유틸리티부터 이해해얀다고 불평하는 사람이 있을 수 있습니다. 그럼 find2를 안썼다고 생각해봅시다. 그러면 find2를 이해하지 않아도 되지만 대신에 find-books 코드를 이해해야됩니다. find-books 코드는 find2와 책방을 찾는 코드가 합쳐진 코드입니다. find-books이 find2보다 이해하기 어려울수밖에 없습니다. 그리고 우리는 하나의 유틸리를 한번만 사용했습니다. 유틸리티라는 것은 원래 반복해서 사용되도록 만들어진 것입니다. 실제 프로그램이라면 find2를 이해할 것이냐 아니면 세네가지의 개별 탐색 코드를 이해해야할 것이냐를 고르는 상황이 될 것입니다. 당연히 find2를 이해하는게 더 쉽겠지요.

맞습니다. 상향식 프로그램을 읽기 위해서는 저자가 정의한 새로운 연산자들을 다 이해하는게 필요합니다. 하지만 그런 연산자들없이 만들어진 코드를 모두 이해하는 것에 비하면 훨씬 일의 양이 줄어듭니다. 어떤 사람들이 어려분이 만든 유틸리티때문에 코드 읽기가 어려워진다고 불평한다면 어려분이 그 유틸리티를 안썼을 때 코드가 어떡게 될건지를 모르고 하는 소리일 것입니다. 상향식 프로그램은 실제로는 거대한 프로그램을 작고 간단하게 보이도록 만듭니다. 그래서 프로그램이 하는 일이 많지 않아보이게 할 수 있고, 읽기 쉬어 보일것처럼 만듭니다. 비숙련 개발자가 프로그램을 잘 파악해서 사실은 엄청 복잡하고 하는 일이 많은 프로그램이라는걸 안다면 경악할 것입니다.

우리는 다른 분야에서도 같은 현상이 있다는 것을 알아냈습니다. 잘 설계된 기계는 더 적은 부품으로 만들어지지만 더 복잡해보일 수 있습니다.  크기가 더 작아지니까요. 상향식 프로그램도 개념적으로 고밀도라고 볼 수 있습니다. 읽기 쉽지 않지만 상향식 프로그램이 아닌 방식으로 만들어졌을 때에 비하면 쉬워집니다. 

유틸리티를 가급적이면 사용하지 않아야될 경우가 하나 있습니다. 다른 코드들과 상관없이 동작하는 작은 프로그램을 만들 경우입니다. 유틸리티는 보통 두세번 이상 사용되어야 값어치를 하게됩니다. 작은 프로그램이라면 그렇게 값어치를 할만큼 많이 호출될 수 없겠지요.

댓글

댓글 본문
작성자
비밀번호
버전 관리
gurugio
현재 버전
선택 버전
graphittie 자세히 보기