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 endvar이라는 렉시컬 변수에 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 함수의 첫째 줄부터 하나 하나 뜯어보자.

~{ ~} 형식 지정자

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에 넣으라는 뜻이다.

저 LOOP 왜저래?

mult-table-row 함수로 돌아가기

FOR var UPFROM start를 하면 루프가 끝날때까지 var 변수가 start부터 계속 증가한다. repeat 3로 3번 반복할 것을 명시하였으므로, 무한루프가 되지 않는 것이다.

APPEND 키월드가 의아할지도 모른다. 루프에는 몇가지 집계 키워드가 있다. maximize, sum, collect, append 등. 여기서는 collectappend를 예시로 살펴본다. 더 많은 집계 키워드를 알고 싶다면 Lispwork CLHS에 쓰인 BNF로부터, list-accumulationnumeric-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 함수로 돌아가기

방금 만든 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열 종대

too-much-space

삼렬 종대로 출력했음에도, 아직 너무 많은 공간이 남는다. 내 맘대로 줄였다 늘렸다 했으면 좋겠다. 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-levelcolumn 인자가 생겼다. 다시 한번 인자들의 역할을 정리할 때가 된 것 같다.

  • 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)))

good

아주 만족스럽다.

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라고는 코빼기도 보이지 않기 때문이다. 그럼 한 번 해볼까?

3.bug

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-digitsquare를 작성했다.

4.19-good

완성된 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))))

신나게 코딩했으니 이제 하던 일이나 마저 하러 가자.