들어가면서
-
OnLisp 읽기
-
이 문서는 번역본이 아닌 코드 위주로 작성된 요약본입니다.
- 물론, OnLisp 내용을 전체 다 읽고 이해한다면 좋을 것입니다.
- 다만, 시간 절약상 OnLisp를 빠르게 훝어보고 싶은, 기억을 상기시키고 싶은 분들을 위해 작성되었습니다.
-
현재 작업중 WIP (Work In Progress) 인 문서입니다.
참고
- https://github.com/lisp-korea/onlisp
- lispworks's hyperspec
- cl-community-spec
- https://www.lurklurk.org/onlisp/onlisp.html
01. 확장 가능한 언어
- 리스프의 가장 특징적인 특성 중 하나는 작성 중인 프로그램에 맞게 조정할 수 있다는 점.
- 리스프 자체가 리스프 프로그램이며, 리스프 프로그램은 리스프 데이터 구조인 리스트로 표현할 수 있다.
1.1. 진화에 의한 디자인
-
프로그램을 작성하면서 계획을 세우는 능력은 두 가지 중대한 결과를 가져온다:
- 프로그램을 작성하는 데 걸리는 시간이 짧아진다.
- 계획을 세우면서 동시에 프로그램을 작성하면 주의 집중이 잘된다.
- 그리고 그렇게 만들어진 프로그램은 더 좋은 프로그램이 된다.
- 프로그램의 최종 디자인은 진화의 산물이기 때문이다.
- 최종 목표를 찾는 동안 잘못된 부분을 발견하면 그 자리에서 반드시 다시 작성한다는 원칙을 지키는 한, 최종적으로 완성된 프로그램은 미리 몇 주 동안 계획을 세웠을 때보다 훨씬 더 우아한 프로그램이 될 것이다.
- 프로그램을 작성하는 데 걸리는 시간이 짧아진다.
-
리스프의 가장 큰 위험은 리스프 자체가 사용자에게 악영향을 미칠 수 있다는 것
- 리스프를 한동안 사용하게 되면 프로그래밍 언어와 애플리케이션의 궁합에 너무 민감해져서 원래 사용하던 프로그래밍 언어로 돌아가도, 리스프만큼의 필요한 유연성을 얻지 못한다라는 생각에 갇히게 될 수 있다.
1.2. 상향식(Bottom-Up) 프로그래밍
패스
1.3. 확장 가능한 소프트웨어
패스
1.4. 리스프의 확장
리스프는 함수나 매크로를 정의하는 것만으로도 언어를 확장할 수 있다.
함수를 이용하여 확장하는 예제: map1-n
(mapcar #'1+ '(1 2 3 4 5 6 7 8 9 10))
;;=> (2 3 4 5 6 7 8 9 10 11)
(map1-n #'1+ 10)
;;=> (2 3 4 5 6 7 8 9 10 11)
(defun map1-n (fn n)
(mapa-b fn 1 n))
(defun mapa-b (fn a b &optional (step 1))
(do ((i a (+ i step))
(result nil))
((> i b) (nreverse result))
(push (funcall fn i) result)))
매크로를 이용하여 확장하는 예제: for
(do ((x 1 (1+ x)))
((> x 5))
(print x))
;; >> 1
;; >> 2
;; >> 3
;; >> 4
;; >> 5
;;=> NIL
(for (x 1 5)
(print x))
;; >> 1
;; >> 2
;; >> 3
;; >> 4
;; >> 5
;;=> NIL
(defmacro for ((var start stop) &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
1.5. 왜(혹은 언제) 리스프인가
패스
짚고 넘어가기
02 함수
2.1 데이터로서 함수
-
리스프 자체가 함수 집합
- 새로운 오퍼레이터를 추가할 수 있음.
-
리스프 함수는
- 런타임에 생성하고 반환 가능
- 인수로 전달가능
- 변수가 값으로 함수를 가질 수 있다.
- 함수단위나 파일 단위로 컴파일할 수 있다.
2.2 함수 정의
- 함수 정의 방법
- defun
- lambda
defun
으로 함수 정의하기
(defun my-double (x)
(* x 2))
#'my-double
;;=> #<Interpreted-Function C66ACE>
(my-double 3)
;;=> 6
lambda
로 함수 정의하기
#'(lambda (x) (* x 2))
;;=> #<Interpreted-Function C674CE>
((lambda (x) (* x 2)) 3)
;;=> 6
함수/변수는 다른 이름공간을 가지고 있다.
이름 공간 분리 | ex | |
---|---|---|
Lisp-1 | 분리를 안함. | Clojure |
Lisp-2 | 분리를 함. | Common Lisp |
(setq my-double 2)
;;=> 2
(my-double my-double)
;;=> 4
(symbol-value 'my-double)
;;=> 2
(symbol-function 'my-double)
;;=> #<Interpreted-Function C66ACE>
(setq x #'append)
;;=> #<Compiled-Function 46B4BE>
(eq (symbol-value 'x)
(symbol-function 'append))
;;=> T
(eq #'my-double (car (list #'my-double)))
;;=> T
defun
은 심볼을 함수 이름 공간에 추가한다.
(defun my-double (x)
(* x 2))
(setf (symbol-function 'my-double)
#'(lambda (x) (* x 2)))
2.3 인자로서의 함수
apply
사용법
(+ 1 2)
;;=> 3
(apply #'+ '(1 2))
;;=> 3
(apply (symbol-function '+) '(1 2))
;;=> 3
(apply #'(lambda (x y) (+ x y)) '(1 2))
;;=> 3
funcall
사용법
(apply #'+ 1 '(2))
;;=> 3
(funcall #'+ 1 2)
;;=> 3
mapcar
사용법
(mapcar #'(lambda (x) (+ x 10))
'(1 2 3))
;;=> (11 12 13)
(mapcar #'+
'(1 2 3)
'(10 100 1000))
;;=> (11 102 1003)
sort
사용법
(sort '(1 4 2 5 6 7 3) #'<)
;;=> (1 2 3 4 5 6 7)
remove-if
사용법
(remove-if #'evenp '(1 2 3 4 5 6 7))
;;=> (1 3 5 7)
(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))))))
2.4 속성으로서의 함수
(defun behave (animal)
(case animal
(dog
(wag-tail)
(bark))
(rat
(scurry)
(squeak))
(cat
(rub-legs)
(scratch-carpet))
(human
(speak))))
(defun wag-tail () (print "wag-tail"))
(defun bark () (print "bark"))
(defun scurry () (print "scurry"))
(defun squeak () (print "squeak"))
(defun rub-legs () (print "rub-legs"))
(defun scratch-carpet () (print "scratch-carpet"))
(defun speak () (print "speak"))
(behave 'dog)
;;>> "wag-tail"
;;>> "bark"
(behave 'rat)
;;>> "scurry"
;;>> "squeak"
(behave 'cat)
;;>> "rub-legs"
;;>> "scratch-carpet"
(behave 'human)
;;>> speak
(defun behave2 (animal)
(funcall (get animal 'behavior)))
(setf (get 'dog 'behavior)
#'(lambda ()
(wag-tail)
(bark)))
(behave2 'dog)
;;>> "wag-tail"
;;>> "bark"
(setf (get 'all 'behavior)
#'(lambda ()
(bark)
(scurry)
(scratch-carpet)))
(behave2 'all)
;;>> "bark"
;;>> "scurry"
;;>> "scratch-carpet"
2.5 범위(Scope)
scope-test함수에서 | ||
---|---|---|
binding | x | 매개변수 x와 바인딩되어있다. |
free variable | y | 스코프 환경에 따라 다르게됨 |
스코프 | y 의 값 | |
---|---|---|
다이나믹(dynamic) | 함수의 호출 체인을 거슬러 올라감 | 5 |
렉시컬(lexical) | 함수가 정의 된 시점의 환경을 거슬러 올라감 | 7 |
(let ((y 7))
(defun scope-test (x)
(list x y)))
;; 다이나믹 스코프
;; - y의 값: 함수 호출을 감싼 let으로 정의한 5.
(let ((y 5))
(scope-test 3))
;;=> (3 5)
;; 렉시컬 스코프 - Common Lisp 기본 설정
;; - y의 값: 앞서 defun시 감싼 let으로 정의한 7.
(let ((y 5))
(scope-test 3))
;;=> (3 7)
2.6 클로져(Closures)
함수에서 binding되지 않는 변수, 즉 free 변수가 있을 때, 그 변수를 포함하는 함수를 클로져라고 한다.
(defun list+ (lst n)
(mapcar #’(lambda (x) (+ x n))
lst))
(list+ ’(1 2 3) 10)
;;=> (11 12 13)
(let ((counter 0))
(defun new-id ()
(incf counter))
(defun reset-id ()
(setq counter 0)))
(defun make-adder (n)
#’(lambda (x) (+ x n)))
(setq add2 (make-adder 2)
add10 (make-adder 10))
(funcall add2 5)
;;=> 7
(funcall add10 3)
;;=> 13
(defun make-adderb (n)
#’(lambda (x &optional change)
(if change
(setq n x)
(+ x n))))
(setq addx (make-adderb 1))
(funcall addx 3)
;;=> 4
(funcall addx 100 t)
;;=> 100
(funcall addx 3)
;;=> 103
(defun make-dbms (db)
(list
#’(lambda (key)
(cdr (assoc key db)))
#’(lambda (key val)
(push (cons key val) db)
key)
#’(lambda (key)
(setf db (delete key db :key #’car))
key)))
(setq cities (make-dbms ’((boston . us) (paris . france))))
(funcall (car cities) ’boston)
;;=> US
(funcall (second cities) ’london ’england)
;;=> LONDON
(funcall (car cities) ’london)
;;=> ENGLAND
(defun lookup (key db)
(funcall (car db) key))
2.7 지역 함수
labels
사용법
(labels ((inc (x) (1+ x)))
(inc 3))
;;=> 4
2.8 꼬리 재귀(Tail-Recursion)
our-length
함수의 끝이 our-length
으로 끝나는게 아니라 1+
가 감싸는 형태로 되어있다. 이와 같은 형태는 꼬리 재귀 형태가 될 수 없다.
(defun our-length (lst)
(if (null lst)
0
(1+ (our-length (cdr lst)))))
our-find-if
는 our-find-if
로 끝나는 꼬리 재귀 형태이다.
(defun our-find-if (fn lst)
(if (funcall fn (car lst))
(car lst)
(our-find-if fn (cdr lst))))
our-length
는 labels
를 이용하여 내부에 꼬리 재귀 형태의 rec
함수를 정의하였다.
(defun our-length (lst)
(labels ((rec (lst acc)
(if (null lst)
acc
(rec (cdr lst) (1+ acc)))))
(rec lst 0)))
;; Common Lisp에서 꼬리 재귀 최적화를 디폴트가 아닌 경우, 다음과 같이 추가 선언이 필요하다.
(proclaim '(optimize speed))
;; 속도 향상을 위한 꼬리 재귀 + 타입 선언 예제.
;; 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)))
2.9 컴파일
compile
함수 사용하여, 함수를 컴파일compile-file
함수 사용하여, 파일을 컴파일
(defun foo (x)
(1+ x))
;;=> FOO
;; 컴파일 됐는지 여부 확인
(compiled-function-p #’foo)
;;=> NIL
(compile ’foo)
;;=> FOO
(compiled-function-p #’foo)
;;=> T
;; 익명함수 컴파일
(compile nil '(lambda (x) (+ x 2)))
;;=> #<Compiled-Function BF55BE>
;; 익명함수에 이름을 붙여 컴파일
(progn
(compile 'bar '(lambda (x) (* x 3)))
(compiled-function-p #’bar))
;;=> T
;; 렉시컬 환경에서는 컴파일이 안된다.
(let ((y 2))
(defun foo (x)
(+ x y)))
;; 함수를 리턴하는 함수를 컴파일하면, 리턴되는 함수도 컴파일이 되는걸 확인 할 수 있다.
(compile 'make-adder)
;;=> MAKE-ADDER
(compiled-function-p (make-adder 2))
;;=> T
;; 인라인 함수의 예제
(proclaim '(inline 50th))
(defun 50th (lst)
(nth 49 lst))
(defun foo (lst)
(+ (50th lst) 1) ;; => (+ (nth 49 lst) 1)
)
2.10 Functions from Lists
패스
짚고 넘어가기
- defun
- eq
- lambda
- symbol-value
- symbol-function
- lisp1 / lisp2
- setq
- mapcar
- funcall
- labels
- let
- case
- declare
- type
- proclaim
- inline
- optimize
- compiled-function-p
- compile
- copmile-file
- progn
03. 함수형 프로그래밍
3.1. 함수형 디자인
- 함수형 프로그래밍은 사이드 이펙트 없이, 값을 반환하는 함수를 조합하여 프로그램을 작성하는 것.
- 사이드 이펙트
- 사이드 이펙트라 함은 객체의 변경 (ex.
rplaca
) 및 변수의 할당의 사용(ex.setq
) 등이 있다. - 사이드 이펙트를 지닌 함수의 갯수가 적고 있더라도 그 영향의 범위가 좁아질 수록, 프로그램의 읽기, 테스트, 디버깅은 간단해진다.
- 사이드 이펙트라 함은 객체의 변경 (ex.
사이드이펙트를 불러일으키는 함수 |
---|
set |
setq |
setf |
psetf |
psetq |
incf |
decf |
push |
pop |
pushnew |
rplaca |
rplacd |
rotatef |
shiftf |
remf |
remprop |
remhash |
let* |
;;; Figure 3.1: A function to reverse lists.
;;; O(n^2)
(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 '(abc))
;;=> (ABC)
(bad-reverse lst)
;;=> NIL
lst
;;=> (CBA)
;;; Figure 3.2: A function to return reversed lists.
;;; O(n)
(defun good-reverse (lst)
(labels ((rev (lst acc)
(if (null lst)
acc
(rev (cdr lst) (cons (car lst) acc)))))
(rev lst nil)))
> (setq lst '(abc))
(ABC)
> (good-reverse lst)
(CBA)
> lst
(ABC)
nreverse
와 같이 사이드 이펙트가 필요한 경우 반환 값을 setq
이용해 대입한다.
> (setq lst '(abc))
(ABC)
> (nreverse lst)
(CBA)
> lst
(A)
- 다른 프로그래밍 언어에서 부작용을 사용하는 가장 큰 이유는 다중 값을 반환하는 함수가 필요하다는 것.
- 언어에서 하나의 값만 반환 할 수 있으면, 다중 값을 반환하기 위해 매개 변수를 활용하여 반환함.
- ex) c#에서 out 파라미터
- 다행히 Common Lisp에서는
values
를 이용, 다중 값을 반환할 수 있다.
- 언어에서 하나의 값만 반환 할 수 있으면, 다중 값을 반환하기 위해 매개 변수를 활용하여 반환함.
(defun powers (x)
(values x (sqrt x) (expt x 2)))
;;=> POWERS
(multiple-value-bind (base root square) (powers 4)
(list base root square))
;;=> (4 2.0 16)
(* (powers 4) 2)
;;=> 8
(truncate 26.21875)
;;=> 26
;;=> 0.21875
(= (truncate 26.21875) 26)
;;=> T
3.2. 명령형을 뒤집어보자
imperative-style
을 임시 변수를 없에면서 역순으로 뒤집어 구현해보면 functional-style
이 된다.
(defun imperative-style (x)
(let (y
sqr)
(setq y (car x))
(setq sqr (expt y 2))
(list 'a sqr)))
(defun functional-style (x)
(list 'a (expt (car x) 2)))
(functional-style '(3))
;;=> (A 9)
3.3. 함수형 인터페이스
함수는 따옴표 붙은 리스트를 반환해서는 안된다.
(defun bad-exclaim (expression)
(append expression '(oh my)))
(bad-exclaim '(lions and tigers and bears))
;;=> (LIONS AND TIGERS AND BEARS OH MY)
(nconc * '(goodness))
;;=> (LIONS AND TIGERS AND BEARS OH MY GOODNESS)
(bad-exclaim '(fixnums and bignums and floats))
;;=> (FIXNUMS AND BIGNUMS AND FLOATS OH MY GOODNESS)
(defun good-exclaim (expression)
(append expression (list 'oh 'my)))
(good-exclaim '(lions and tigers and bears))
;;=> (LIONS AND TIGERS AND BEARS OH MY)
(nconc * '(goodness))
;;=> (LIONS AND TIGERS AND BEARS OH MY GOODNESS)
(good-exclaim '(fixnums and bignums and floats))
;;=> (FIXNUMS AND BIGNUMS AND FLOATS OH MY)
3.4. 인터렉티브 프로그래밍
-
숙련 된 Lisp 프로그래머는 테스트하기 쉽도록 프로그램을 디자인한다.
- 사이드 이펙트를 사용하는 부분을 몇 가지 함수로 분리하고 프로그램의 대부분은 순수한 함수형 프로그래밍 스타일로 쓴다.
- 사이드 이펙트를 사용하는 것을 피할 수 없다면, 적어도 거기에 함수형 인터페이스를 포함하려고 한다.
- 하나의 함수에는 하나의 목적만.
-
소프트웨어 개발은 코드 작성과 테스트 사이클로 구성된다
- 리스프에서는 그 사이클이 매우 짧다.
짚고 넘어가기
- rotatef
- nreverse
- values
- multiple-value-bind
- expt
- nconc
04. 유틸리티 함수
- 오퍼레이터 종류
- 함수(function)
- 매크로(macro)
- 스페셜 폼(special form)
- 단, 스페셜 폼은 유저가 만들 수 없다.
4.1. 유틸리티의 탄생
- 유틸리티: 프로그램을 쉽게 쓸 수 있게 해주는 연산자.
- "유틸리티"라는 단어에 정확한 정의가 없음.
- 어플리케이션이라고 하기엔 작고, 일부분이라고 하기에는 너무 범용적인 경우 "유틸리티"라 칭함.
(defun nicknames (name)
(list 'nick (concatenate 'string "foo-" name)))
(nicknames "park")
;;=> (NICK "foo-park")
(setq names '("park" "jane" "june"))
;; all-nicknames 함수를 만들어도 되지만
(defun all-nicknames (names)
(if (null names)
nil
(nconc (nicknames (car names))
(all-nicknames (cdr names)))))
;; mapcan을 알고 있다면, all-nicknames 함수를 만들 필요가 없다.
(mapcan #'nicknames names)
;;=> (NICK "foo-park" NICK "foo-jane" NICK "foo-june")
(mapcan #'reverse '((2 1 0) (5 4 3)))
;; (0 1 2 3 4 5)
(defun find-books (towns)
(let ((town (find-if #'bookshops towns))) ;; bookshops 함수 호출
(values town (bookshops town)))) ;; bookshops 함수 호출
(find-books1 towns)
(defun find-books2 (towns)
(if (null towns)
nil
(let ((shops (bookshops (car towns)))) ;; bookshops 함수 호출
(if shops
(values (car towns) shops)
(find-books2 (cdr towns))))))
(find-books2 towns)
(defun find2 (fn lst)
(if (null lst)
nil
(let ((val (funcall fn (car lst)))) ;; fn 함수 호출
(if val
(values (car lst) val)
(find2 fn (cdr lst))))))
(find2 #'bookshops towns)
4.2. 추상화에 투자하라
프로그램을 작성하고 유지하는 데 드는 비용은 프로그램이 길어짐에 따라 증가한다.
- 유틸리티
- 유틸리티는 당면한 문제뿐만 아니라 일반적인 상황에 대해 작성해야 한다.
- 서둘러 작성해서는 안 된다.
- 나중에 필요할지 확실하지 않을 때는, 일단 작성해 본다.
- 단, 아직은 유틸리티가 아닌 서브 루틴 신분이다.(해당 프로그램에서만 사용)
- 다른 프로그램에서 그 서브 루틴을 사용할 일이 생기면, 유틸리티로 승격시켜 널리 사용할 수 있도록 한다.
4.3. 리스트에 대한 연산
리스프(Lisp)의 이름은 LIS
t P
rocessing에서 따왔다.
longer
함수의 예:
(defun longer (x y)
(labels ((compare (x y)
(and (consp x)
(or (null y)
(compare (cdr x) (cdr y))))))
(if (and (listp x) (listp y))
(compare x y)
(> (length x) (length y)))))
(longer '(1 2 3) '(4 5))
;;=> T
(longer '(1 2) '(3 4 5))
;;=> NIL
filter
함수의 예:
(defun filter (fn lst)
(let ((acc nil))
(dolist (x lst)
(let ((val (funcall fn x)))
(if val
(push val acc))))
(nreverse acc)))
(filter #'evenp '(1 2 3 4 5))
;;=> (T T)
(filter #'(lambda (x) (if (numberp x) (1+ x)))
'(a 1 2 b 3 c d 4))
;;=> (2 3 4 5)
group
함수의 예:
(defun group (source n)
(if (zerop n)
(error "zero length"))
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons (subseq source 0 n) acc))
(nreverse (cons source acc))))))
(if source
(rec source nil)
nil)))
(group '(a b c d e f g) 2)
;;=> ((A B) (C D) (E F) (G))
flatten
함수의 예:
(defun flatten (x)
(labels ((rec (x acc)
(cond ((null x) acc)
((atom x) (cons x acc))
(t (rec (car x) (rec (cdr x) acc))))))
(rec x nil)))
(flatten '(a (b c) ((d e) f)))
;;=> (A B C D E F)
prune
함수의 예:
(defun prune (test tree)
(labels ((rec (tree acc)
(cond ((null tree)
(nreverse acc))
((consp (car tree))
(rec (cdr tree)
(cons (rec (car tree) nil) acc)))
(t
(rec (cdr tree)
(if (funcall test (car tree))
acc
(cons (car tree) acc)))))))
(rec tree nil)))
(prune #'evenp '(1 2 (3 (4 5) 6) 7 8 (9)))
;;=> (1 (3 (5)) 7 (9))
4.4. 검색
(defun before (x y lst &key (test #'eql))
(and lst
(let ((first (car lst)))
(cond ((funcall test y first)
nil)
((funcall test x first)
lst)
(t
(before x y (cdr lst) :test test))))))
(before 'a 'b '(a))
;;=> (A)
(defun after (x y lst &key (test #'eql))
(let ((rest (before y x lst :test test)))
(and rest (member x rest :test test))))
(after 'a 'b '(b a d))
;;=> (A D)
(after 'a 'b '(a))
;;=> NIL
(defun duplicate (obj lst &key (test #'eql))
(member obj
(cdr (member obj lst :test test))
:test test))
(duplicate 'a '(a b c a d))
;;=> (A D)
(defun split-if (fn lst)
(let ((acc nil))
(do ((src lst (cdr src)))
((or (null src) (funcall fn (car src)))
(values (nreverse acc) src))
(push (car src) acc))))
(split-if #'(lambda (x) (> x 3))
'(1 2 3 4 5))
;;=> (1 2 3)
;;=> (4 5)
(defun best (fn lst)
(if (null lst)
nil
(let ((wins (car lst)))
(dolist (obj (cdr lst))
(if (funcall fn obj wins)
(setq wins obj)))
wins)))
(best #'> '(1 2 3 4 5))
;;=> 5
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setq wins obj)
(setq max score))))
(values wins max))))
(most #'length '((a b) (a b c) (a) (e f g)))
;;=> (A B C)
;;=> 3
(defun mostn (fn lst)
(if (null lst)
(values nil nil)
(let ((result (list (car lst)))
(max (funcall fn (car lst))))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(cond ((> score max)
(setq max score)
(setq result (list obj)))
((= score max)
(push obj result)))))
(values (nreverse result) max))))
(mostn #'length '((a b) (a b c) (a) (e f g)))
;;=> ((A B C) (E F G))
;;=> 3
4.5. 맵핑
(defun mapa-b (fn a b &optional (step 1))
(map-> fn
a
#'(lambda (x) (> x b))
#'(lambda (x) (+ x step))))
(defun map0-n (fn n)
(mapa-b fn 0 n))
(map0-n #'1+ 5)
;;=> (1 2 3 4 5 6)
(defun map1-n (fn n)
(mapa-b fn 1 n))
(mapa-b #'1+ -2 0 .5)
;;=> (-1 -0.5 0.0 0.5 1.0)
(defun mapa-b (fn a b &optional (step 1))
(do ((i a (+ i step))
(result nil))
((> i b) (nreverse result))
(push (funcall fn i) result)))
(defun map-> (fn start test-fn succ-fn)
(do ((i start (funcall succ-fn i))
(result nil))
((funcall test-fn i) (nreverse result))
(push (funcall fn i) result)))
(defun mappend (fn &rest lsts)
(apply #'append (apply #'mapcar fn lsts)))
(defun mapcars (fn &rest lsts)
(let ((result nil))
(dolist (lst lsts)
(dolist (obj lst)
(push (funcall fn obj) result)))
(nreverse result)))
(defun recur-mapcar (fn &rest args)
(if (some #'atom args)
(apply fn args)
(apply #'mapcar
#'(lambda (&rest args)
(apply #'recur-mapcar fn args))
args)))
(defun our-mapcan (fn &rest lsts)
(apply #'nconc (apply #'mapcar fn lsts)))
(recur-mapcar #'princ '(1 2 (3 4 (5) 6) 7 (8 9)))
;;>> 123456789
;;=> (1 2 (3 4 (5) 6) 7 (8 9))
(recur-mapcar #'+ '(1 (2 (3) 4)) '(10 (20 (30) 40)))
;;=> (11 (22 (33) 44))
4.6. I/O
(defun readlist (&rest args)
(values (read-from-string
(concatenate 'string "(" (apply #'read-line args) ")"))))
(readlist)
;;<< Call me "Ed"
;;=> (CALL ME "Ed")
(defun prompt (&rest args)
(apply #'format *query-io* args)
(read *query-io*))
(prompt "Enter a number between ~A and ~A.~%>> " 1 10)
;;>> Enter a number between 1 and 10.
;;<< 3
;;=> 3
(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))))))
(break-loop #'eval #'(lambda (x) (eq x :q)) ">> ")
;;>> Entering break-loop.
;;>> >>
;;<< (+ 2 3)
;;>> 5
;;>> >>
;;<< :q
;;=> NIL
4.7. 심볼과 문자열
(defun mkstr (&rest args)
(with-output-to-string (s)
(dolist (a args)
(princ a s))))
(mkstr pi " pieces of " 'pi)
;;=> "3.141592653589793d0 pieces of PI"
(defun symb (&rest args)
(values (intern (apply #'mkstr args))))
(symb 'ar "Madi" #\L #\L 0)
;;=> |ARMadiLL0|
(defun reread (&rest args)
(values (read-from-string (apply #'mkstr args))))
(reread 'a 'b "c")
;;=> ABC
(defun explode (sym)
(map 'list #'(lambda (c)
(intern (make-string 1 :initial-element c)))
(symbol-name sym)))
(explode 'bomb)
;;=> (B O M B)
4.8. 밀도
-
상향식 프로그램을 읽으려면 정의된 새로운 유틸리티를 모두 이해해야 한다.
- 이해하는데 걸린 시간은, 유틸리티 없는 경우에 비해 적을것이다.
- 유틸리티를 사용해서 코드가 읽는 게 어렵다고 말하는 사람들이 있다면, 그 사람들은 유틸리티를 사용하지 않으면 코드가 어떤 식이 될지 이해하지 못하는 사람들이다.
-
의도적으로 유틸리티를 피하는 경우가 하나 있다.
- 나머지 코드와 독립적으로 배포할 작은 프로그램을 작성해야 하는 경우.
- 소규모 프로그램에서는 유틸리티로 만들만큼 충분히 사용되지 않을 수 있다.
- 나머지 코드와 독립적으로 배포할 작은 프로그램을 작성해야 하는 경우.
짚고 넘어가기
- find-if
- mapcan
- push
- zerop
- nreverse
- subseq
- nthcdr
- dolist
- do
05. 함수를 반환하기
새로운 함수를 생성하고 반환하는 함수를 정의함으로써, 함수를 인자로 받는 유틸리티의 효과를 증폭시킬 수 있다.
5.1. 진화하는 Common Lisp
- CLTL2에서는
complement
라는 함수가 추가되었다.- 함수의 여함수(Complement Function)를 반환하는 함수.
- 초기 Common Lisp에는
remove-if
와remove-if-not
같이 쌍을 이루는 함수들이 있었다. - CLTL2에 와서는 결과적으로
-if-not
함수들이 사라지게 되었다.
(defun sample-complement (fn)
#'(lambda (&rest args)
(not (apply fn args))))
(remove-if (sample-complement #'oddp) '(1 2 3 4 5 6))
;;=> (1 3 5)
5.2. 직교성(Orthogonality)
-
직교성(orthogonal)을 지닌 프로그래밍 언어는 적은 수의 오퍼레이터를 다양한 방법으로 결합시킴으로써 여러가지 의미를 표현할 수 있다.
-
setf
매크로는 리스프의 직교성을 향상시킨다.get-color
,set-color
와 같은 함수들이 필요한게 아닌,get
과setf
를 조합하여 동일한 효과를 얻는다.
(setf (get 'ball 'color) 'red)
(symbol-plist 'ball)
;;=> (COLOR RED)
(get 'ball 'color)
;;=> RED
새로운 표현만들기 def!
와 !
:
(defvar *!equivs* (make-hash-table))
(defun ! (fn)
(or (gethash fn *!equivs*) fn))
(defun def! (fn fn!)
(setf (gethash fn *!equivs*) fn!))
(def! #'remove-if #'delete-if)
(delete-if #'oddp '(1 2 3 4))
;;=> (2 4)
(funcall (! #'remove-if) #'oddp '(1 2 3 4))
;;=> (2 4)
5.3. 메모이징(Memoizing)
캐쉬(cache)를 만들어서, 함수의 결과를 저장해두고, 같은 인자로 호출될 때는 캐쉬에 저장된 값을 반환하는 방식.
(defun memoize (fn)
(let ((cache (make-hash-table :test #'equal)))
#'(lambda (&rest args)
(multiple-value-bind (val win) (gethash args cache)
(if win
val
(setf (gethash args cache)
(apply fn args)))))))
(setq slowid (memoize #'(lambda (x) (sleep 5) x)))
(time (funcall slowid 1))
;;>> Elapsed Time = 5.15 seconds
;;=> 1
(time (funcall slowid 1))
;;>> Elapsed Time = 0.00 seconds
;;=> 1
5.4. 합성 함수(Composing Functions)
- 함수
f
의 여함수(Complement Function)는~f
로 표시.
(funcall (complement #'evenp) 1)
;;=> T
- 함수
f
와g
가 있을때 합성함수(Composing Function)f(g(x))
는f ○ g
로 표시.
(defun compose (&rest fns)
(if fns
(let ((fn1 (car (last fns)))
(fns (butlast fns)))
#'(lambda (&rest args)
(reduce #'funcall fns
:from-end t
:initial-value (apply fn1 args))))
#'identity))
(funcall (compose #'1+ #'find-if) #'oddp '(2 3 4))
;;=> 4
fif
f
unctionif
if
만족시then
아니면else
적용
(defun fif (if then &optional else)
#'(lambda (x)
(if (funcall if x)
(funcall then x)
(if else (funcall else x)))))
(mapcar (fif #'oddp #'1+ #'1-)
'(1 2 3 4 5))
;;=> (2 1 4 3 6)
fint
f
unctionint
ersection- 함수들을 모두 만족시
(defun fint (fn &rest fns)
(if (null fns)
fn
(let ((chain (apply #'fint fns)))
#'(lambda (x)
(and (funcall fn x) (funcall chain x))))))
(mapcar (fint #'oddp #'(lambda (x) (> x 3)))
'(1 2 3 4 5))
;; (NIL NIL NIL NIL T)
fun
f
unctionun
ion- 함수들 중 하나라도 만족시
(defun fun (fn &rest fns)
(if (null fns)
fn
(let ((chain (apply #'fun fns)))
#'(lambda (x)
(or (funcall fn x) (funcall chain x))))))
(mapcar (fun #'oddp #'(lambda (x) (> x 3)))
'(1 2 3 4 5))
;; (T NIL T T T)
5.5. cdr을 이용한 재귀
cdr
은 리스트의 두번째 요소부터 끝까지를 반환하는 함수.car
와cdr
을 이용하여 전체 리스트를 순회할 수 있다.
(cdr '(1 2 3 4))
;;=> (2 3 4)
(defun our-length (lst)
(if (null lst)
0
(1+ (our-length (cdr lst)))))
(our-length '(1 2 3))
;;=> 3
(defun our-every (fn lst)
(if (null lst)
t
(and (funcall fn (car lst))
(our-every fn (cdr lst)))))
(our-every #'evenp '(2 4 6))
;;=> T
- lrec
l
istrec
urser- 리스트에 대한 재귀를 추상화한 함수
- 꼬리 재귀(tail recursion)를 사용하여 최적화한 버전이 아니지만, 간단히 구현해 본 것.
(defun lrec (rec &optional base)
(labels ((self (lst)
(if (null lst)
(if (functionp base)
(funcall base)
base)
(funcall rec (car lst)
#'(lambda ()
(self (cdr lst)))))))
#'self))
;; 리스트 복사
(lrec #'(lambda (x f) (cons x (funcall f))))
;; 중복 삭제
(lrec #'(lambda (x f) (adjoin x (funcall f))))
;; find-if, fn을 만족시키는 x 찾기
(lrec #'(lambda (x f) (if (fn x) x (funcall f))))
; some, fn을 적용시켜 거짓이 아니면 반환
(lrec #'(lambda (x f) (or (fn x) (funcall f))))
5.6. 서브트리(subtree)와 재귀
표현식 | cons cell |
---|---|
(a . b) | (a . b) |
(a b c) | (a . (b . (c . nil))) |
(a b (c d)) | (a . (b . ((c . (d . nil)) . nil))) |
our-copy-tree
예제:
(defun our-copy-tree (tree)
(if (atom tree)
tree
(cons (our-copy-tree (car tree))
(if (cdr tree) (our-copy-tree (cdr tree))))))
(our-copy-tree '((a b (c d)) (e) f))
;;=> ((A B (C D)) (E) F)
count-leaves
예제:
(defun count-leaves (tree)
(if (atom tree)
1
(+ (count-leaves (car tree))
(or (if (cdr tree) (count-leaves (cdr tree)))
1))))
(count-leaves '((a b (c d)) (e) f))
;;=> 10
our-flatten
예제:
(defun our-flatten (tree)
(if (atom tree)
(mklist tree)
(nconc (flatten (car tree))
(if (cdr tree) (our-flatten (cdr tree))))))
(our-flatten '((a b (c d)) (e) f))
;;=> (A B C D E F)
rfind-if
예제:
(defun rfind-if (fn tree)
(if (atom tree)
(and (funcall fn tree) tree)
(or (rfind-if fn (car tree))
(if (cdr tree) (rfind-if fn (cdr tree))))))
(rfind-if (fint #'numberp #'oddp) '(2 (3 4) 5))
;;=> 3
5.7. 어느 시점에 함수를 만들어야 하는가
#'(lambda ... )
는 상수 표현식이나, 함수 호출은런타임에 평가
된다.#.( ... )
에서의#.
은 리드 매크로(read macro)로 뒷따르는표현식을 읽는 시점에 평가
된다.
짚고 넘어가기
06. 표현으로서의 함수
- 일반적으로 데이터 구조는 무언가를 표현하는 데 사용됨.
- 배열은 기하학적 변환을 나타낼 수 있음.
- 트리는 명령의 계층을 표현할 수 있음.
- 그래프는 철도 네트워크를 나타낼 수 있음.
- 리스프에서는 클로져(clozure)가 표현 수단에 사용될 수 있음.
- 클로저 내에서 변수 바인딩은 정보를 저장할 수 있으며 복잡한 데이터 구조를 구축 할 때 포인터의 역할을 할 수 있음.
- 바인딩을 공유하거나 서로를 참조 할 수있는 클로저 그룹을 만들어 데이터 구조와 프로그램의 장점을 결합한 하이브리드 객체를 만들 수 있다.
6.1. 네트워크(network)
네트워크(network): 노드(node)로 연결된 집합체
- 클로저에는 세 가지 유용한 특성이 있다.
- 활성화 되어있다.(they are active)
- 로컬 상태를 가진다.(they have local state)
- 여러 인스턴스를 만들 수 있다.(we can make multiple instances of them)
6.2. 네트워크 컴파일
6.3. 앞으로
짚고 넘어가기
- defstruct
07. 매크로
매크로
는 코드를 만드는 함수
7.1. 매크로가 동작하는 방식
- 정의한 표현식을 빌드한 다음
- 매크로 호출시 대신 해당 표현식을 평가
(defmacro nil! (var)
(list 'setq var nil))
(defvar x 10)
;; 새로운 표현식을 빌딩하는 단계를 `매크로 확장(macroexpansion)` 이라고 한다.
;; 아래 매크로 호출은, 매크로 확장을 거쳐, (setq x nil)이 된다.
(nil! x)
x
;;=> NIL
7.2. 역 따옴표(Backquote)
- 따옴표
'
방향을 역으로 한 역 따옴표`
- 백쿼트(backquote) 혹은 백틱(backtick)라고도 하는데 리스프에서는 백쿼트라는 말을 쓴다.
(defvar a 1)
(defvar b '(2 2))
(defvar c 3)
(defvar d 4)
;; `(a b c) == '(a b c) == (list 'a 'b 'c)
;; `(a ,b c) == '(a (2 2) c)
;; `(a ,@b c) == (list 'a 2 2 'c)
`(a ,b c)
;;=> (A (2 2) C)
`(a ,@b c)
;;=> (A 2 2 C)
;; list를 쓴 버전
(defmacro nil! (var)
(list 'setq var nil))
;; 백쿼트(`)를 쓴 버전
(defmacro nil! (var)
`(setq ,var nil))
;; list를 쓴 버전
(defmacro nif (expr pos zero neg)
(list 'case
(list 'truncate (list 'signum expr))
(list 1 pos)
(list 0 zero)
(list -1 neg)))
;; 백쿼트(`)를 쓴 버전
(defmacro nif (expr pos zero neg)
`(case (truncate (signum ,expr))
(1 ,pos)
(0 ,zero)
(-1 ,neg)))
(mapcar #'(lambda (x) (nif x 'pos 'zero 'neg))
'(0 2.5 -8))
;;=> (ZERO POS NEG)
7.3. 간단한 매크로 정의
(defmacro memq (obj lst)
`(member ,obj ,lst :test #'eq))
(memq 'a '(1 2 3)) ; 확장 (member 'a '(1 2 3) :test #'eq)
;;=> NIL
(memq 'a '(a b c)) ; 확장 (member 'a '(a b c) :test #'eq)
;;=> (A B C)
7.4. 매크로 확장(Macroexpansion) 확인
(defmacro while (test &body body)
`(do ()
((not ,test))
,@body))
(let ((i 0))
(while (< i 3)
(print i)
(incf i)))
;;>> 1
;;>> 2
;;>> 3
;;=> NIL
macroexpand
, macroexpand-1
함수를 사용하면 매크로 확장 결과를 확인할 수 있다.
(pprint (macroexpand `(while (able) (laugh))))
;;>> (BLOCK NIL
;;>> (LET ()
;;>> (DECLARE (IGNORABLE))
;;>> (TAGBODY
;;>> (GO #:G486)
;;>> #:G485
;;>> (TAGBODY (LAUGH))
;;>> (PSETQ)
;;>> #:G486
;;>> (UNLESS (NOT (ABLE)) (GO #:G485))
;;>> (RETURN-FROM NIL (PROGN)))))
(pprint (macroexpand-1 `(while (able) (laugh))))
;;>> (DO () ((NOT (ABLE))) (LAUGH))
(defmacro mac (expr)
`(pprint (macroexpand-1 ',expr)))
(mac (while (able) (laugh)))
;;>> (DO () ((NOT (ABLE))) (LAUGH))
7.5. 인자 구조화된 할당
destructuring-bind
(destructuring-bind (x (y) . z) '(a (b) c d)
(list x y z))
;;=> (A B (C D))
(defmacro our-dolist ((var list &optional result) &body body)
`(progn
(mapc #'(lambda (,var) ,@body)
,list)
(let ((,var nil))
,result)))
(our-dolist (x '(a b c))
(print x))
;;>> A
;;>> B
;;>> C
;;=> NIL
(defmacro when-let ((var expr) &body body)
`(let ((,var ,expr))
(when ,var
,@body)))
(when-let (a 1)
(1+ a))
;;=> 2
(when-let (a nil)
(1+ a))
;;=> NIL
;; Figure 7.6: A sketch of defmacro.
(defmacro our-expander (name)
`(get ,name 'expander))
(defmacro our-defmacro (name parms &body body)
(let ((g (gensym)))
`(progn
(setf (our-expander ',name)
#'(lambda (,g)
(block ,name
(destructuring-bind ,parms (cdr ,g)
,@body))))
',name)))
(defun our-macroexpand-1 (expr)
(if (and (consp expr) (our-expander (car expr)))
(funcall (our-expander (car expr)) expr)
expr))
(our-defmacro hello (a)
`(1+ ,a))
(our-macroexpand-1 '(hello 10))
;;=> (1+ 10)
7.6. 매크로 모델
패스
7.7. 프로그램으로서의 매크로
setq
와 psetq
(p
arallel setq
)의 차이
(let ((a 1))
(setq a 2 b a)
(list a b))
;;=> (2 2)
(let ((a 1))
(psetq a 2 b a)
(list a b))
;;=> (2 1)
;; Figure 7.8: Implementing do.
(defmacro our-do (bindforms (test &rest result) &body body)
(let ((label (gensym)))
`(prog ,(make-initforms bindforms)
,label
(if ,test
(return (progn ,@result)))
,@body
(psetq ,@(make-stepforms bindforms))
(go ,label))))
(defun make-initforms (bindforms)
(mapcar #'(lambda (b)
(if (consp b)
(list (car b) (cadr b))
(list b nil)))
bindforms))
(defun make-stepforms (bindforms)
(mapcan #'(lambda (b)
(if (and (consp b) (third b))
(list (car b) (third b))
nil))
bindforms))
(do ((w 3)
(x 1 (1+ x))
(y 2 (1+ y))
(z))
((> x 10) (princ z) y)
(princ x)
(princ y))
;;>> 12233445566778899101011NIL
;;=> 12
(our-do ((w 3)
(x 1 (1+ x))
(y 2 (1+ y))
(z))
((> x 10) (princ z) y)
(princ x)
(princ y))
;;>> 12233445566778899101011NIL
;;=> 12
7.8. 매크로 스타일
;; Figure 7.9: Two macros equivalent to and.
(defmacro our-and-1 (&rest args)
(case (length args)
(0 t)
(1 (car args))
(t `(if ,(car args)
(our-and-1 ,@(cdr args))))))
(defmacro our-and-2 (&rest args)
(if (null args)
t
(labels ((expander (rest)
(if (cdr rest)
`(if ,(car rest)
,(expander (cdr rest)))
(car rest))))
(expander args))))
매크로는 확장되기 전에 실재 구현이 숨겨져 있으므로, 성능에 영향을 주는 코드의 영향력을 파악하기 어렵다.
7.9. 매크로 의존성
(defmacro mac (x)
`(1+ ,x))
(setq fn (compile nil '(lambda (y) (mac y))))
(defmacro mac (x)
`(+ ,x 100))
(funcall fn 1)
;;=> 2
- 어떠한
A
매크로를 정의한 후,A
매크로를 호출하는 함수(또는 매크로)를 정의한다. - 어떠한
A
매크로를 수정했다면, 직접 또는 다른 매크로를 통해 이를 호출하는 모든 함수(또는 매크로)도 다시 컴파일한다.
7.10. 함수에서 매크로로
(defun sum (&rest args)
(apply #'+ args))
(sum 1 2 3)
;;=> 6
(defmacro sum-1 (&rest args)
`(apply #'+ (list ,@args)))
(macroexpand-1 '(sum-1 1 2 3))
;;=> (APPLY #'+ (LIST 1 2 3))
;;=> T
(defmacro sum-2 (&rest args)
`(+ ,@args))
(macroexpand-1 '(sum-2 1 2 3))
;;=> (+ 1 2 3)
;;=> T
(defun foo (x y z)
(list x (let ((x y))
(list x z))))
(foo 1 2 3)
;;=> (1 (2 3))
(defmacro foo-macro (x y z)
`(list ,x (let ((x ,y))
(list x ,z))))
(foo-macro 1 2 3)
;;=> (1 (2 3))
7.11. 심볼 매크로
- CLTL2는 커먼 리스프에 새로운 종류의 매크로인 심볼 매크로(symbol-macro)를 도입했다.
종류 | 정의 | 호출 출 |
---|---|---|
일반 매크로 | defmacro | 함수처럼 호출 |
심볼 매크로 | symbol-macrolet | 심볼처럼 호출 |
(symbol-macrolet ((hi (progn (print "Howdy")
1)))
(+ hi 2))
;;>> "Howdy"
;;=> 3
18장에서 심볼 매크로를 사용하는 방법을 설명한다.
짚고 넘어가기
- defmacro
'
`
,
,@
- macroexpand
- macroexpand-1
- symbol-macrolet
08. 언제 매크로를 사용하는가
8.1. When Nothing Else Will Do
8.2. 매크로 혹은 함수?
8.3. Applications for Macros
짚고 넘어가기
09. 변수 캡쳐(Variable Capture)
9.1. Macro Argument Capture
9.2. Free Symbol Capture
9.3. 언제 캡쳐가 일어나는가
9.4. Avoiding Capture with Better Names
9.5. Avoiding Capture by Prior Evaluation
9.6. Avoiding Capture with Gensyms
9.7. Avoiding Capture with Packages
9.8. Capture in Other Name-Spaces
9.9. Why Bother?
짚고 넘어가기
10. 또 다른 매크로의 위험성
10.1. 평가 횟수
;;; Figure 10.1: Controlling argument evaluation.
;; 올바른 버전
(defmacro for-correct ((var start stop) &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
;; 여러 번 평가:
(defmacro for-eval ((var start stop) &body body)
`(do ((,var ,start (1+ ,var))) ; 올바른 버전에 비해, gensym을 사용하지 않았다.
((> ,var ,stop))
,@body))
;; 옳지 않은 평가 순서:
(defmacro for-order ((var start stop) &body body)
(let ((gstop (gensym)))
`(do ((,gstop ,stop)
(,var ,start (1+ ,var))) ; 올바른 버전에 비해, 1+ 가 아래에 있다
((> ,var ,gstop))
,@body)))
10.2. 평가 순서
(setq x 10)
(+ (setq x 3) x)
;;=> 6
(let ((x 1))
(for-order (i x (setq x 13))
(princ i)))
;;>> 13
;;=> NIL
(let ((x 1))
(for-correct (i x (setq x 13))
(princ i)))
;;>> 12345678910111213
;;=> NIL