Foreword
Lisp
- 인공지능 소프트웨어를 만들기 위해 사용하는 프로그래밍 언어
- 리스트 형태로 된 데이터를 처리하도록 설계되었는데 프로그램과 데이터가 같은 형태이기 때문에 자료구조가 프로그램처럼 시행될 수 있으며, 프로그램 또한 리스트로 연산되고 기본자료구조가 linked list 인 것을 이용해 일반적인 연산을 수행하기도 함
- linked list: head 가 존재하며 단일방향, 쌍방향, 순환형으로 노드가 이어진 리스트
- linked list 는 다음 요소의 메모리 주소만을 갖고 있기 때문에 특정 데이터를 찾기 위한 순차탐삭에서는 처음부터 살펴봐야 하니 느림
- 하지만 동적 할당을 하기 때문에 크기를 미리 지정하지 않아도 됨
- 동적 할당: 실행 시간 동안 사용할 메모리 공간을 할당하는 것. 사용이 끝나면 운영체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재 할당을 받을 수 있음
- 정적 할당: 프로그램이 실행하는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리의 할당이 이루어짐
- 하지만 동적 할당을 하기 때문에 크기를 미리 지정하지 않아도 됨
Fortran
- IBM704 에서 과학적 계산을 하기 위해 만든 프로그래밍 언어
Scheme
- 함수형 프로그램과 절차적 프로그램을 지원하는 다중 패러다임 프로그래밍 언어
- Lisp 의 방종
- Lisp 와 다르게 정적 영역 규칙을 사용
- 정적 영역 규칙: 현대의 언어는 '정적 해석'기능이 잘 되어있기 때문에 사실 이젠 Lisp 보다 더 에러가 발생할 수 없는 환경
- 정적 해석: 컴파일 할 때 여러 체크를 하는 것 등
- e.g. 데이터형 힌트
- 이 시대에는 아직 데이터 '형태'에 관한 기술이나 프로그래밍 언어의 문법을 정의하는 기술이 발전하지 않았기 때문에 이런 관점에서 Lisp 를 더 좋게 평가했다고 할 수 있음
- 정적 영역 규칙: 현대의 언어는 '정적 해석'기능이 잘 되어있기 때문에 사실 이젠 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.
- 원초적인 표현을 사용(Primitive Expressions); 언어가 다루는 개체들을 최대한 단순히 표현
(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 (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
- (and < $ e_1 $> ... < $ e_n $ >)
절차적 언어와 선언적 언어
함수(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)
'기술서 읽고 정리' 카테고리의 다른 글
『Web API The Good Parts』를 읽고 - 엔드포인트 설계와 리퀘스트 형식 (0) | 2024.02.23 |
---|---|
『Web API The Good Parts』를 읽고 - Web API란 무엇인가 (0) | 2024.02.23 |
The Art of Readable Code를 읽고 (0) | 2023.04.13 |
SQL Antipattern -Avoiding the Pitfalls of Database Programming- 을 읽고 (0) | 2021.03.22 |
도메인구동설계입문을 읽고 (0) | 2020.12.06 |