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

ch06. 함수로 객체를 표현하기

자료구조는 보통 뭔가를 표현하는데 사용됩니다. 배열을 기하학적인 처리를 표현하는데 사용하고, 트리가 어떤 명령의 계층 구조를 표현하는데 사용되고, 그래프를 기차 노선도를 표현하는데 사용되는 것처럼 말이지요. 리습을 이용하면 클로저로도 뭔가를 표현하는데 사용할 수 있습니다. 클로저 내부를 생각해보면 바인딩(역자주: binding을 변수라고 번역할까도 했지만, 변수라고만 생각하기에는 무리가 있어서 바인딩이라고 직역합니다.)들은 정보를 저장하거나 복잡한 자료구조에 대한 포인터로 사용될 수 있습니다. 공통된 바인딩등을 공유하는 클로저들을 만들거나, 서로 참조하는 클로저를 만들면 데이터 구조와 프로그램의 역할을 모두 할 수 있는 복합적인 객체를 만들 수도 있습니다.

 

내부를 들여다보면 바인딩을 공유한다는 것은 포인터를 사용한다는 것입니다. 클로저는 포인터를 추상화해서 상위 레벨에서 쉽게 사용할 수 있도록 만들어주는 것뿐입니다. 지금까지 정적 데이터 구조로 표현하던 것들을 클로저로 표현하게되면 많은 경우 코드도 보기 좋아지고 효율성도 높일 수 있습니다.

 

 

6.1 Networks 네트워크

 

클로저는 3가지 장점이 있습니다. 능동적이고, 내부 상태를 가지고, 여러개의 인스턴스를 만들 수 있습니다. 능동적이고 내부 상태를 가진 객체의 복사본 여러개를 어디에 쓸 수 있을까요? 다른 것들도 있지만 네트워크가 그런걸 사용할 수 있는 어플리케이션입니다. 네트워크에서 노드를 표현할 때 주로 클로저를 씁니다. 클로저는 내부 상태를 가질 수 도 있는것 뿐 아니라 다른 클로저를 참조할 수도 있습니다. 그래서 네트워크에서 노드 표현에 사용된 클로저는 자기가 송신해야할 다른 노드(또 다른 클로저들)에 대해서도 알 수 있게 됩니다. 이게 무슨 말이냐면 결국 네트워크를 있는 그대로 코드로 만들수 있다는 의미입니다.

> (run-node ’people)
Is the person a man?
>> yes
Is he living?
>> no
Was he American?
>> yes
Is he on a coin?
>> yes
Is the coin a penny?
>> yes
LINCOLN

Figure 6.1: Session of twenty questions. 스무고개 예제

 

이번 절과 다음 절까지 네트워크를 순회하는 두가지 방법을 보겠습니다. 첫번째는 전통적인 방법대로 구조체로 노드를 만들고, 네트워크를 순회하는 코드를 따로 만드는 것입니다. 다음 절에서는 같은 프로그램을 단일 추상화 계층으로 구현하는 방법을 보여드리겠습니다.

예제로 최대한 간단한 어플리케이션을 사용하겠습니다. 스무고개가 한 예입니다. 바이너리 트리를 예제로 사용할텐데 각 단말노드가 아닌 노드들은 예/아니오 질문을 포함합니다. 대답에 따라 왼쪽 서브트리로 갈건지 오른쪽 서브트리로 갈건지가 결정됩니다. 단말 노드는 반환값을 가집니다. 단말노드가지 순회하게되면 단말노드의 값이 순회의 결과로 반환됩니다. 그림 6.1이 프로그램의 실행예제입니다.

 

전통적인 방법대로라면 노드를 표현하는 데이터 구조체를 만드는 것부터 시작해야할 것입니다. 하나의 노드에는 몇가지 정보가 들어가게됩니다. 자기 자신이 단말 노드인지, 만약 그렇다면 반환해야할 값은 뭔지, 아니라면 어떤 질문을 해야하는지, 대답에 따라 어디로 넘어가야하는지 등입니다. 그림 6.2에 우리에게 필요한 데이터 구조의 적당한 예가 있습니다. 크기를 최소한으로 줄일 수 있도록 만들어졌습니다. contents 필드는 질문을 저장하거나 반환값을 저장할 수 있습니다. 단말노드가 아닐때는 yes 필드가 no 필드에 대답에 따라 어디로 가야할지를 저장하게 됩니다. 단말노드일때는 이 필드들이 모두 비어있으므로 단말노드이것을 알 수 있습니다. 전역변수 *nodes*는 해시테이블로서 이름을 키값으로 하는 노드들이 저장됩니다. 마지막으로 defnode는 새로운 노드(단말일 수도 있고 아닐 수도 있는)를 만들고 *nodes*에 저장합니다. 이제 이것들을 가지고 우리의 첫번째 노드를 만들어보겠습니다.

 

(defnode 'people "Is the person a man?"
  'male 'female)

 

 

(defstruct node contents yes no)

(defvar *nodes* (make-hash-table))

(defun defnode (name conts &optional yes no)
  (setf (gethash name *nodes*)
    (make-node :contents conts
		   :yes yes
		   :no no)))

Figure 6.2: Representation and definition of nodes. 노드의 표현과 정의의

 

(defnode ’people "Is the person a man?" ’male ’female)
(defnode ’male "Is he living?" ’liveman ’deadman)
(defnode ’deadman "Was he American?" ’us ’them)
(defnode ’us "Is he on a coin?" ’coin ’cidence)
(defnode ’coin "Is the coin a penny?" ’penny ’coins)
(defnode ’penny ’lincoln)

Figure 6.3: Sample network. 예제 네트워크

 

 

그림 6.3은 그림 6.1에 나온 스무고개를 만들 수 있는 네트워크 예를 보여줍니다.

이제 해야할 일은 이 네트워크의 순회를 시작해서, 질문을 출력하고, 다음 네트워크로 넘어가는 함수를 만드는 것입니다. 그림 6.4에 있는 run-node함수가 그 일을 하는 함수입니다. 이름을 전달하면 해당 노드를 찾을 수 있습니다. 단말 노드가 아니라면 contents에 질문이 들어있을 것이고, 대답에 따라 둘중 하나의 노드로 넘어가면 됩니다. 노드가 단말 노드라면 run-node는 contents를 반환하고 종료합니다. 그림 6.3에 나온대로 네트워크를 만들면 그림 6.1과 같은 출력을 얻을 수 있습니다.

 

(defun run-node (name)
  (let ((n (gethash name *nodes*)))
    (cond ((node-yes n)
       (format t "~A~%>> " (node-contents n))
	   (case (read)
	     (yes (run-node (node-yes n)))
	     (t (run-node (node-no n)))))
	  (t (node-contents n)))))

Figure 6.4: Function for traversing networks. 네트워크를 순회하는 함수

 

(defvar *nodes* (make-hash-table))

(defun defnode (name conts &optional yes no)
  (setf (gethash name *nodes*)
    (if yes
	    #'(lambda ()
		(format t "~A~%>> " conts)
		(case (read)
		  (yes (funcall (gethash yes *nodes*)))
		  (t (funcall (gethash no *nodes*)))))
	  #'(lambda () conts))))

Figure 6.5: A network compiled into closures. 클로저로 만들어진 네트워크

 

6.2 Compiling Networks 네크워크 컴파일일

 

 

(defvar *nodes* nil)

(defun defnode (&rest args)
  (push args *nodes*)
  args)

(defun compile-net (root)
  (let ((node (assoc root *nodes*)))
    (if (null node)
    nil
      (let ((conts (second node))
	    (yes (third node))
	    (no (fourth node)))
	(if yes
	    (let ((yes-fn (compile-net yes))
		  (no-fn (compile-net no)))
	      #'(lambda ()
		  (format t "~A~%>> " conts)
		  (funcall (if (eq (read) 'yes)
			       yes-fn
			     no-fn))))
	  #'(lambda () conts))))))

Figure 6.6: Compilation with static references. 정적 참조를 이용한 컴파일

 

이전 장에서는 다른 언어들이 사용할법한 방식으로 네트워크 프로그램을 만들어봤습니다. 사실 프로그램 자체는 너무 간단해서 다른 방식으로 만들 수 있다고 생각하기도 어려울 수 있습니다. 하지만 사실 훨씬 더 간단하게 만들 수 있습니다.

그림 6.5에 있는 코드를 보면 알 수 있습니다. 네트워크를 만들기 위해 필요한 전부가 있습니다. 노드를 데이터 구조로 만들고, 함수로 노드를 순회하는 방식으로 노드와 함수를 분리해서 만들지 않고, 노드를 클로저로 표현할 수 있습니다. run-node같은 함수를 만들 필요가 없습니다. 겉으로는 안보이지만 노드 안에 모든게 다 있습니다. 시작 노드에서 funcall을 호출하는 것만으로 순회를 시작할 수 있습니다.

 

 

(funcall (gethash ’people *nodes*))

Is the person a man?

>>

 

 

그리고 이어지는 대화는 위에서 구현된 것과 동일합니다. 클로저로 노드를 구현하면 스무고개의 네트워크 전체를 코드로만 구현할 수 있습니다. 그리고 런타임에 이름을 가지고 다른 노드를 찾게됩니다. 그런데 우리는 한번 정의된 네트워크가 실행중에 변경되지 않을걸 알고있습니다. 따라서 코드를 좀더 확장할 수 있습니다. 노드 함수가 자기가 가야할 다음 노드를 직접 호출하고, 해시 테이블을 사용하지 않도록 만들수 있습니다.

그림 6.6을 보면 새로 만든 프로그램이 있습니다. *nodes*는 해시테이블이 아니라 노드를 넣고 뺄 수 있는 리스트입니다. 모든 노드는 이전과 같이 defnode로 정의되고, 여기까지는 아직 클로저가 생성되지 않습니다. 모든 노드가 정의된 다음에 compile-net을 호출해서 전체 네트워크를 한꺼번에 컴파일합니다. 이 함수가 트리를 단말노드까지 재귀호출로 내려갑니다. 그리고 각 단계에서 양쪽 서브트리에 해당하는 노드이면서 함수인 객체를 반환하면서 되돌아옵니다. (저자주1) 따라서 이후부터는 각 노드마다 어디로 가야할지를 이름만 가지는게 아니라 노드에 대한 직접적인 참조를 가지게됩니다. 최초의 compile-net 호출이 반환될때는 컴파일된 네트워크 전체를 표현하는 함수를 반환합니다.

 

> (setq n (compile-net ’people))

#<Compiled-Function BF3C06>

> (funcall n)

Is the person a man?

>>

 

compile-net은 네트워크를 표현한 추상화계층을 코드로 변환합니다. 그리고 자기 자신도 컴파일해서 빌드된 함수를 반환합니다. (원서25페이지 참조)

네트워크가 컴파일된 후에는 defnode로 만든 리스트는 더 이상 필요없습니다. 없애버리면 (*nodes*을 nil로 설정하면됩니다) 가비지 콜랙터가 메모리를 해지하게됩니다.

(저자주1: 여기에서는 이 예제에서 보듯이 네트워크가 트리라고 가정합니다.)

 

 

6.3 앞으로 나아갈 방향

 

네트워크를 비롯해서 많은 프로그램들이 노드를 클로저로 컴파일하는 방식으로 구현될 수 있습니다. 클로저는 데이터 객체입니다. 그러므로 구조체가 하듯이, 클로저도 뭔가를 표현하는데 사용될 수 있습니다. 약간 새로운 생각을 해내야하지만, 더 빠르고 더 우아한 프로그램을 만들 수 있습니다.

매크로를 쓰면 클로저로 개념을 표현할 때 큰 도움을 얻을 수 있습니다. 클로저로 표현한다는 것은 컴파일한다는 것과 같은 말입니다. 매크로가 컴파일타임에 동작하기 때문에, 클로저의 동작과 잘 맞아떨어집니다. 매크로를 소개한 후에 23장과 24장에서 이번장에 사용한 전략을 이용한 더 큰 프로그램을 소개하겠습니다.

댓글

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