Common Lisp로 구구단을 외자
삶에 지쳤다면 구구단이나 외며 시간을 보내자.
별로 어려운 일이 아닐것이다... Common Lisp과 함께한다면.
1단계: mult-table-1
파라미터 level에 해당하는 곱셈 표를 출력하는 mult-table-1
함수를 만들자. 아래와 같은 출력을 갖는다.
CL-USER> (mult-table-1 8)
8x1=8
8x2=16
8x3=24
8x4=32
8x5=40
8x6=48
8x7=56
8x8=64
8x9=72
우선 곱셈표의 한 줄을 어떻게 만들지 정의하는 mult-table-row
함수부터 정의할 것이다.
;;; Example:
;; CL-USER> (mult-table-row 8 2)
;; 8x2=16
(defun mult-table-row (level mult)
(format t "~dx~d=~d~%"
level mult (* level mult)))
format
함수의 포맷 스트링은 C/C++/Java에 있는 것과 유사한 면이 있다. 현재 보이는 중 다른 점이라면 ~S
형태로 형식 지정자가 나타난다. 그리고 ~%
형식 지정자는 개행을 나타낸다. 처음에는 외우기에 아주 골이 아프다고 생각했는데, 결국 익숙함의 문제다. 익숙해지면 별거 아니다.
이제 이를 바탕으로, 곱셈표를 출력하는 반복 프로세스를 표현해야 한다. 이 때 두 가지 길이 있다. 바로 재귀와 루프다. 만약 당신이 재귀를 사랑하는 경우,
(defun mult-table-1 (level)
(mult-table-iter level 1))
(defun mult-table-iter (level mult)
(when (<= mult 9)
(mult-table-row level mult)
(mult-table-iter level (1+ mult))))
이렇게 짤 수도 있겠지만, 곱셈표를 출력하는 일에 재귀를 이용한 반복 표현은 그다지 어울리지 않는다. 곱셈표를 출력하는 일이 절차적으로 정의되어 있기 때문이다.
그럴 때는 LOOP 매크로를 쓴다. LOOP는 절차형의 반복을 탁월하게 표현하는 DSLDomain Specific Language를 제공하므로 읽고 쓰기에 아주 쉽다. 재귀 방식에서 보였던 *-iter
과 같은 재귀반복용 헬퍼 함수도 필요치 않다.
다음은 같은 기능을 하지만, loop
매크로로 구현된 mult-table-1
함수이다.
(defun mult-table-1 (level)
(loop for mult from 1 to 9 do
(mult-table-row level mult)))
LOOP FOR var FROM start TO end
는 var
이라는 렉시컬 변수에 start
부터 end
까지 값을 대입하면서 반복하겠다는 뜻이다. DO form
는 반복하는 동안 form
을 실행하라는 뜻이다.
이렇게 1단계를 통과했다.
2단계: mult-table
1단부터 9단까지 모든 곱셈 표를 출력하는 mult-table
함수를 만든다.
CL-USER> (mult-table)
1x1=1
1x2=2
1x3=3
...(중략)...
9x7=63
9x8=72
9x9=81
1단계 에서 만든 mult-table-1
함수를 그대로 이용하면 아주 쉽게 해결할 수 있다.
(defun mult-table ()
(loop for level from 1 to 9 do
(mult-table-1 level)
(princ #\Newline)))
3단계: 삼렬 종대
일렬 종대로 주욱 출력되니 보기에 여간 불편한게 아니다. 삼렬 종대로 출력되도록 하자.
CL-USER> (mult-table)
| 1x1= 1 | 2x1= 2 | 3x1= 3 |
| 1x2= 2 | 2x2= 4 | 3x2= 6 |
| 1x3= 3 | 2x3= 6 | 3x3= 9 |
...(중략)...
| 7x7=49 | 8x7=56 | 9x7=63 |
| 7x8=56 | 8x8=64 | 9x8=72 |
| 7x9=63 | 8x9=72 | 9x9=81 |
mult-table-row
함수를 수정하자. 한 행에 세개의 단을 출력할 것이다.
(defun mult-table-row (min-level mult)
(format t "|~{ ~dx~d=~2d |~}~%"
(loop repeat 3
for level upfrom min-level
append `(,level ,mult ,(* level mult)))))
아주 복잡해보이지만, 별거 아니다. mult-table-row
함수의 첫째 줄부터 하나 하나 뜯어보자.
~{ ~}
형식 지정자는 인자로 주어진 리스트에서 필요한 만큼 값을 가져오란 뜻이다. 예컨대:
(format t "~{~a: ~a~%~}"
'(title "The Great Escape"
artist "Boys Like Girls"
genre "Pop Punk"))
위 표현식을 실행한 결과는 아래와 같다.
TITLE: The Great Escape
ARTIST: Boys Like Girls
GENRE: Pop Punk
참고로, 리스트의 원소 개수는 ~{ ~}
사이에 있는 형식 지정자 개수(이 예시에선 둘)의 배수여야만 한다. 그렇지 않으면 예외가 발생할지도 모른다.
따라서, "|~{ ~dx~d=~2d |~}~%"
포맷 스트링은 주어진 리스트에서 3개씩 뽑아 ~d, ~d, ~2d
에 넣으라는 뜻이다.
FOR var UPFROM start
를 하면 루프가 끝날때까지 var
변수가 start
부터 계속 증가한다. repeat 3
로 3번 반복할 것을 명시하였으므로, 무한루프가 되지 않는 것이다.
APPEND
키월드가 의아할지도 모른다. 루프에는 몇가지 집계 키워드가 있다. maximize
, sum
, collect
, append
등. 여기서는 collect
와 append
를 예시로 살펴본다. 더 많은 집계 키워드를 알고 싶다면 Lispwork CLHS에 쓰인 BNF로부터, list-accumulation
과 numeric-accumulation
을 찾아보라.
(loop for i below 5
collect i)
;;=> (0 1 2 3 4)
(loop for i from 2 to 5
append (list i (* i i)))
;;=> (2 4 3 9 4 16 5 25)
그러므로 append ``(,level ,mult ,(* mult level))
는 (3 2 6)
, 혹은 (4 2 8)
과 같은 리스트를 계속 이어붙인 리스트를 만들어내라는 뜻이다.
방금 만든 mult-table-row
함수를 이용하면 삼렬 종대 곱셈표 만드는건 일도 아니다.
(defun mult-table ()
(loop for level from 1 to 9 by 3 do
(loop for mult from 1 to 9 do
(mult-table-row level mult))
(princ #\Newline)))
아주 새로운 점이라고는 BY
키워드 뿐이다. by
키워드에 지정한 숫자만큼 반복을 건너뛴다. 예를들어:
(loop for i below 10 by 2 collect i)
위 loop
반복문은 (0 2 4 6 8)
를 만들어낸다.
우리가 3씩 건너뛴 이유는, L, L+1, L+2단을 한꺼번에 출력하고 나면 L+3단부터 출력해야하기 때문이다.
4단계: n열 종대
삼렬 종대로 출력했음에도, 아직 너무 많은 공간이 남는다. 내 맘대로 줄였다 늘렸다 했으면 좋겠다. mult-table
함수가 한 번에 몇 열을 출력할건지 받게 하자.
역시 또 mult-table-row
함수를 수정해야 한다.
(defun mult-table-row (min-level max-level column mult)
(format t "|~{ ~dx~d=~2d |~}~%"
(loop repeat column
for level from min-level to max-level
append `(,level ,mult ,(* level mult)))))
이전 버전에 비해 아주 크게 바뀐것 같지는 않다. max-level
과 column
인자가 생겼다. 다시 한번 인자들의 역할을 정리할 때가 된 것 같다.
- min-level: 현재 행에 출력할 최소 단수
- max-level: 최종 단수(구구단이라면 9로 고정)
- column: 한 행에 출력하고자 하는 열 수
- mult: 각 단마다 곱해지는 배수
헌데 궁금하지 않은가? column 매개변수가 있는데 max-level은 왜 설정한걸까? 그냥 이 함수로 넘겨줄 때 column을 잘 설정해서 넘겨주면 될 것 같은데 말이지.
답은, 그런 값 설정의 귀찮음을 피하기 위해서이다. 이쪽 함수에서 받아 처리해 주는 편이 훨씬 loop에 쓰기에 자연스럽고 편리하다.
위 함수를 이용해서 아주 쉽게 mult-table
함수를 구현할 수 있다.
(defun mult-table (column)
(loop for level from 1 to 9 by column do
(loop for mult from 1 to 9 do
(mult-table-row level 9 column mult))
(princ #\Newline)))
아주 만족스럽다.
5단계: 19x19단
여태 우리가 작업한 코드이다.
(defun mult-table-row (min-level max-level column mult)
(format t "|~{ ~dx~d=~2d |~}~%"
(loop repeat column
for level from min-level to max-level
append `(,level ,mult ,(* level mult)))))
(defun mult-table (column)
(loop for level from 1 to 9 by column do
(loop for mult from 1 to 9 do
(mult-table-row level 9 column mult))
(princ #\Newline)))
10줄 정도 되는 코드로 n열 종대 구구단까지 만들었다. 그러나 우린 만족할 수 없다. 구구단에서 멈추면 19단을 외우는 인도인에게 뒤쳐진다. 우리는 한국인이 노벨상을 못타는건 (아마도) 19단을 안 외웠기 때문이라고 확신했다.
물론 농담이다. 어쨌든 이만큼 했으니, 별 어려움 없이 n단까지 확장할 수 있을 것 같다. 까짓거, 해보지 뭐. mult-table
함수에 새로운 키워드 인자를 추가한다.
(defun mult-table (&key (max-level 9) (column 3))
...)
n단까지 확장하려면 우리가 쓴 9라는 상수를 모두 바꾸어주어야 한다. 그럼 대충 이런 꼴이 된다.
(defun mult-table (&key (max-level 9) (column 3))
(loop for level from 1 to max-level by column do
(loop for mult from 1 to max-level do
(mult-table-row level max-level column mult))
(princ #\Newline)))
우리 mult-table-row
함수는 이정도 확장은 견딜 수 있을 정도로 충분히 일반적이다. 9라고는 코빼기도 보이지 않기 때문이다. 그럼 한 번 해볼까?
Uh-oh. 논리적으로는 일반화되었지만 출력 폼이 영 아니다. 출력 형식을 유동적으로 조절할 수 있도록 mult-table-row
를 수정해주자. 두 인자를 더 추가할 것이다. 인수의 자리수를 나타내는 fact-digit
, 곱 수의 자리수를 나타내는 prod-digit
인자이다. 인수가 늘어나는 것은 부담스럽지만, mult-table-row
에서 모두 계산한다면 계산량이 테이블 그리는 횟수만큼 늘어나기 때문에 mult-table
함수가 계산해서 전달하기로 하자.
(defun row-format (fact-digit prod-digit)
(let ((fact-spec (write-to-string fact-digit))
(prod-spec (write-to-string prod-digit)))
(concatenate 'string
"|~{ ~" fact-spec "dx~" fact-spec
"d=~" prod-spec "d |~}~%")))
(defun mult-table-row (min-level max-level column mult
fact-digit prod-digit)
(format t (row-format fact-digit prod-digit)
(loop repeat column
for level from min-level to max-level
append `(,level ,mult ,(* level mult)))))
어디, 헬퍼함수로 만든 row-format
함수가 제대로 기능하는지 테스트해보자.
CL-USER> (row-format 2 3)
"|~{ ~2dx~2d=~3d |~}~%"
만족스럽다. 이제 포맷스트링은 문제없을것이다. mult-table
함수도 수정된 mult-table-row
함수 스펙에 맞게 다시 수정해 주어야 한다.
(defun calc-digit (n)
(1+ (truncate (/ (log n) (log 10)))))
(defun square (n)
(* n n))
(defun mult-table (&key (max-level 9) (column 3))
(let ((fact-digit (calc-digit max-level))
(prod-digit (calc-digit (square max-level))))
(loop for level from 1 to max-level by column do
(loop for mult from 1 to max-level do
(mult-table-row level max-level column
mult fact-digit prod-digit))
(princ #\Newline))))
가독성을 위해 소소한 헬퍼함수 calc-digit
과 square
를 작성했다.
완성된 19단이 아름답다.
전체 코드는 아래와 같다. 공백을 제외하면 25줄이 안되는 짧은 코드이다.
(defun row-format (fact-digit prod-digit)
(let ((fact-spec (write-to-string fact-digit))
(prod-spec (write-to-string prod-digit)))
(concatenate 'string
"|~{ ~" fact-spec "dx~" fact-spec
"d=~" prod-spec "d |~}~%")))
(defun mult-table-row (min-level max-level column mult
fact-digit prod-digit)
(format t (row-format fact-digit prod-digit)
(loop repeat column
for level from min-level to max-level
append `(,level ,mult ,(* level mult)))))
(defun calc-digit (n)
(1+ (truncate (/ (log n) (log 10)))))
(defun square (n)
(* n n))
(defun mult-table (&key (max-level 9) (column 3))
(let ((fact-digit (calc-digit max-level))
(prod-digit (calc-digit (square max-level))))
(loop for level from 1 to max-level by column do
(loop for mult from 1 to max-level do
(mult-table-row level max-level column
mult fact-digit prod-digit))
(princ #\Newline))))
신나게 코딩했으니 이제 하던 일이나 마저 하러 가자.