2019년 1월 4일 금요일

[on lisp] 4. Utility Functions 유틸리티 함수

4. Utility Functions
리스프 연산자에는 세 가지 유형이 있다.
1. 함수 2. 매크로 3. special form (우리가 쓸 수 없는 것)
이번 장에서 우리는 새로운 함수를 이용하여 리스프를 확장하는 기술을 설명한다.
여기서 기술은 보통 하던 것과 다르다.
이런 함수들에 대해 알아야할 중요한 점은 이것들이 어떻게 짜여지는 것이 아니라 어디에서 왔는가 이다.(어떤 생각에서 만들어지는지)

리스프의 확장은 여타 다른 리습 함수를 만드는 것과 동일한 기술을 이용한다.
이 확장에서 어려운 부분은 어떻게 쓸지를 결정하는 것이 아니라. 어떤 녀석을 확장할 지다.

4.1 Birth of a Utility
간단한 형태로, 상향식 프로그래밍은 누가 당신의 리스프를 설계했던간에 사후평가하는 것을 의미한다.(??)
당신이 프로그램을 짜는 동시에, 당신의 프로그램을 쉽게 쓸 수 있게 해주는 연산자를 또한 추가한다.
이런 연산자(operator)를 유틸리티라고 한다.

"유틸리티"라는 용어는 정확한 정의를 가지고 있지 않다.
코드의 한 부분이 별도의 어플리케이션이라고 보기에는 너무 작고, 어떤 특정 프로그램의 부분으로 고려하기에는 너무 범용적인 경우 "유틸리티"라 한다.
예를들어, 데이터베이스 프로그램은 유틸리티가 아니라 목록에서 단일작업을 수행하는 기능이 될 수 있다.

대부분 유틸리티는 리습이 이미 가지고 있는 매크로와 함수와 닮았다.(범용적이니까)
실제로 많은 커먼리습의 빌트인 오퍼레이터는 유틸리티로 시작된 녀석들이다.
함수 remove-if-not 같이 목록(리스트)에서 특정 predicate을 만족하는 모든 요소를 삭제하는 기능 또한 개별 프로그래머에게 정의되었던 녀석이다.

유틸리티를 쓰는 법을 배우는 것은 그것을 쓰는 기술이라기보다는 그것을 쓰는 습관을 배우는 것으로 더 잘 설명될 것이라 한다.
상향식 프로그래밍(Bottom-up programming)은 프로그램을 짜는 것과 동시에 프로그래밍 언어를 짜는 것이다.
이것을 잘하려면 어던 오퍼레이터가 현재 프로그램에 부족한지 알아내는 섬세한 감각을 개발해야 한다.
당신은 프로그램을 보고 이렇게 말할 수 있어야 한다. "Ah, what you really mean to say is this"
직관적으로 이해할 수 있어야 한다는 말인듯?

예를들어보자, nicknames라는 함수가 있다고 하자. 이름을 매개변수로 받아서 이것에서 파생되는 모든 닉네임들을 리스트로 뱉는다 해보자.
어떻게 우리는 리스트에 있는 모든 이름들의 닉네임을 받을 수 있을까?
우린리습을 배웠으니 아래처럼 할 것이다.
(defun all-nicknames (names)
  (if (null names)
      nil
   (nconc (nicknames (car names))
          (all-nicknames (cdr names)))))
좀 더 경험있는 리스프 프로그래머라면, "mapcan"이라는 걸 쓸 것이다.
====
mapcan - function &rest lists+ => concatenated-result
====
출처 http://www.lispworks.com/documentation/HyperSpec/Body/f_mapc_.htm
이제 위에 "mapcan"을 쓰면 아래식으로 끝난다.
(mapcan #'nicknames people)
일전에 정의했던 all-nicknames는 바퀴를 재발명한 것이다. 하지만 그것만이 문제는 아니다.
범용연산자로 할 수 있는 일이 특정 함수에 묻혀버린 것이다.
여기서 "mapcan"은 이미 존재하는 녀석이다. 이걸 아는 사람들은 "all-nicknames"라는 함수를 보기 불편할 것이다.
상향식 프로그래밍에 능숙하다는 것은 누락된 연산자가 아직 만들어지지 않았을 때 똑같이 불편함을 느끼는 것이다.
당신은 "당신이 원하는 것은 x다"라고 말할 수 있어야 하고, 동시에 x가 무엇이어야 하는지 알 수 있어야 한다.

리스프 프로그래밍은 필요로 하는 새로운 유틸리티를 분리하는 일도 포함된다.
이 절의 목적은 이러한 유틸리티가 어떻게 생성되는지 보여주는 것이다.

아래 코드를 보자. 'towns심볼은 근처에 있는 town들을 리스트로 가지고 있다. 그리고 가까운 것이 먼저나오는 순서로 정렬되어 있다.
#'bookshops 함수는 도시 안에 bookshop의 리스트를 리턴한다. 만약 우리는 서점이 있는 가장가까운 도시를 찾는다면
지은이는 이렇게 적을거라 한다.
(let ((town (find-if #'bookshops towns))) ;; bookshops 함수 한번
  (values town (bookshops town)))  ;; bookshops 함수 두번
하지만 위 코드는 아름답지 않다. find-if가 #'bookshops의 리턴값이 non-nil인 것을 찾는다. 이 리턴값에 따라 재.계.산. 된다.
만약 #'bookshops이 아주 비싼(시간이 걸리는) 호출이라면, 이 코드는 문제가 있다. 이런 불필요한 일을 없애기 위해 아래처럼 바꾸자.
(defun find-books (towns)
  (if (null towns)
      nil
      (let ((shops (bookshops (car towns))))  ;; let으로 이제 bookshops는 한번만 실행된다.
           (if shops  ;; 내용이 있는지 확인
     (values (car towns) shops)  ;; 있으면 바로 리턴
        (find-books (cdr towns))))))  ;; 없으면 리스트의 다음 타자로 재귀
이제는 필요 이상의 계산이 없이 원하는 것을 얻을 수 있다.

그런데 이런종류의 검색을 종종 할 것같은 생각이 든다. (이럴때 유틸리티로 분리를 하는 것)
여기서 우리가 원하는 건 find-if와 some을 이 합쳐진 것이다. 성공적인 요소와 테스트 함수에서 반환한 값을 리턴하는 녀석!
(defun find2 (fn lst)
  (if (null lst)
      nil
      (let ((val (funcall fn (car lst))))
           (if val
        (values (car lst) val)
        (find2 fn (cdr lst))))))
잘보면 find-books와 find2는 아주 비슷하다.
find2가 find-books의 골격이라고 보면 될 것 같다.
이제 사용해보자.
(find2 #'bookshops towns)
이것이 리습의 독특한 특징중 하나이다. 함수가 매개변수로 중요한 역할을 하는 것.
이것이 왜 리습이 상향식 프로그래밍(bottom-up programming)에 잘 맞는지 말할 수 있는 이유 중 하나다.
함수를 매개변수로 던져서 어떤 함수의 살을 붙일 수 있다면, 함수의 골격을 추상화 하는것이 쉬워진다.

프로그래밍을 입문 할 때, 추상화를 하면 중복된 노력을할 필요가 없다고 한다.
첫번째 레슨은 : Don't wire in behavior
예를들어 하나 또는 두 개의 상수에 대해 동일한 작업을 수행하는 두 함수를 정의하는 대신 단일 함수를 정의하고 상수를 인수로 전달하라.

lisp에서는 함수를 매개변수로 전달할 수 있기 때문에 아이디어를 더 잘 수행할 수 있다.
앞서 보여준 두 예제 모두. 특정 기능을 위한 함수에서 좀 더 일반적인 함수로 변형되었다. (함수를 매개변수로 넘기는 방식을 이용하여)

첫번째경우에는 미리 정의된 mapcan을 이용하였고, 두번째 경우에는 find2를 이용하였다. 둘다 원칙은 같다.

"범용 함수와, 구체적인 함수를 혼합하는 대신 범용함수를 정의하고 구체적인 것을 매개변수로 넣어라."

이 내용을 신중하게 적용하면 원칙에 따라 훨씬 더 우아한 프로그램을 만들어낼 수 있다.
이것이 상향식 디자인(bottom-up design)을 강요하는 유일한 이유는 아니지만, 주요한 이유중 하나이다.
이 챕터에서 정의되는 32개의 유틸리티에서 18개는 함수형 매개변수를 받는다.

댓글 없음 :

댓글 쓰기