기술서 읽고 정리

SICP Building Abstractions with Procedures.The Elements of Programming (1.1)

Jonchann 2020. 8. 2. 20:00

Foreword


Lisp

  • 인공지능 소프트웨어를 만들기 위해 사용하는 프로그래밍 언어
  • 리스트 형태로 된 데이터를 처리하도록 설계되었는데 프로그램과 데이터가 같은 형태이기 때문에 자료구조가 프로그램처럼 시행될 수 있으며, 프로그램 또한 리스트로 연산되고 기본자료구조가 linked list 인 것을 이용해 일반적인 연산을 수행하기도 함
    • linked list: head 가 존재하며 단일방향, 쌍방향, 순환형으로 노드가 이어진 리스트

  • linked list 는 다음 요소의 메모리 주소만을 갖고 있기 때문에 특정 데이터를 찾기 위한 순차탐삭에서는 처음부터 살펴봐야 하니 느림
    • 하지만 동적 할당을 하기 때문에 크기를 미리 지정하지 않아도 됨
      • 동적 할당: 실행 시간 동안 사용할 메모리 공간을 할당하는 것. 사용이 끝나면 운영체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재 할당을 받을 수 있음
      • 정적 할당: 프로그램이 실행하는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리의 할당이 이루어짐

Fortran

  • IBM704 에서 과학적 계산을 하기 위해 만든 프로그래밍 언어

Scheme

  • 함수형 프로그램과 절차적 프로그램을 지원하는 다중 패러다임 프로그래밍 언어
  • Lisp 의 방종
  • Lisp 와 다르게 정적 영역 규칙을 사용
    • 정적 영역 규칙: 현대의 언어는 '정적 해석'기능이 잘 되어있기 때문에 사실 이젠 Lisp 보다 더 에러가 발생할 수 없는 환경
      • 정적 해석: 컴파일 할 때 여러 체크를 하는 것 등
      • e.g. 데이터형 힌트
    • 이 시대에는 아직 데이터 '형태'에 관한 기술이나 프로그래밍 언어의 문법을 정의하는 기술이 발전하지 않았기 때문에 이런 관점에서 Lisp 를 더 좋게 평가했다고 할 수 있음
  • 반복문을 지원하지 않아 재귀함수로 처리
  • S 표현식 (S-expression) 뿐: $ f(arg_1, \ arg_2, \ ... ) => (f \ arg_1 \ arg_2 \ ... ) $
    • Symbolic Expression 이라는 뜻으로 sexp 라고도 함
    • 구조적인 데이터를 사람이 읽을 수 있는 형태로 나타내기 위한 표현식

Lisp:

(defun factorial (x)
    (if (zerop x) 1
        (* x (factorial (- x 1)))))

Scheme:

(define (factorial x)
     (if (zero? x) 1
         (* x (factorial (- x 1)))))

Computer Program

  • 기능이나 동작에 이름을 붙인 작은 그룹.
  • 현대 언어들에서 말하는 모듈, 클래스, 인터페이스란 심볼과 함수를 조합한 것.
    • 문자열: string 클래스의 조합
    • 날짜: date 클래스의 조합
    • 이와 같이 작은 부품 하나 하나를 모아 조합해 만들어내는 것이 프로그램.

1.0 Building Abstractions with Pro


모듈화의 중요성

  • 어떠한 복잡한 아이디어도 결국 더 단순한 아이디어로 쪼개지고 쪼개지는 것.
  • 재이용 가능하도록 모듈화 하는 것은 버그를 예방하는 것에도 도움이 됨.

recursion equations in Lisp

  • 점화식; 재귀식.
    • 수열에서 이웃하는 두 개의 항 사이의 관계를 나타낸 관계식
    • e.g. $ a_{n+1} = f(a_n) $
  • 깊게 짜여진 (deeply nested) 식에서 최대한 재귀 형태를 간결하게 표현해야 함

1.1 The Elements of Programming

  • Powerful 한 프로그래밍 언어의 3 가지 특징:
    • 원초적인 표현을 사용(Primitive Expressions); 언어가 다루는 개체들을 최대한 단순히 표현
      • 숫자와 산술연산은 가장 원초적인 데이터, 절차이다
      • 내장된 연산자의 값은 해당 연산자가 도출하는 기계어 명령 값이다
      • 다른 이름을 갖는 값은 environment 에 이름과 함께 할당된 object 이다
    • 조합의 수단(Means of Combinations); 단순한 것들을 조합
      • 여러 조합을 nest 하는 것은 연산을 조합하는 것과 같다
    • 추상화 수단(Means Of Abstraction); 이름 붙일 수 있으며 하나의 단위로 조작할 수 있는 요소들로 구성
      • 값에 이름을 정해 선언하는 것은 그 개념을 추상화하는 것이다
      • e.g.
(define  (square     x)        (*        x       x))
    |        |       |          |        |       |
   To     square  something,  multiply   it by itself

Lisp 구문

  • 자동으로 indent 적용
  • 괄호 일치 하이라이트 기능도 있음
(+ 137 349)
486

(+ (* 3
      (+ (* 2 4)
         (+ 3 5)))
   (+ (- 10 7)
      6))
57

(define (<name> <formal parameters>) <body>)
(define pi 3.14159)
(define radius 10)
(* pi (* radius radius))
314.159
  • Lisp 같이 문법이 간단한 언어가 아닌 이상 Syntatic Sugar 범벅이 되어버림 ("Lisp 최고")

    • Syntatic Sugar: 복잡한 문법을 간략하게 표기하는 것 => 읽는 사람이 직관적으로 코드를 이해할 수 있게 됨

      • e.g.

        function() => {}            
        
        // ↓
        
        () => {}
      • e.g.

      def my_decorator(func):
          def wrapper():
              print(1)
              func()
              print(2)
          return wrapper
      
      def say_three():
            print(3)
      
      say_three = my_decorator(say_three)
      
      # 1
      # 3
      # 2
      
      # ↓
      
      @my_decorator
      def say_three():
          print(3)
      
      say_three = say_three()
      # 1
      # 3
      # 2

변수 명명

  • 이름은 최대한 간단하고 단순하게
  • 프로그램을 짜다 보면 복잡한 것도 있겠지만 그것들도 다 작은 조각들을 합친 모자이크와 같은 것
  • 컴퓨터 속에서 이름을 갖는다는 것은 메모리에 할당한다는 것
    • 이 메모리를 environment (global environment) 라 함
      • 이 시대에는 아직 지역변수라는 개념이 없었기 때문
  • 굳이 environment 에 캐시하는 값이라면 중요하다는 것이므로 문맥을 알 수 있게 명명해야 함
    • e.g. (+ x 1) 은 문맥을 알 수 없음
  • special forms: 기본 연산이 아닌 define 등으로 각각의 rule 을 갖는 형태

인수를 받는 Procedure

  • substitution model

    • Applicative order: evaluate the arguments and then apply

      • 인수 값을 받아 계산한 결과를 얻으면 다음 procedure 에 대입

      • 'Lisp 는 연산을 한번씩 덜 하니까 더 효율적' <= 연산이니까

        (sum-of-squared (+ 5 1) (* 5 2))
        (+ (square (+ 5 1))    (square (* 5 2))   )
        (+ (* (5 + 1) (+ 5 1)) (* (5 2) (* 5 2))  )

        (+ (* 6 6) (* 10 10))
        (+    36       100  )
    • Normal order: fully expand and then reduce

      • 다음 계산에서 인수가 필요하기 전까지 앞 procedure 의 계산을 처리하지 않음
      • 하지만 함수를 인수로 받는 함수의 경우 필요하지 않을 경우에 대비해 필요한 순간에만 계산을 처리하는 것이 더 효율적

조건 표현과 예측

cond

  • predicate 가 True 를 찾을 때까지 연산
    • cond 는 symbol
    • (< $ p_1 $ > < $ e_1 $ >) 는 절(clause)
      • p: predicate
      • e: expression
        • 연속적인 표현이 올 수 있음
  • 만약 True 를 찾지 못한다면 아래 조건식의 값은 undefined
(define (abs x)
   (cond ((> x 0) x)
         ((= x 0) 0)
         ((< x 0) (- x))))

(define (abs x)
   (cond ((< x 0) (- x))
   (else x)))

if

  • (if < predicate > < consequent > < alternative >)
    • < consequent > 와 < alternative > 는 연속하지 않는 독립적인 표현
(define (abs x)
   (if (< x 0)
       (- x)
       x))

논리 연산

  • 논리연산

    • 모든 sub 표현식이 다 실행되지 않아도 되기 때문에 procedure 가 아님
  • 가장 많이 사용되는 논리연산

    • (and < $ e_1 $> ... < $ e_n $ >)
      • 하나라도 False 인 < $ e $ > 가 있다면 and 의 값은 False 가 되고 종료
    • (or < $ e_1 $> ... < $ e_n $ >)
      • 하나라도 True 인 < $ e $ > 가 있다면 or 의 값은 True 가 되고 종료
    • (not < $ e $>)
      • 모든 < $ e $> 가 False 일 때에만 False

절차적 언어와 선언적 언어

  • 함수(function)

    • 특성 서술 (describing properties)
    • 선언적 지식 (declarative knowledge)
      • '선언적': prolog (SICP 에서 초고레벨 언어로 설명하는 선언적 언어) 처럼 지식을 선언하는 것만을 가리킴
      • e.g. 지식을 선언하고 문제를 내면 선언된 지식에 따라 답을 판정
        • prolog: 제 1차 AI 붐이 왔을 때 how to 를 적지 않고 지식을 선언해 그것을 학습하는 것이 가장 인공지능 개발에 도움이 된다고 생각했지만 예외 적용, 예측 등의 반사적 반응을 할 수 없다는 것을 깨닫고 더이상 초고레벨 언어로 분류하지는 않음
        • 프로세스가 현실적인 속도로 움직일 수 있으려면 how to (process) 를 구체적이고 자세히 적어야 한다는 것을 깨닫고 부터는 현대 언어들과 같은 절차적 언어가 high-layer (레벨이라는 말이 주는 어감이 좋지 않아서 바뀐 표현) 언어가 됨
      • 현대 언어 중에서는 SQL 을 예로 들 수 있음
        • 테이블을 선언된 지식이라고 보고 쿼리는 문제, 반환되는 값은 답에 해당
  • 절차(procedure)

    • 방법 서술 (describing how to do this)

    • 절차적 지식 (imperative knowledge)

      • '절차적': 어떠한 기능을 구현할 때 그 방법을 서술하는 것

      • e.g. 아래와 같은 sqrt 함수는 어떻게 sqrt 를 구하는지에 대한 방법이 없기 때문에 전혀 절차적이지 못하고 컴퓨터도 제대로 처리하지 못함

        (define (sqrt x)
         (the y (and (>= y 0)
                (= (square y) x))))
      • 아래와 같이 '절차'(SICP 에서는 뉴턴법을 이용해 근사값을 구함)를 제대로 서술해야 함

        (define (improve guess x)
            (average guess (/ x guess)))
        
        (define (average x y)  
            (/ (+ x y) 2))
        
        (define (good-enough? guess x)  
            (< (abs (- (square guess) x)) 0.001))
        
        (define (sqrt-iter guess x)  
            (if (good-enough? guess x)
                guess
                   (sqrt-iter (improve guess x) x)))

추상화: 지역변수와 블록구조

  • 어떠한 procedure 를 더 큰 procedure 의 보조 procedure 로 사용하는 사용자는 보조 procedure 의 정제를 몰라도 되야 함(Procedure as Black-Box Abstractions)
    • 따라서 procedure 의 정의는 상세내용을 감추면서도 알기 쉬워야 함: 추상적인 표현
    • 즉, 중요한 것은 무엇이 반환되는가 하는 점이지 어떻게 반환되는가 하는 점이 아님
    • 예를 들어, 아래와 square 를 구하는 방법은 아래와 같이 여러가지이지만 이는 사용자가 알아야 하는 영역이 아니며 어떻게든 x 라는 값을 받아 그의 square 를 반환받으면 됨
      (define (square x) (* x x))
      (define (square x) (exp (double (log x))))
      (define (square x) (+ x x))
    • 절차는 인수 이름과 상관 없어야 함: 아래와 같이 같은 절차에 x 라는 이름의 인수를 받든 y 라는 이름의 인수를 받든 같은 처리를 해야 함
      (define (square x) (* x x))
      (define (square y) (* y y))

lexical scoping

  • 전역영역

    • 전역영역 변수는 좋지 못한 습관으로 인식되지만 함수명, 클래스명은 대부분 전역에서 접근 가능함
    • namespace 를 사용하면 전역변수와 함수명이 동일할 때의 충돌 방지
  • 모듈영역

    • Python 과 같은 모듈을 지원하는 프로그래밍 언어에서는 모듈단위의 변수를 선언하면 모듈 안에서는 얼마든지 접근할 수 있게 되어있음
    • 모듈 밖에서는 모듈 내에서 선언된 변수를 참조할 수 없음
    • $ C^{++} $ 같은 모듈을 지원하지 않는 객체지향언어에서는 클래스가 모듈영역 역할을 대신하기도 함
  • 함수영역

    • 함수 내에서만 유효한 지역변수를 제공

    • 함수영역을 사용하는 지역변수는 함수가 반환되면 더이상 사용할 수 없음: 정적영역규칙

      • 만약 동적영역규칙을 사용한다면: 다른 함수의 인자 이름과 겹칠 경우 이름 마스킹이 발생할 것
      • 동적영역규칙은 버그를 일으킬 수 있는 가능성이 높기 때문에 대부분 정적영역규칙을 사용
      def square(num):
          return num * num
      
      def sum_of_squares(num):
          sum = 0
          for n in num:
              sum += n
          return sum
  • 블록영역

    • for 루프의 i 는 for 블록에서만 유효함

      def sum_of_squares(n):
        ret = 0
        for i in n:
          n_squared = i * i
          ret += n_squared
      return ret
    • 루프나 조건문에서 유효하게 사용할 수 있지만 변수영역을 제한하기 위한 용도로 블록을 따로 선언할 수 있음

      int main()
      {
        {
          int num = 1;  // 블록 안에 변수 선언
      }    
      return num;  // 블록 밖에서는 참조할 수 없으므로 에러
      }
  • 표현식영역

    • 함수형 프로그래밍언어에서 지원하는 let: scope 를 갖는 변수 선언
    let x = 1;
    
    if (x === 1) {
        let x = 2;
      console.log(x);  // 2
    }
    
    console.log(x);  // 1

블록구조

  • Algol60 에서 유래

  • Algol 계 언어란

    • 1950년대 미국에서 만든 Fortran 에 대항해 유럽을 중심으로 개발된 언어

    • 어휘범위의 nested function 정의를 처음 추가한 묘사 지향적 언어

      • nested function: 중첩함수; 함수 안에 정의된 함수
    • 알고리즘 연구개발을 위한 언어로 BCPL, B, 파스칼, C 에 영향

      • BCPL: 1966년 케임브리지 대학교에서 설계한 절차적 명령형, 구조적 컴퓨터 프로그래밍 언어. 다른 언어들을 위한 컴파일러를 작성하기 위해 고안해 더이상 일반적으로 쓰이지 않게 되었지만 문법적으로 변형한 것이 B 이며 BCPL 에 기반을 둔 것이 C 임 (joke: Before C Programming Language). {} 를 처음으로 사용언 언어
      GET "LIBHDR"
      LET START () BE
      {
          WRITES ("Hello World!*N")
      }
      • B: 1969년에 AT&T 벨 연구소가 개발한 프로그래밍 언어이지만 C 언어로 흡수되는 형태로 멸종상태. B 언어로 만든 프로그램은 컴파일러에 의해 중간 코드로 변환되어 실행하는 인터프리터를 필요로 함
      • C: 1972년 유닉스 운영 체제에서 사용하기 위해 개발한 프로그래밍 언어로 벨 연구소의 B 를 따서 명명한 B 를 개선해 C 언어가 됨. 유닉스 시스템의 바탕 프로그램은 모두 C 로 작성되었으며 수많은 운영 체제의 커널도 C 로 만들어짐. $ C^{++} $ 는 C 를 객체지향형 언어로 발전시킨 것
      main() {
          printf("Hello World\n");
      }
      • Pascal: 1969년에 스위스 ETH 취리히의 컴퓨터 과학자 니클라우스 비르트가 개발한 프로그래밍 언어로, 1980~1990년대 초반까지 널리 사용되었음. 아주 기본적인 컴퓨터 언어 요소만을 갖고 있어 시스템을 직접 다루기엔 부족. 포인터를 사용한 구조적 프로그래밍과 데이터 구조화가 특징. Algol 60 에 영향을 받아 C 와 비슷하지만 더 간결하게 하고 버그를 쉽게 잡아내기 위해 기능을 제한해 C 보다 활용도가 떨어짐. 그렇지만 TeX 나 초기 매킨토시 운영체제 제작에 사용되기도 했으므로 완전히 비실용적이라고 할 수는 없음
      PROGRAM HelloWorld;
      
      BEGIN
          WRITELN('Hello World');
      END.
    • Fortran 은 전역 변수만 사용가능하지만 Algol 은 처음으로 지역변수 사용

    • 대표적인 블록구조 언어로 세계최초의 구조화 언어

    • 대수계산, 논리연산에 적합

    • BNF (베커스 나우르 표기) 를 처음으로 사용

      • ::=
      • ->, =>

종속변수와 자유변수

  • 종속변수는 지역변수
    • 종속: 주가 되거나 강한 힘을 가진 것에 달려 있거나 좌우되는 관계에 있음
    • procedure 의 formal parameter 는 bound variable 이며 procedure 는 formal parameter 를 bind 한 것
  • 자유변수
    • 수식 속에서 상수값으로 치환할 수 있는 값
  • 인수로 받는 종속변수(e.g. x, y, guess)와 자유변수(<, -, abs, square)는 구별될 수 있는 이름이어야 함
    • 단, 끊임없이 이름을 변경하는 것은 창피한 에러를 발생시키니 주의

위의 내용을 바탕으로 sqrt 함수를 작성하면 아래와 같음

(define (sqrt x)
    (define (good-enough? guess)
        (< (abs (- (square guess) x)) 0.001))
    (define (improve guess x)
        (average guess (/ x guess)))
    (define (sqrt-iter guess x)
        (if (good-enough? guess x)
            guess
            (sqrt-iter (improve guess x) x)))
    (sqrt-iter 1.0 x))

Python 으로 적으면 아래와 같이 적을 수 있음

def sqrt(x):
    def good_enough(guess):
        return abs(guess ** 2 - x) < 0.001
    def improve(guess):
        return average(guess, x / guess)
    def sqrt_iter(guess):
        if good_enough(guess):
            return guess
        return sqrt_iter(improve(guess))
    return sqrt_iter(1.0)