2019년 1월 6일 일요일

[on lisp] 4.2 Invest in Abstraction (추상화에 투자)

4.2 Invest in Abstraction (추상화에 투자)

간결함이 지혜 영혼이라면, 그것은 효율성과 함께 좋은 소프트웨어의 본질이다.

프로그램의 작성,유지보수의 비용은 그 길이에 따라 증가한다.
만약 다른 부분이 동일하다면, 프로그램은 짧은 수록 좋다.

이런 관점에서 보면, 유틸리티의 작성은 자본지출로 간주되어야 한다.
find-books를 유틸리지 find2로 교체함으로써, 우리는 많은 코드 라인을 직면하게 된다.

하지만 우리는 더 짧게 만들었다고 말할 수도 있게 되는데, 유틸리티 코드의 길이는 현재 프로그램에 자본지출로 부과되지 않을 것이다.
(왜냐하면 범용적이기 때문이라고 생각함)
이것은 단순히 Lisp의 확장을 자본지출로 취급하는 것은 단지 회계속임수가 아니다.
유틸리티들은 다른 파일로 분리될 수 있다. 이것들은 우리가 프로그램 작업을 할 때, 우리의 시각을 혼란스럽게 만들지 않을 것이고,
우리가 나중에 돌아와서 프로그램의 어떤 곳을 변경하려 할 때, 참여하지 않을 것이다.(다시 한번, 유틸리티는 범용적이다)

자본 지출로서, 유틸리티는 더 많은 관심이 필요하다.
잘 짜는 것이 중요하다. 이것들은 반복적으로 사용될 것이기 때문에, 부정확성이나 비효율성이 배가 될 것이다.(잘못짜면)
이런 추가적인 관심은 디자인에 반영되어야 한다. (설계시 주의해야 한다.)
유틸리티는 당면한 문제가 아니라 일반적인(범용) 경우를 위해 만들어져야 한다.

마지막으로, 여타 다른 자본 지출과 마찬가지로, 우리는 유틸리티를 만드는데 서두를 필요가 없다.
새로운 연산자로 사용하려고 생각하고 있지만, 그것을 정말로 원하는건지 확신하지 못한다면,
일단 작성하되, 현재 사용하고 있는 (도메인에)특정한 프로그램 또한 남겨놔라.
나중에 다른 프로그램에서 새 연산자를 사용하면 서브루틴에서 유틸리티로 승격하여 일반적으로 접근 가능하도록 할 수 있다.
(뭐 헬퍼 펑션을 만든다는 말인듯?)

유틸리티 'find2는 좋은 투자로 보입니다.
7줄을 자본지출하여, 즉시 7줄의 비용절감을 한다.
유틸리티는 처음 사용했을 때 이미 그 대가를 치뤘다.

가이스틸은 다음과 같이 썼다.
"간결성에 대한 우리의 자연스러운 경향에 협력해라"
"cooperate with our natural tendency towards brevity"

'우리는 프로그래밍 구성의 비용은 그것이 서경(書痙: 글씨를 너무 많이 써서 생기는 손의 통증)의 양에 비례한다고 믿는다.
(그러니까 우리가 키보드질 많이 하는 것이 비용이라고 하는 것 / 여기서 '믿는다'는 열렬한 신념이 아니라 무의식적인 경향을 말한다)
사실 이것이 언어설계자가 염두해야할 나쁜 심리 원칙은 아니다.
우리는 덧셈이 저렵하다고 생각한다. 왜냐하면 우리는 "+"라는 한글자로 구분할 수 있기 때문이다.
아무리 어떤 A의 구성이 비싸다고 믿어라도, 그것이 우리의 작문 노력을 반으로 줄인다면, 싼 구성보다 A의 구성을 선호할 것이다.

모든 언어에서 "간결성을 향한 경향"은 새로운 유틸리티로 배출하지 않으면 문제를 일으킬 것이다. (요구를 말하는지 욕구를 말하는지?)
가장 짧은 숙어가 가장 효율적인 숙어는 이기는 드물다.
만약 당신이 한 리스트가 다른 리스트보다 긴지 여부를 알고 싶다면, 원시 Lisp는 우리에게 뭔가 작성하기를 유혹할 것.(아래처럼)
(> (length x) (length y))
여러 리스트에 함수를 매핑하려면, 먼저 함수를 다음처럼 결합해야 한다.
(mapcar fn (append x y z))   ;; append로 결합
이런 예는 우리가 비효율적으로 처리하는 상황이 오면 유틸리티를 작성하는 것이 중요하다는 것을 알려준다.
적절한 유틸리티로 강화된 언어는 우리가 더 많이 추상화하여 프로그램을 만들 수 있다.
이러한 유틸리티가 적절히 정의되면, 보다 효율적인 유틸리티를 작성하게 될 것이다.

유틸리티 모음을 사용하면 확실히 프로그래밍이 쉬워진다. 하지만 이것들은 그 이상을 할 수 있다: 더 나은 프로그램으로 만들 수 있다.
요리사 같은 뮤즈(예술가)들은 재료가 보이면 바로 행동을 취한다.
이것이 예술이들이 그들의 스튜디오에 많은 도구와 재료를 갖고 싶어하는 이유다.
그들은 (가지고 오는데) 준비가 필요한 것을 가까이에 가지고 있다면 새로운 것을 시작할 가능성이 많다는 것을 안다.
상향식 프로그래밍에도 동일한 현상이 나타난다.
일단 당신이 새로운 유틸리티를 짰다면, 당신은 그것을 생각했던 것보다 더 많이 사용하고 있는 당신을 발견할 것이다.

다음 섹션에서는 유틸리티 함수의 여러 클래스를 설명한다.
이것들이 lisp에 추가될 수 있는 모든 종류의 기능을 대표하지는 않는다.
그러나 예로서 주어진 모든 유틸리티는 실제로 그들의 가치를 증명한 것들이다.

2019년 1월 4일 금요일

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

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

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

4.1 Birth of a Utility
간단한 형태로, 상향식 프로그래밍은 누가 이 Lisp를 만들었건 사후평가(나중에 평가해서 사용한다)하는 것을 의미한다.
동시에 당신이 프로그램을 짤 때, 당신의 프로그램을 쉽게 쓸 수 있게 해주는 연산자를 또한 추가한다. (리스트는 더 새로워진다)
이런 연산자(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개는 함수형 매개변수를 받는다.

2019년 1월 3일 목요일

[on lisp] 3.4 Interactive Programming

3.4 Interactive Programming

함수형 방식은 단순히 이뻐보여서 제시하는 것이 아니라. 일을 더 쉽게 할 수 있도록 한다.
리습의 동적 환경에서 함수형 프로그램은 비정상적인 속도로 작성되며 동시에 매우 신뢰할 수 있는 코드를 생산한다.

리스프에서 디버그는 비교적 쉽다.
오류의 원인을 추적하는 많은 정보를 런타임에서 이용할 수 있다.
하지만 더 중요한 것은 테스트를 쉽게 할 수 있다는 것이다.
컴파일 후 테스트를 하는 작업을 항상 같이 한번에 해야 할 필요가 없다.
toplevel 루프에서 함수를 개별적으로 호출해서 테스트를 할 수 있다.

Incremental testing은 리스프 스타일이 그것을 이용하기 위해 진화해왔기 때문에 아주 귀중하다.
함수형 스타일로 짜여진 프로그램은 하나의 함수가 하나의 기능으로 이해될 수 있으며,
읽는 사람의 관점에서 이것은 아주 큰 장점이다.

하지만 함수형 스타일은 또한 incremental testing에 아주 잘 맞는다.
(아까 위에서 설명했듯이) 함수형 스타일로 짜여진 프로그램은 한번에 한가지의 함수(기능)을 테스트할 수 있다.

함수가 외부 상태를 조회하거나 수정하지 않으면, 버그는 즉시 나타난다.(바로 알 수 있다는 말)
이런 기능은 오로지 리턴값으로만 바깥 세상에 영향을 미친다.
그러므로 우리는 우리가 짠 코드를 더 믿을 수 있게 된다.

순련된 리스프 프로그래머는 실제로 테스트하기 쉽게 프로그램을 설계한다.
1. 그들은 몇가지 side-effect를 분리하려고 노력한다. 많은 부분을 순수 함수형 스타일로 작성할 수 있도록 한다.
2. 만약 side-effect를 수행해야 하는 함수의 경우, 최소한 함수적 인터페이스(functional interfaces)로 제공한다.
3. 각 기능에 잘 정의된 단일 목적을 제공한다.

리스프에서는 테스트가 단일로 toplevel loop로 즉각 알 수 있기 때문에 피드백이 아주 빠르다.
리스프에서 개발은 (다른 언어와 마찬가지로) 쓰기와 테스트의 순환이다.
하지만 리스프는 그 주기가 아주 짧다. 하나의 함수 혹은 여러 함수들의 부분이라해도 그렇다.

만약 해당 함수를 짜고 테스트를 돌려서, 에러가 나면 어디서 발생했는지 바로 안다. (마지막으로 쓴 곳)
간단하게 들리겠지만, 이 원칙은 상향식 프로그래밍을 가능하게 하는 것이다.
이것은 리습 프로그래머들이 기존 개발 방식에서 자유와 자신감을 준다. (오래된 계획 후 구현의 하향식 개발방식)

1.1절에서 상향식 설계(bottom-up design)가 진화하는 과정(점차점차 진화시키는 방식)이라 강조했다. 당신은 그 안에 프로그램을 짜면서 언어를 만들어내는 것이다. (쌓아 올라가는 것)
이런 접근법은 당신이 낮은 수준의 코드를 신뢰하는 경우에만 작동할 수 있다. (아래부분이 믿을만해야 한다는 말)
당신이 마주치는 모든 버그는 언어 자체가 아니라 당신의 어플리케이션에서 발생한 버그라고 가정할 수 있어야 한다.

결국 종단에는 당신의 잘못인 경우가 많다는 것이다. 당신이 만든 추상화가 이런 무거운 짐을 견딜 수 있으며, 필요할 때 쪼갤 수 있도록 만들어져야 하는가?
만약 그렇다면, Lisp 로 둘다 가질 수 있다. 함수형 스타일로 코드를 짜고 점진적으로 테스트를 할 때, (순간적으로)순식간에 일을 유연하게 할 수 있다.
그리고 이것은 신중한 계획에 신뢰성을 더할 것.

[on lisp] 3.3 Functional Interfaces

3.3 Functional Interfaces

어떤 사이드이팩트는 다른 것들보다 나쁘다.
예를들자. 이 함수는 nconc를 호출한다.
(defun qualify (expr)
  (nconc (copy-list expr) (list 'maybe)))
이것은 참조 투명성을 유지한다. 같은 인수를 넣으면 항상 같은 값을 반환할 것이다. (리스트를 수정하고 있다고는 하지만 새로 복사를 하기 때문에 상관없다.)
그러므로 호출자의 관점에서는 이건 순수 함수형 코드일 것이다.
우리는 이전에서 본 bad-reverse와 같다고 할 수 없다. (실제 매개변수를 변경하고 있는 녀석과는 다르다.)

모든 사이드이팩트를 나쁘게 다루는 것 보다. 이런 경우들과는 구별이 되도록 해야겠다.
비공식적으로, 아무도 소유하지 않은 것을 수정하는 것은 무해하다 말할 수 있다.
예를 들어, 위에 nconc는 무해하다. 왜냐하면 매개변수로 받은 expr는 새롭게 만들어져서 더해졌기 때문이다.
새롭게 만들어진 이 녀석은 아무도 소유할 수 없었다.

일반적으로, 우리는 소유권에 대해 함수가 아닌, 함수의 호출에 대해 이야기 해야 한다.
아래 코드에서 아무도 변수 x를 소유하지 않고 있다
(let ((x 0))
  (defun total (y)
    (incf x y)))
이 호출의 영향은 다음 호출에서 나타날 것이다.
따라서 다음의 규칙을 따라야 한다: 주어진 호출은 고유하게 소유하는 것을 안전하게 수정할 수 있어야 한다.

이렇게 사이드이팩트가 있어보여도 없는 것이 있고 있는 것이 있다.
책에 예제를 보면
(defun ok (x)
  (nconc (list 'a x) (list 'c)))

(defun not-ok (x)
  (nconc (list 'a) x (list 'c)))
ok함수는 nconc를 해도 매개변수로 온 아이가 문제되지 않는다. list함수로 새로 만들어졌기 때문이다.
not-ok는 그대로 사용하기 때문에 nconc가 매개변수를 수정할 여지가 있다.

이렇게 사이드이팩트가 밖으로 나가지만 않으면 괜찮다고 생각하는 것 것 같다.

어떻게 해야 좀 더 안전한가. 함수 안에 변수를 넣기만 하면 수정을 해도 사이드이팩트가 없는 것을 보장하도록 하는 방법은?
(defun f (x)
  (let ((val (g x)))
    ;; 여기서 val은 정말 안전한가?
))
안전하지 않다. g가 identity함수라면 그냥 던져진다.
어떻게 해야 하나 그러면, 함수형프로그래밍이 되는가. f라는 함수안에서만 변경되어야 하는데 밖으로 나가진다니... 그렇다면 모든 함수가 위험하고
결국 믿지 못하게 된다. 이렇게 모든 경우를 계속 확인해야 하는가.

Instead of worrying about the whole program, we now only have to consider the subtree beginning with f.
f함수ctional Interfaces

어떤 사이드이팩트는 다른 것들보다 나쁘다.
예를들자. 이 함수는 nconc를 호출한다.
(defun qualify (expr)
  (nconc (copy-list expr) (list 'maybe)))
이것은 참조 투명성을 유지한다. 같은 인수를 넣으면 항상 같은 값을 반환할 것이다. (리스트를 수정하고 있다고는 하지만 새로 복사를 하기 때문에 상관없다.)
그러므로 호출자의 관점에서는 이건 순수 함수형 코드일 것이다.
우리는 이전에서 본 bad-reverse와 같다고 할 수 없다. (실제 매개변수를 변경하고 있는 녀석과는 다르다.)

모든 사이드이팩트를 나쁘게 다루는 것 보다. 이런 경우들과는 구별이 되도록 해야겠다.
비공식적으로, 아무도 소유하지 않은 것을 수정하는 것은 무해하다 말할 수 있다.
예를 들어, 위에 nconc는 무해하다. 왜냐하면 매개변수로 받은 expr는 새롭게 만들어져서 더해졌기 때문이다.
새롭게 만들어진 이 녀석은 아무도 소유할 수 없었다.

일반적으로, 우리는 소유권에 대해 함수가 아닌, 함수의 호출에 대해 이야기 해야 한다.
아래 코드에서 아무도 변수 x를 소유하지 않고 있다
(let ((x 0))
  (defun total (y)
    (incf x y)))
이 호출의 영향은 다음 호출에서 나타날 것이다.
따라서 다음의 규칙을 따라야 한다: 주어진 호출은 고유하게 소유하는 것을 안전하게 수정할 수 있어야 한다.

이렇게 사이드이팩트가 있어보여도 없는 것이 있고 있는 것이 있다.
책에 예제를 보면
(defun ok (x)
  (nconc (list 'a x) (list 'c)))

(defun not-ok (x)
  (nconc (list 'a) x (list 'c)))
ok함수는 nconc를 해도 매개변수로 온 아이가 문제되지 않는다. list함수로 새로 만들어졌기 때문이다.
not-ok는 그대로 사용하기 때문에 nconc가 매개변수를 수정할 여지가 있다.

이렇게 사이드이팩트가 밖으로 나가지만 않으면 괜찮다고 생각하는 것 것 같다.

어떻게 해야 좀 더 안전한가. 함수 안에 변수를 넣기만 하면 수정을 해도 사이드이팩트가 없는 것을 보장하도록 하는 방법은?
(defun f (x)
  (let ((val (g x)))
    ;; 여기서 val은 정말 안전한가?
))
안전하지 않다. g가 identity함수라면 그냥 던져진다.
어떻게 해야 하나 그러면, 함수형프로그래밍이 되는가. f라는 함수안에서만 변경되어야 하는데 밖으로 나가진다니... 그렇다면 모든 함수가 위험하고
결국 믿지 못하게 된다. 이렇게 모든 경우를 계속 확인해야 하는가.

Instead of worrying about the whole program, we now only have to consider the subtree beginning with f.
f함수의 하위구조를 한번 보자.
변경할 내용을 함수로 감싸서 실행하는 방식이 수정 사이드이팩트를 막아주지 못한다는 것을 알았다. 우리는 또 다른 법칙이 필요하다.

1. Thus one should avoid writing functions whose return values incorporate quoted objects.
따라서 반환 값에 (quote가 붙은)객체가 포함 된 함수를 작성하지 않아야합니다.
(defun exclaim (expression)
  (append expression '(oh my)))

> (exclaim '(lions and tigers and bears))
(LIONS AND TIGERS AND BEARS OH MY)
> (nconc * '(goodness))
(LIONS AND TIGERS AND BEARS OH MY GOODNESS)
이렇게 되면 안에 있는 함수를 바꿀 수 있게 된다.
> (exclaim '(fixnums and bignums and floats))
(FIXNUMS AND BIGNUMS AND FLOATS OH MY GOODNESS)
왜냐하면 '(oh my)가 리턴되었고 그 값은 함수 안에 있는 참조랑 같게 되는 것이다. 그러니 밖에서 GOODNESS가 더해져도
함수 안에 있는 '(oh my)도 변해서 '(oh my goodness)가 되는 것이다.

해결책은
(defun exclaim (expression)
  (append expression (list 'oh 'my)))
이렇게 하면 새로 리스트가 만들어 진거니까 괜찮아진다.

[on lisp] 3.2 Imperative Outside-In 명령형 뒤집기

3.2 Imperative Outside-In

함수형 프로그래밍의 목적은 명령형 프로그래밍과 대조될 때 더 명확하게 드러난다.
함수형 프로그램은 당신이 무엇을 원하는지 말한다. 명령형 프로그램은 당신이 무엇을 해야 하는지 말한다.

비교를 해보자면
함수형 프로그램은 "Return a list of a and the square of the first element of x"라고 말한다.
(defun fun (x)
  (list 'a (expt (car x) 2)))
명령형 프로그램은 "Get the first element of x, then square it, then return a list of a and the square"라고 말한다.
(defun imp (x)
  (let (y sqr)
    (setq y (car x))
    (setq sqr (expt y 2))
    (list 'a sqr)))

명령형 에서 함수형으로 변환하는 트릭이 있다.
그 트릭은 명령형 프로그램은 함수형 프로그램이 안에서 밖으로 변형된 것이며
함수형 프로그램은 명령형 프로그램이 밖에서 안으로 변형된 것이라 해보자.

첫번째로 우리가 주목해야 하는 것은 let 내부에 있는 y, sqr의 생성이다.
이것은 나쁜일이 따라올 거라는 징조다. 런타임에 eval을 하는 것처럼 초기화 되지 않은 변수들은 거의 필요하지 않다.
하여 일반적으로 프로그램 내 질병의 징후로 취급해야 한다.

이러한 변수들은 프로그램을 자연스러운 모양(natural shape)으로 되지 않도록 고정시키는 핀처럼 쓰인다.
일단 이것들은 넘어가고 함수의 마지막으로 가보자.
명령형에서 가장 마지막에 실행되는 녀석은 함수형에서는 가장 바깥쪽에서 실행된다.

그러므로 우리가 할 일의 첫번째 단계는, 마지막 호출을 잡은 채로 프로그램의 나머지를 안에 집어 넣는 것이다!
마치 셔츠를 뒤집는 것처럼. 해보자.
마지막에 있는 값 (list 'a sqr)을 잡은 채로 나머지 내용을 안에 넣는 것이다.
(list 'a sqr)
;; sqr을 안에 넣는다.
(list 'a (expt y 2))
;; 이제 y도 안에 넣는다.
(list 'a (expr (car x) 2))
;; 명령형이 함수형으로 변경되었다.
필자 말로는 이 최종결과가 이전 초기버전보다 낫다고 한다.
기존 명령형 코드를 보면, 우리는 리턴을 하는 마지막 표현식 (list 'a sqr)을 만나게 되는데
sqr이 어디서 왔는지 알 수 없기 때문에 즉각적으로 명확히 할 수 없다.
이제 변형된 리턴코드는 로드맵처럼 제시되어 있다.

이 기법은 더 큰 기능에 적용되면서 가치가 높아진다.
사이드이팩트를 수행하는 함수에조차 그렇지 않은 부분을 정리할 수 있다.

2019년 1월 2일 수요일

[on lisp] 3.1 Functional Design 함수형 프로그래밍

3. Functional Programming
이전 장에서는 리스프와 리스프 프로그램 모두 하나의 원재료로 만들어졌음을 알려줬다. (함수)
여타 다른 건축 자재처럼, 이것들의 품질은 우리가 만드는 것들의 종류와 방식까지 모두 영향을 미친다.

3.1 Functional Design
객체의 특징은 그것이 뭐로 만들어졌는지에 영향을 받는다.
목조건물과 석조건물은 다르다. 목조건물로 할 수 없는 것을 석조건물은 할 수 있다. 그 반대도 마찬가지다.
멀리서 건물을 볼 때, 그것이 나무인지 돌인지는 확인할 수 없어도, 건물의 전체적인 모양을 보고 그것이 무엇으로 만들어졌는지 알 수 있다.

Lisp함수의 특성은 Lisp 프로그램의 구조에 그와 상응하는 영향을 미친다.

Functional programming means writing programs which work by returning values instead of by performing side-effects.
함수형 프로그래밍은 사이드이펙트를 만드는 대신 값을 리턴하는 방식으로 프로그램을 짜는 것이다.

사이드이펙트는 파괴적인 객체변환(rplaca함수를 사용: cons의 car를 치환)과, 변수를 할당하는 것이다.(setq)
사이드이펙트가 더 적고, 국지적일경우(범위가 작아질 경우), 프로그램의 읽기, 테스트, 디버그가 쉬워진다.
리스프 프로그램이 항상 이런 스타일로 작성된 것은 아니지만, 시간이 흐르면서 리스프와 함수형 프로그래밍은 분리될 수 없게되었다.

예를들어, 다른 언어와 다른 것을 보여줄 것이다.
리스트를 역순으로 만들고 싶다고 하자.

리스트를 역순으로 변.경.하는 함수를 짜는대신에, 우리는 리스트를 받고 똑같은 요소를 가진 리스트를 리턴하는 함수를 만들자. (물론 역순)
(defun bad-reverse (lst)
  (let* ((len (length lst))
  (ilimit (truncate (/ len 2))))
    (do ((i 0 (1+ i))
  (j (1- len) (1- j)))
 ((>= i ilimit))
      (rotatef (nth i lst) (nth j lst)))))
(setq lst '(a b c))
(bad-reverse lst) ; NIL
lst ; (C B A)

(bad-reverse '(1 2 3 4 5))  ;; ?? !!
이름부터 bad-reverse다. 이건 리스트를 새로 받아서 변.경.하는 함수를 짠 것이다.
리턴 값은 뭔가? NIL 내가 원하는 값과는 관련이 없다.
이건 리스프의 스타일이 아니다.
더 알아야 할 점은 이런 추함은 전염된다는 것이다. 이런 함수는 나중에 사용하는 사람에게도 해를 끼치ㅐㄴ다.
그리고 사용하는 사람도 함수적인 프로그래밍을 할 수 없게 만든다. 기능적 제한을 만들어주는 꼴이다.

새로 만들어보자.
(defun good-reverse (lst)
  (labels ((rev (lst acc)
  (if (null lst)
      acc
   (rev (cdr lst) (cons (car lst) acc)))))
   
 (rev lst nil)))
(setq lst '(a b c))
(good-reverse lst) ; (C B A)
lst ; (A B C)
이렇게 하면 아주 함수적으로 만든것이다.

이 책의 저자는 사람의 특징을 판단할 때 그의 머리스타일을 보고 판단할 수 있다고 생각했다 한다.
이것이 사실이던 아니던 리스프 프로그램에서는 사실이다.
Functional Program은 imperative Program과는 다른 형태를 가지고 있다.

함수형 프로그램의 구조는 전적으로 표현식 내에 인수들의 구성(방식)으로 표현된다.

인수들이 (인덴트로) 들쑥날쑦하기 때문에, 함수적인 코드는 들여쓰기에서 더 많은 움직임이 있다.
함수적코드는 페이지 위에 액체 같고, 명령형코드는 단단하고 나무토막 같다.
(뭐 함수형 코드가 더 유동적으로 보이고, 명령형 코드는 덜 유동적이라 자유롭지 않다는 말인듯? 중요하지 않은 내용이므로 고민말고 넘어감)

심지어 멀리서 봐도, bad-reverse와 good-reverse의 형태는 어떤 것이 더 나은 프로그램인지 암시한다.
(확실히 함수형 프로그램은 들여쓰기가 계속 생겨서 마구마구 움직이는 것 같지만 명령형 코드는 들여쓰기가 없고 목재마냥 직사각형을 보이고 있다.)
비록짧지만 good-reverser가 더 효율적이다. O(n) instead of O(n2).

커먼리습에는 가실 reverse 기능을 내장하고 있다.
(setq a '(1 2 3))

(print (reverse a))

(print a)
이 기능은 기존 내용을 변경하지 않고 새로 리턴한다. 그러므로 반약에 a를 변경하고 싶다면? 새로 값을 넣어야 한다.
(setq a (reverse a))
good-reverse와 bad-reverse의 차이에서 간과한 점은 bad-reverse는 "cons"를 하지 않는다는 것이다.
새로운 list structure를 만드는 것이 아니라. 기존 리스트에 실행된다. 이런건 아주 위험할 수 있다.
리스트는 다른 곳에서 사용될 수도 있다. 하지만 효율성을 고려할 때 필요한 상황이 있을 수 있다.
... 중략 ...
함수형 프로그래밍은 일반적으로 좋은 생각이다. 특히 Lisp에서, 왜냐하면 Lisp은 이것을 제공하기 위해 진화해왔기 때문이다.

[on lisp] 2.8 Tail-Recursion 꼬리재귀

2.8 Tail-Recursion

재귀는 자기자신을 호출하는 거다.
꼬리재귀는 자기자신을 호출할 때, 콜스택이 바닥나지 않도록 미리 일을 해놓는다고 한다.
아래 내용은 꼬리재귀가 아니다.
(defun our-length (lst)
  (if (null lst)
      0
    (1+ (our-length (cdr lst)))))
왜냐하면 재귀호출에서 리턴을 할 때, 1+이라는 함수에게 넘겨야 하기 때문이다.
아래는 꼬리재귀다.
(defun our-find-if (fn lst)
  (if (funcall fn (car lst))
      (car lst)
   (our-find-if fn (cdr lst))))
왜냐하면 재귀호출의 값이 즉시 리턴되기 때문이다.

꼬리재귀는 꽤나 중요한데, 이런 재귀호출을 루프로 변환해주도록 컴파일러들이 디자인되어 있다.
이런 우아한 재귀호출을 사용하면서도 함수를 호출하는 오버헤드를 느낄 수 없게 된다.
이정도의 최적화면 재귀호출가 루프를 대신할 정도는 된다고 할만 하다.

꼬리재귀가 아닌 함수는 종종 꼬리재귀로 변형하기 위해서 accumulator를 내부에 넣어서 상태를 기록하도록 한다.
(defun our-length (lst)
  (labels ((rec (lst acc)
                (if (null lst)
        acc
     (rec (cdr lst) (1+ acc))))))
  (rec lst 0))
여기 acc를 보면 계속 1+로 1을 더하면서 재귀를 실행한다. 이렇게 누산기를 넣어서 재귀를 하는 거다.
그리고 acc를 마지막에 리턴할 거다.
커먼리습 컴파일러는 이런 꼬리재귀를 최적화 할 수 있다. 하지만 이게 디폴트인 경우는 드믈다.
파일 맨 위에 아래 내용을 적어라.
(proclaim '(optimize speed))
꼬리재귀와 타입선언으로 커먼리습 컴파일러는 코드가 C언어만큼 (혹은 더 빠르게) 코드를 생성한다.
Richard Gabriel 이라는 사람이 소개한 예제를 적는다.
1부터 n까지 더하는 함수이다.
(defun triangle (n)
  (labels ((tri (c n)
  (declare (type fixnum n c))
  (if (zerop n)
      c
    (tri (the fixnum (+ n c))
         (the fixnum (- n 1))))))
    (tri 0 n)))
(print (triangle 10))
아래처럼 acc를 이용하여 꼬리재귀를 이용한다.
declare,type을 이용하여 타입을 지정하는 것으로 보인다.

[on lisp] 2.7 Local Functions 로컬변수처럼 함수쓰기

2.7 Local Functions

람다표현식을 사용할 때, defun을 사용할 때는 직면하지 않았던 문제에 직면하게 된다.
람다표현식은 이름이 없기 때문에 자기 자신을 부를 방법이 없다.
그래서 커먼리습에선 재귀를 람다만으로 구현할 수 없다.

예를들면
(mapcar #'(lambda (x) (+ 2 x)) '(2 5 7 3))  ;; (4 7 9 5)
만약 여기서 재귀함수를 넣고 싶다면?
(mapcar #'copy-tree '((a b) (c d e)))
이렇게 그냥 넣으면 된다. 하지만 람다는?
label이라는 녀석을 사용하면 된다.
(labels ((inc (x) (1+ x))) (inc 3))  ;; 4
labels는 let의 함수버전이라고 생각하면 된다.
(defun count-instances (obj lsts)
  (labels ((instances-in (lst)
    (if (consp lst)  ;; list면
        (+ (if (eq (car lst) obj) 1 0) ;; obj와 같냐 틀리냐고 1,0으로 나뉘고  
    (instances-in (cdr lst)))  ;; 재귀적으로 나머지 리스트를 다시 호출해서 obj('a)가 몇개 있는지 확인한다.
      0)))  ;; 리스트가 아니면 0
    (mapcar #'instances-in lsts)))
    
(print (count-instances 'a '((a b c) (d a r p a) (d a r) (a a))))
;; 각 리스트에 'a가 몇개 있는지
;; 1 2 1 2
이렇게 쓸 수 있다.
라벨링을 일단 하고 사용할 함수에 그 녀석을 넣으면 된다.

[on lisp] 2.6 Closures 클로저 사용하기

2.6 Closures

커먼리습은 lexical scope이기 때문에, 함수를 free variable을 포함하여 정의할 수 있다.
free variable을 포함하여 정의를 하면 시스템은 함수가 정의된 시점에 해당 변수에 바인딩된 녀석을 찾고 사본을 저장해야 한다.

이런 함수와 변수바인딩의 조합을 closure(클로저)라고 한다.
클로저는 커먼리습에서 너무 만연하여 자신도 모른체 사용하고 있을 수도 있다.


Every time you give mapcar a sharp-quoted lambda-expression containing free variables, you're using closures.
mapcar에 #'(lambda ...)에 free variable이 포함된 것을 봣다면 지금까지 closure를 쓴것이다.

예를들어, 리스트를 받아서 특성 숫자를 각 요소에 더하는 함수를 만든다고 하자.
(defun list+ (lst n)
  (mapcar #'(lambda (x) (+ x n))
          lst))
(list+ '(1 2 3) 10)
list+를 자세히 보면 closure가 적용된 것을 알 수 있다.
잘 보면 lambda 함수 안에 n은 free variable이다. 이 녀석은 둘러쌓인 환경(defun list+ ...)에서 바인딩 된다.
이렇게 함수를 매핑하는 곳에는 왠만하면 closure가 있다.

Closure는 렉시컬 스코프에서 쓰이는 로컬변수의 상태라고 생각해보자.(블로거 생각)
이런 로컬 변수를 사용하는 경우를 보자.
(let ((counter 0))
  (defun new-id () (incf counter))  ;; incf 는 counter를 1 증가시킨다. 
  (defun reset-id () (setq counter 0)))
위의 두 함수를 counter 변수를 공유한다.
첫번째는 counter를 +1 하고 두번째 함수를 counter를 0으로 변경한다.
이렇게 두 함수가 같은 변수로 연결될 수 있다.
이처럼 local state와 함께 함수를 리턴하는 것은 아주 유용한 기술이다.
(defun make-adder (n)
  #'(lambda (x) (+ x n)))
이렇게 하면 free-variable n은 make-adder의 매개변수에 바인딩 되고 그 카피본과 함께 함수를 리턴된다.
리턴된 함수를 n을 가지고 있게 되는 것이다.
(setq add2 (make-adder 2)
function :LAMBDA (X) (+ X N)
(funcall add2 5)
7
위에서 make-adder에서 리턴된 내부 상태는 변경되지 않는다. 하지만 우리는 가능하게 할 수 있다.
(defun make-adderb (n)
  #'(lambda (x &optional change)
      (if change
       (setq n x)
    (+ x n))))
(setq addx (make-adderb 1))
(funcall addx 3) ;; 4
;; change optional
(funcall addx 100 t)
(funcall addx 3) ;; 103

클로저 덩어리를 리턴하는 것도 가능하다.
당연히 함수를 일반 객체처럼 쓸 수 있으니, 리스트에 넣어서 빼쓰는 거라든게 해시맵에 저장하는 거라든가 모든게 가능할 것이다.

[on lisp] 2.5 Scope 렉시컬 스코프

2.5 Scope

커먼리습은 lexically scoped Lisp이다. 스킴이라는 언어가 lexical scope를 가장 오래전부터 가진 언어다.
그전까지는 모두 dynamic scope를 고려하여 만들어졌었다.

lexical과 dynamic scope의 차이는 free variables를 어떻게 다룰 것인지를 구현한 방식에 따라 다르다.
아무것도 바인딩 되지 않은 symbol을 우리는 free하다고 한다.
(let ((y 7))
  (defun scope-test (x)
    (list x y)))
여기 defun 안에 있는 변수를 보자. x는 매개변수로 연결되어 있지만 y는 free하다.
free variable은 값이 뭐가 되야 할지 명확하지 않다.
bound variable(여기서는 x)에서는 불확실성이 없다. scope-test가 실행되면 x값은 인수로 전달될 것이다.
하지만 y는 어디에서 받는가? 이것이 스코프 규칙에 따라 달라진다.

dynamic scoped Lisp에서 free variable은 scope-test를 실행할 때 찾는다.
함수호출의 체인을 반대로 돌아보면서 확인한다. (콜스택을 확인한다는 말인듯)
(let ((y 5))
  (scope-test 3)) ;; (3 5)
이게 dynamic scope이다. 위에 y는 7로 연결되어 있지만, dynamic scope에서는 실행한 순간 바인딩 되어 있는
녀석을 보기 때문에 의미 없어지고 y=5로 바인딩 된다.

Lexically scoped Lisp에서는 함수호출체인을 뒤로 둘러보는 것이 아니라.
함수가 정의된 순간에 포함된 환경을 둘러본다. 그러면 y=7이 바인딩 될 것이다.
(let ((y 5))
  (scope-test 3)) ;; ( 3 7)
5를 설정한게 전혀 쓸모가 없어진 것이다.

이 기능은 다음 섹션에서 보여줄 새로운 프로그래밍 기술을 가능하게 해준다.(클로저)

[on lisp] 2.4 함수를 프로퍼티처럼 땡겨쓰기

2.4 Functions as Properties

동물의 유형에 따라 다른 행동을 하도록 만들어보자.
대부분 언어에서는 case 문을 이용할 것이다.
리스트에서도 가능하다.

(defun behave (animal)
  (case animal
    (dog (wag-tail)
      (bark))
 (rat (scurry)
   (squek))
 (cat (rub-legs)
   (scratch-carpet))))
여기 dog, rat, cat에 따른 일들을 정의했다.
만약 새로운 타입의 동물이 필요하다면? 케이스문을 나열할건가?
대부분 타입은 시간이 지나면 계속 추가된다. 이 책에서는 다른 방식을 추천한다.
이 방식은 어떨까?
(setf (get 'dog 'behavior)
      #'(lambda ()
       (wag-tail)
    (bark)))
이렇게 lookup테이블을 만드는데 거기에다가 함수를 담는 것이다.
실행은 어떻게 하는지 보자.
(defun behave (animal)
  (funcall (get animal 'behavior)))
이렇게 케이스문을 없애버렸다. 이렇게 하면 이제 behave함수는 변경될 이유가 없다.
lookup 테이블에 함수만 추가하면 된다.
보니까 저 위에 get 함수는 'dog 안에 behavior를 가져오는 것 같다.

[on lisp] 2.3 함수 매개변수

2.3 Functional Arguments
함수를 객체처럼 쓴다는 말은, 매개변수로 다른 함수에 던질 수도 있다는 말이다.
이 기능은 리스프의 상향식 프로그래밍에서 중요한 역할을 한다.

함수를 객체로 허용하는 언어는 이것을 호출하는 기능을 제공해야 한다.
일반적으로 "apply"라는 함수에 두개의 매개변수(함수, 매개변수리스트)를 보내서 실행한다.

(+ 1 2)
(apply #'+ '(1 2))
(apply (symbol-function '+) '(1 2))
(apply #'(lambda (x y) (+ x y)) '(1 2))

커먼 리스프에서는 첫번째 야규먼트가 리스트로 들어가고 뒤에 매개변수들이 cons로 뒤에 붙는다.
그래서 아래와 같은 것도 가능하다.
(apply #'+ 1 '(2))
이러면 3이 나온다.

만약 매개변수가 리스트로 들어오는 것이 맘에 들지 않는다면, funcall을 사용하면 된다.
(funcall #'+ 1 2)
많은 빌트인 함수들은 함수를 매개변수로 받는다.
가장 빈번하게 사용되는 곳은 함수매핑이다.
예를들어 mapcar는 2개 이상의 매개변수를 받는다.
함수와 하나 혹은 그 이상의 리스트들을 받아서 각 리스트 요소에 함수를 적용시킨다.
(mapcar #'(lambda (x) (+ x 10)) '(1 2 3))
(mapcar #'+
        '(1 2 3)
  '(10 100 1000))
sort, remove-if 기능도 마찬가지다.
(sort '(1 4 2 5 6 7 3) #'<)
(remove-if #'evenp '(1 2 3 4 5 6 7))
아래 remove-if 기능을 살펴보자.
(defun our-remove-if (fn lst)
  (if (null lst)
      nil
   (if (funcall fn (car lst))
       (our-remove-if fn (cdr lst))
    (cons (car lst) (our-remove-if fn (cdr lst))))))
여기서 주목해야 할 점은 fn에 #'(sharp-quote)가 없이 사용되고 있다는 점이다.
함수는 객체이고, 해당 변수는 일반 변수처럼 함수를 가질 수 있다.
그러니까 #'(sharp-quote)는 심볼(이름)에서 함수를 빼낼때 사용되는 것이다.

[on lisp] 2.2 defining function 커먼리스프의 #'

2.2 Defining Functions
(defun double (x) (* x 2))
이렇게 하면 만들어짐.

커먼리스프에서 심볼은 클로저와 다르게 하나의 심볼에 함수와 값이 동시에 저장될 수 있다.
(setq double 2)

(double double)
4
double에서 변수값, 함수를 따로 어떻게 구분지어 뽑아내려면
(symbol-value 'double)
(symbol-function 'double)
#'double 
하지만 함수도 또한 변수처럼 사용될 수 있다.
(setq x #'append)
(eq (symbol-value 'x) (symbol-function 'append))
T
내부적으로는 defun은 첫번째 매개변수(함수 이름)의 symbol-function의 자리에 이름없는 함수를 만들어서 연갈한다고 생각하면 된다.
(defun double (x) (* x 2))
(setf (symbol-function 'double)
  #'(lambda (x) (* x 2)))
대부분 심볼을 넘길 때, 함수임을 명확하게 하기 위해서 #'(sharp qoute)를 심볼 앞에 붙여서 넘긴다.