2015년 1월 20일 화요일

컴퓨터 프로그램의 구조와 해석(1) 2장 1

2장. 데이터를 요약해서 표현력을 끌어올리는 방법

  개요:
1. 복잡한 데이터를 요약하는 방법(계산 물체를 만들 수 있어야 한다.)
2. 여러 데이터 물체를 엮어서 묶음 데이터로 요약하는 방법을 알아보자.
3. 데이터 요약(데이터 간추리기):
데이터를 만드는 부품과 데이터를 쓰는 부품을 따로 나누어 생각하는 방법
4. 묶음 데이터를 만들어 쓰는 데 가장 중요한 것은, 프로그래밍 언어에서 여러 데이터를 한     데 '묶는' 기능이다.- 차례열(sequence)과 나무(tree)구조의 데이터 만드는 방법
5. 닫힘 성질(closure property): 묶음 데이터들을 묶으면 다시 묶음 데이터가 된다는 것.
6. 공통 인터페이스(conventional interface):
  프로그램을 조립하다 보면, 서로 아귀가 맞지 않는 부품이 있게 마련인데, 묶음 데이터가 여러 부품 사이에서 아귀를 맞춰 쓸 수 있게끔 두루 쓸 수 있는 인터페이스 구실을 하게 된다
7. 수 말고도 글자(기호) 데이터로 만든 글자 식
8. 집합을 나타내는 여러 방법
9. 한 프로그램에서 한 가지 데이터를 여러 방법으로 나타낼 수 있어야 할 때 프로그램을 어      떻게 설계해야 하는 지 공부한다.
  -> 서로 다른 타입 데이터를 다룰 수 있도록 일반화된 연산을 만들어야 한다.
        -> 일반화된 연산을 받아들이면서도 조립식 설계의 좋은 점을 읽지 않으려면??
              -> 요약의 경계를 잘 그어야 한다.
                       ->데이터 중심으로 프로그램 짜는 법(data-directed programming)


2.1 데이터 요약(데이터 간추리기, 데이터 내용 감추기)

   간추린 프로시저를 쓰기 때문에 (기본 연산으로) 프로시저를 만드는 쪽과 쓰는 쪽을 둘로 나누어 생각할 수 있도록 둘 사이에 '요약의 경계'를 그을 수 있다.
 ==> 데이터 요약
           //데이터를 나타낼 때 input과 output을 제외하고는 신경쓰지 않겠다.
 ==>만드는 쪽과 쓰는 쪽을 이어주는 프로시저(인터페이스): 선택자(selector)
                                                                                      구성자(constructor)

2.1.1 연습: 유리수를 위한 산술 연산

   분자와 분모로 유리수를 짜맞추는 방법(연산)과 어떤 유리수에서 분자와 분모를 골라내는 방법(연산)이 있다고 하자. 그리고 선택자와 구성자가 다음 프로시저로 정의되어 있다고 하자.

(make-rat<n><d>): 분자가 <n>이고 분모가<d>인 유리수를 내놓는다.
(numer <x>): 유리수 <x>의 분자를 내놓는다.
(denom <x>):유리수 <x>의 분모를 내놓는다.

ex) (n1/d1)+(n2/d2)=(n1*d2+n2*d1)/(d1*d2)
   => (define (add-rat x y)
            (make-rat (+ (* (numer x) (denom y) )
                               (* (numer y) (denom x) ) )
                            (* (denom x) (denom y) ) ) )

 이런 식으로 프로시저들을 짬으로서 유리수 연산을 할 수 있다.
하지만 numer, denom, make-rat 같은 선택자와 구성자를 정의하는 법을 모른다.

  쌍(pair)
ex) (define x (cons 1 2) )                              (define x (cons 1 2) )
   
      (car x): 1                                               (define y (cons 3 4) )

      (cdr x): 2                                                (define z (cons x y) )
     
                                                                  (car (car z) ): 1

                                                                   (car (cdr z) ): 3

쌍으로 생성한 데이터 물체: 리스트 구조를 갖춘(list-structed) 데이터라고 한다.

유리수 만들기
(make-rat<n><d>), (numer <x>), (denom <x>) 정의

(define (make-rat n d) (cons n d) )
(define (numer x) (car x) )
(define (denom x) (cdr x) )

==> 기약분수로 나타낼 수 있게 하는 make-rat정의
                (define (make-rat n d)
                   (let ( (g (gcd n d) ) )
                      (cons (/ n g) (/ d g) ) ) )


2.1.2 요약의 경계
경계마다 있는 프로시저들은 요약의 경계들을 연결시켜주는 인터페이스다.

*각 프로시저끼리 서로 영향을 덜 받게 프로그램을 짠다.
  (Why? 나중에 한 프로시저를 바꿀 때 다른 프로시저까지 바꿔야 될 수도 있기 때문)
ex)
(define  (numer x)
   (let  ( (g (gcd (car x) (cdr x) ) ) )
      (/ (car x)  g) ) )
 =>이런 식으로 짜놓으면 나중에 add-rat,sub-rat같은 한 레벨 위에 있는 프로시저까지 고칠     필요 없다.


2.1.3 데이터란 무엇인가
   데이터:선택자나 구성자 같은 프로시저가 알맞은 데이터 표현을 만들어 내는지 따져볼 수 있는 조건까지 함께 정의된 것

ex)
(define (cons x y)
    (define (dispatch m)
        (cond ( (= m 0) x)
                  ( (= m 1) y)
                  (else (error "Argument not 0 or 1 -- CONS"  m) ) ) )
 dispatch)

(define (car z)  (z  0) )  => (define (car (cons x y) ) ((cons x y)  0) )

(define (cdr z)  (z  1) )  => (define (cdr (cons x y) ) ((cons x y)  1) )

데이터를 프로시저로 표현하는 기법: 메시지 패싱


2.2 계층 구조 데이터와 닫힘 성질

쌍의 구조를 보는 법: 상자와 화살표로 나타내기
쌍이 다시 쌍의 원소가 될 수 있다 -> 닫힘 성질


2.2.1 차례열의 표현 방법
             => 모든 쌍의 car는 원소를 가리키고, cdr은 연결된 다음 쌍을 가리킨다.
리스트(list)

(list <a1> <a2> ... <an>) = (cons <a1> (cons <a2> (cons ... (cons <an> nil) ...) ) )

ex)
(car one-through-four): 1
(cdr one-through-four): (2  3  4)
(car (cdr  one-through-four) ): 2
(cons 10  one-through-four): (10  1  2  3  4)

* nil = 빈 리스트

  리스트 연산

리스트와 n을 인자로 받아서 n번째원소를 내놓는 list-ref
(define  (list-ref  items  n)
   (if  (= n  0)
        (car  items)
        (list-ref  (cdr  items)  (-n  1) ) ) )

전체를 훑어내려가기
(define  (length  items)
   (if  (null?  items)
        0
        (+ 1  (length  (cdr  items) ) ) ) )

append 프로시저
(define (append list1 list2)
  (if  (null?  list1)
        list2
       (cons  (car  list1)  (append  (car  list1)  list2) ) ) )

리스트 매핑

   map은 프로시저 하나와 리스트를 인자로 받아, 리스트 원소마다 똑같은 프로시저를 적용한 결과를 묶은 리스트를 내놓는다.
ex)
(define  (map  proc  items)
   (if  (null?  items)
        nil
        (cons  (proc (car  items) )
                  (map  proc  (cdr  items) ) ) ) )


2.2.2 계층 구조

(cons (list  1  2)  (list  3  4) )
(cons (list  1  2)  (list  3  4))로 만든 구조

리스트 구조를 나타낸 나무꼴
(define (count-leaves  x)
    (cond ((null? x) 0)
              ((not (pair? x)) 1)
              (else (+ (count-leaves (car x))
                      (count-leaves (cdr x))))))

나무 매핑 (map + 재귀처리방식)
(define (scale-tree tree factor)
   (cond ((null? tree) nil)
       ((not (pair? tree)) (* tree factor))
        (else (cons (scale-tree (car tree) factor)
                          (scale-tree (cdr tree) factor)))))


2.2.3 공통 인터페이스로서 차례열의 쓰임새

(define (sum-odd-squares tree)                                     -- {enumerate
   (cond ((null? tree) 0)
             ((not (pair? tree))                                              -- filter
               (if (odd? tree) (square tree) 0))                        -- map
               (else (+ (sum-odd-squares (car tree))             -- acccumulate
                           (sum-odd-squares (cdr tree)))))              }


(define (even-fibs n)                                                     -- {enumerate
     (define (next k)                                                        -- map
          (if  (> k n)
                nil
                (let (even? f)                                                  -- filter
                      (cons f (next (+ k 1)))                               -- accumulate
                      (next (+ k 1))))))                                             }
    (next 0))   

// enumerate(원소 늘어놓기) 부분이 여기저기 널부러져 있다.

차례열 연산 <= 각 부분이 명확하게 나뉘게 프로그램을 짤 수 있게 하고 싶다!!!

How? 계산단계를 넘어갈 때의 "신호"를 유심히 보자!
          //각각의 단계에 맞는 프로시저를 만든다

(map square (list 1 2 3 4 5))                                 -- map

(define (filter predicate sequence)                        -- filter
   (cond ((null? sequence) nil)
             ((predicate (car sequence))
               (cons (cat sequence)
                         (filter predicate (cdr sequence))))
             (else (filter predicate (cdr sequence)))))

(define (accumulate op initial sequence)                -- accumulate
   (if (null? sequence)
         initial
         (op (car sequence)
               (accumulate op initial (cdr sequence)))))

(define (enumerate-interval low high)                      -- enumerate(두번째 것)
     (if (> low high)
          nil
          (cons low (enumerate-interval (+ low 1) high))))

==> (define (even-fibs n)
           (accumulate cons
                              nil
                              (filter even?
                                       (map fib
                                                (enumerate-interval 0 n)))))

// 프로그램 코드가 더 길어진 것 같지만 마치 기계부품같이 map, filter, accumulate 프로시저들을 다른 프로그램에서도 쓸 수 있다. <= 모듈방식

겹친 매핑<= 모듈방식을 겹친 루프를 써서 구현해보자
ex) 
양의 정수 n,i,j가 있을 때, i=<j<i=<n을 만족하고 i + j의 값이 소수가 되는, i와 j의 모든 순서쌍을 구하자.
=>1. n보다 작거나 같은 양의 정수로 이루어진 모든 순서쌍을 차례열로 묶는다.
    2. 그 중 합이 소수인 쌍들만 filter로 골라내고, 골라넨 쌍(i, j)에 대해 트리플(i, j, i + j)를 만         든다.
(accumulate append
                   nil
                  (map (lambda (i)
                                (map (lambda (j) (list i j)
                                         (enumerate-interval 1 (-i 1))))
                            (enumerate-interval 1 n)))

(define (flatmap proc seq)
      (accumulate append nil (map proc seq)))

(define (prime-sum? pair)
     (prime? (+ (car pair) (cadr pair))))

(define (make-pair-sum pair)
     (list (car pair) (cadr pair) (+ (car pair) (cadr pair))))

(define (prime-sum-pairs n)
    (map make-pair-sum
             (filter prime-sum?
                      (flatmap
                          (lambda (i)
                              (map (lambda (j) (list i j))
                                       (enumerate-interval 1 (- i 1))))
                           (enumerate-interval 1 n))))) 


2.2.4 .연습 : 그림 언어

사용하는 그림언어: 페인터(painter)
   
     -- beside 연산: 페인터 두개를 인자로 받아서 첫 번째 페인터는 틀의 왼편에 그림을 그리도록 하고 두 번째 페인터는 틀의 오른편에 그림을 그리게끔, 새로운 페인터를 만들어낸다.
    -- below 연산: 첫 번째 페인터가 두 번째 페인터 그림 아래에다 그림을 그리도록, 두 페인터를 묶어 새로운 페인터를 짜 맞춘다.
    -- flip-vert 와 flip-horitz: 각각 페인터가 위아래로 , 옆으로 뒤집힌 그림을 그린다.

==> 복잡한 그림을 짜맞출 수 있는 이유: 닫힘 성질

                                  right-split n                                 corner-split n                                

(define (right-split painter n)  //2.
   (if (= n 0)
        painter
       (let ((smaller (right-split painter (- n 1))))            //let으로 smaller 프로시저 정의
            (beside painter (below smaller smaller)))))      //재귀 연산이 들어간다.

(define (corner-split painter n)  //2.
    (if (= n 0)
        painter
         (let  ((up  (up-split painter (- n 1)))                 //let으로 up, right 프로시저 정의
                 (right (right-split painter (- n 1))))
           (let ((top-left (beside up up))                       //let으로 top-left와 bottom-right,
                   (bottom-right (below right right))               corner 프로시저가 정의됨
                   (corner (corner-split painter (- n 1))))
             (beside (below painter top-left)                
                          (below bottom-right corner)))))


차수 높은 연산
페인터 연산을 인자로 받아 새로운 페인터 연산을 만들어 내는 프로시저를 짜보자
// ?? 잘 모르겠다 예시로 이해하자
                                                         //3.
ex) (define (square-of-four tl tr bl br)  //top-left,top-right   bottom-left,bottom-right
          (lambda (painter)
              (let ((top  (beside  (tl painter) (tr painter)))     //top에 좌우
                     (bottom  (beside  (bl  painter)  (br  painter))))     //bottom에 좌우
                (below  bottom  top))))

    (define (flipped-pairs painter)          //3.
        (let  ((combine4  (square-of-four  identity flip-vert
                                                          identity flip-vert)))
           (combine4  painter)))

==> flipped-pairs 프로시저를 정의하기 위하여 square-of-four이라는 페인터연산을 가져다      사용했다.


그림틀

그림틀을 만드는 연산: make-frame
세 벡터를 골라내는 연산:
origin-frame, edge1-frame, edge2-frame










(define  (frame-coord-map  frame)              //벡터의 크기 변환  //1.
    (lambda  (v)                          
         (add-vect
            (origin-frame  frame)
            (add-vect  (scale-vect  (xcor-vect  v)
                                               (edge1-frame  frame))
                            (scale-vect  (ycor-vect  v)
                                               (edge2-frame  frame))))))


페인터
페인터는 프로시저로 나타낸다. 이 프로시저는 그림틀을 인자로 받아서, 그 틀에 맞춘 그림을 그린다.

(define  (segments->painter segment-list)            //1.
   (lambda  (frame)
      (for-each
          (lambda  (segment)
              (draw-line
                  ((frame-coord-map  frame)  (start-segment  segment))
                  ((frame-coord-map  frame)  (end-segment  segment))))
      segment-list)))

==> 화면의 두 점 사이에 선을 긋는 draw-line 프로시저


페인터를 변환해서 엮어 쓰는 방법

페인터 연산들은 transform-painter 프로시저에 바탕을 둔다. 이 프로시저는 페인터 하나와 틀을 변환하는 데 필요한 정보(새 틀의 꼭짓점을 나타내는 벡터)를 인자로 받아서 새로운 페인터를 만들어낸다.
ex)
(define  (transform-painter painter origin corner1 corner2)     //1.
  (lambda  (frame)
      (let  ((m  (frame-coord-map  frame)))
         (let  ((new-origin  (m  origin)))
           (painter
              (make-frame  new-origin
                                    (sub-vect  (m  corner1)  new-origin)
                                    (sub-vect  (m  corner2)  new-origin)))))))

(define  (flip-vert  painter)                                        //2.
    (transform-painter  painter
                                 (make-vert  0.0  1.0)     //원점 설정
                                 (make-vert  1.0  1.0)     //첫 번째 점
                                 (make-vert  0.0  0.0)))  //두 번째 점
==> 실제로 그래프를 그리면 어떻게 그림이 그려질 지 알 수 있다.

여러 페인터를 엮어내는 수단으로서도 사용가능

(define  (beside  painter1  painter2)                          //2.
   (let  ((split-point  (make-vert  5.0  0.0)))
     (let  ((painter-left
              (transform-painter  painter1
                                           (make-vert  0.0  0.0)
                                          split-point
                                           (make-vert  0.0  1.0)))
              (painter-right
               (transform-painter painter2
                                           split-point
                                           (make-vert  1.0  0.0)
                                           (make-vert  0.5  1.0))))
      (lambda  (frame)
         (painter-left  frame)
          (painter-right  frame)))))
==> make-vert, transform-painter 프로시저를 알아야 이해할 수 있다.


단단하게 설계할 때 쓰는 언어 계층

다층설계
ex) 컴퓨터 공학
1. 디지털 논리 회로
2. 프로세서, 버스 구조, 메모리 시스템
3. 컴퓨터
4. 컴퓨터 언어                                                                                                             5. 네트워크 연결(컴퓨터 간의 연결)

그림 언어
1. 기본 페인터(점이나 선을 나타내는 언어) 
2. 페인터 연산(beside, below) 
3. 페인터 연산을  기본으로 엮어낸 페인터 연산(square-of-four)
  //이 과정을 보고 그림언어의 전체적인 구조를 파악하기
  //프로시저들을 단계적으로 엮어내고 있다!!!




댓글 없음:

댓글 쓰기