모든 것이 함수다: David Beazley와 SICP 강의 후기
(ezzeriesa.notion.site)David Beazley와 SICP 강의 후기: 1주간의 경험
2022년 말 David Beazley의 SICP 강의에 참여한 경험을 공유함. 여러 무료 자료가 있지만, Dave의 강의는 특정 주제를 선택하고 심도 있게 설명함으로써 매우 효과적이었음.
시작점
SICP 강의는 Scheme 언어로 진행되었으며, 여기서는 Python으로 간단한 Scheme 해석기를 구현하여, 기초 개념인 대입(substitution) 모델을 설명함.
Scheme 언어의 기본
- 프리미티브(Primitive): 기본 값들 (예: 정수)
- 연산자:
+,-,*,/등의 기본 연산을 접두어 표기법으로 사용 - define: 변수 정의
> (define x 2)
> (+ x 3) ; 결과: 5
- if: 조건문
- lambda: 익명 함수 정의
> ((lambda (x) (* x x)) 3) ; 결과: 9
Python에서의 Scheme 해석기
Python을 사용하여 Scheme 코드를 평가하는 간단한 해석기 구현. 기본 연산은 Python 함수로 정의.
definitions = {
"+": lambda x, y: x + y,
"*": lambda x, y: x * y,
}
예시:
> evaluate(("+", 2, 3)) # 결과: 5
define과 lambda의 구현, 그리고 조건문 if 처리까지 포함.
대입 모델(Substitution Model)
대입 모델은 간단한 프로그램 해석 방식으로, 변수를 값으로 대체하며 프로그램을 평가함. 그러나 **할당(assignment)**이 포함되면 이 모델은 실패함.
상태(State)
대입 모델이 깨지는 예로 **할당(assignment)**을 들 수 있음. 예를 들어 은행 계좌 잔고를 모델링할 때 set!을 사용하여 변수를 업데이트함.
(define balance 100)
(define (withdraw amount)
(set! balance (- balance amount))
balance)
이 경우 대입 모델은 이전과 이후의 잔고 상태를 구분하지 못함.
환경(Environment) 모델이 필요해짐. 변수는 환경 내에서 정의되고, 각 절차는 자신만의 환경을 가짐.
스트림(Streams)
상태를 모델링하는 또 다른 방식으로 스트림이 있음. 스트림은 지연 평가(lazy evaluation)를 통해 미래의 값도 모델링할 수 있음.
무한 루프와 평가 순서
평가 순서의 차이: 대부분의 언어는 **적용 순서 평가(applicative-order evaluation)**를 사용하여 인자를 먼저 평가함.
> (square (+ 1 2)) ; 결과: 9
하지만 **정상 순서 평가(normal-order evaluation)**는 인자가 실제로 필요할 때까지 평가를 지연시킴. 이로 인해 무한 루프를 피할 수 있음.
> (define (p) (p))
> (define (test x y) (if (= x 0) 0 y))
> (test 0 (p)) ; 정상 순서에서는 0 반환, 적용 순서에서는 무한 루프
람다 계산법과 Church 숫자
Church 인코딩을 통해 숫자를 절차(procedure)로 표현할 수 있음. 이는 함수형 프로그래밍의 중요한 개념임.
(define (zero f) (lambda (x) x))
(define (increment n) (lambda (f) (lambda (x) (f ((n f) x)))))
zero는 인자를 그대로 반환하는 함수 (identity함수).increment는 함수 호출을 한 번 더 적용함.
예시
> ((zero (lambda (x) (+ x 1))) 0) ; 결과: 0
> (((increment zero) (lambda (x) (+ x 1))) 0) ; 결과: 1
반복 vs 재귀
Scheme은 for 루프 대신 재귀를 사용하여 반복 작업을 수행함.
재귀 예시: 팩토리얼
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
이 재귀 호출은 스택을 사용하여 메모리를 많이 차지할 수 있음.
꼬리 재귀 최적화(Tail-call optimization)
Scheme은 꼬리 재귀 최적화를 통해 메모리 사용을 줄임. 이로 인해 반복적(iterative) 프로세스처럼 작동하게 됨.
(define (factorial n)
(define (iter product counter)
(if (> counter n)
product
(iter (* product counter) (+ counter 1))))
(iter 1 1))
마무리
David Beazley의 강의는 SICP의 주요 개념을 선택하여 깊이 있게 다룸. 특히 함수형 프로그래밍, 람다 계산법, 평가 순서 등 다양한 프로그래밍 패러다임을 이해하는 데 도움을 줌.
Knuth의 인용
이론만 공부한다면 실천적인 부분에 집중할 때가 되었음을 의미하고, 실천만 한다면 이론적인 부분에 집중할 때가 되었음을 의미함.
댓글과 토론
Hacker News 의견들
-
경고하자면 SICP/Lisp/Scheme에 깊이 들어가면 프로그래밍을 생각하는 방식이 바뀔 수 있고, 그런 지적 자극은 늘 환영할 만함
하지만 그 아이디어를 객체지향 코드베이스에 통째로 적용하면 대개 역효과가 나거나 팀원들의 반발을 부름
예를 들어 Lisp 이후 모든for루프를forEach로 바꾸거나 모든 것을map/reduce체인으로 만들고 싶어질 수 있는데, 언어가 함수형 프로그래밍을 온전히 끌어안지 않는다면 가독성과 성능을 모두 해칠 수 있음
결국 가변 메모리와 CPU가 코드를 처리한다는 사실을 기억하는 게 기반을 잡아주며, 요즘은 Church 숫자 같은 추상 개념보다 데이터 지향 설계와 하드웨어 현실에 맞춘 “기계적 공감”이 일상적으로 더 실용적이라고 봄- 불변성이 더 안전하다고 봄
부작용이 적을수록 더 예측 가능하고, 객체지향은 그냥 함수로 충분한 곳에서도 자주 쓰임
게임 개발에서는 계산과 위험한 코드가 많아 객체지향이 꽤 잘 맞지만, 웹 개발에서는 함수형 프로그래밍이 훨씬 자연스럽고, SaaS에서는 Elixir 같은 언어가 더 신뢰성 높고 버그가 적으며 테스트하기 좋은 코드를 쓰게 해줌 - Common Lisp Object System 같은 것도 있지 않나 싶음
게다가forEach,map/reduce같은 것은 Smalltalk 컬렉션에 이미 있었고, 1급 문법은 아니더라도 Object Pascal, C++에도 복사됐음
밑바탕 메모리가 가변이기 때문에, 정말 필요할 때는 ML 계열 언어에도 변이를 다루는 메커니즘이 있음 - CPU가 제어 흐름을 구현하는 유일한 방식이
goto에 가깝지만,goto는 Lisp보다도 인기가 없음
더 기묘한 건 현대 CPU에서 성능을 내려면 캐시가 필수인데도, 메모리 계층을 1급 시민으로 다루는 언어가 아직 없다는 점임
가장 가까운 건 밑줄이 바다처럼 넘치는 Linux식 C 정도로 보임
- 불변성이 더 안전하다고 봄
-
순수 함수로 상태를 인코딩하는 좋은 입문을 제공함
실제로 트리, 정수, 합/곱 타입, 이미지, 모나드 등 온갖 데이터에 대해 순수 함수형 인코딩이 훨씬 많음
인코딩은 약간 헷갈릴 수 있지만 동시에 우아하고 작음
예를 들어 JavaScript로 Maybe 모나드를 함수형으로 구현하면 다음처럼 됨:
Nothing = nothing => just => nothing
Just = v => nothing => just => just(v)
pure = Just
bind = mx => f => mx(mx)(f)
evalMaybe = maybe => maybe("Nothing")(v => "Just " + v)
console.log(evalMaybe(bind(Nothing)(n => pure(n + 1)))) // Nothing
console.log(evalMaybe(bind(Just(42))(n => pure(n + 1)))) // Just 43- 이런 구현은 해당 타입의 재귀 원리에서 유도할 수 있음
data Maybe a = Nothing | Just a
foldMaybe :: (Unit -> r) -> (a -> r) -> Maybe a -> r
foldMaybe에 넘기는 두 고차 함수가 각각Nothing과Just에 해당함
다만Nothing쪽은 조금 더 정확하게 하려고Unit매개변수를 추가한 형태임 - 귀납적 타입을 그 타입의 재귀자 함수 타입으로 바꾸는 것으로 볼 수 있음
타입 이론에서는 꽤 멋지지만, 실제 프로그래밍에는 그다지 좋지 않음 - 수학적으로는 우아할 수 있지만, 정렬이나 내부 정당화 없이 ASCII 문자 집합만으로 된 언어를 통해 전달되면 보기에는 꽤 고통스러움
- 짧게 말하면 무타입 람다 계산은 튜링 완전함
- 이런 구현은 해당 타입의 재귀 원리에서 유도할 수 있음
-
예전에 실제 SICP 강의, 즉 MIT OCW의 1986년 녹화본을 봤음
정보 밀도가 높다고 자주 칭찬받지만, 실제로는 학생 질의응답, 강의실에서 “멀티미디어” 발표를 시도한 것에 강사가 주의를 끄는 시간, 전체 수업 계획이 미리 완전히 정리되지 않아 질의응답을 선제적으로 막지 못하는 부분 등으로 꽤 많은 시간이 낭비됨
칠판에 쓰는 시간도 누적되면 상당함
물론 자료 순서는 얼마든지 논쟁하고 재배치할 수 있고, 언젠가 내 감각에 맞게 이 내용을 설명하는 영상 시리즈를 직접 만들 계획도 있음
이 강의가 Python 같은 더 현대적인 언어를 쓰면서도 뿌리를 유지하는 듯해 반갑고, Python은 실용적인 다중 패러다임 언어라서 완전한 순수성은 아니더라도 함수형 관용구를 통한 표현력을 사람들이 충분히 인정하지 않는다고 생각함- 이 강의는 Python으로 Scheme을 구현한 뒤, 다시 Scheme으로 Scheme을 구현함
Python은 이 강의에서 빼도 되고 빼야 한다고 봄
Python은 함수형 프로그래밍 지원이 매우 약함
리스트는 cons 기반이 아니고, 람다는 제약이 많고, 패턴 매칭은 끔찍하며 표현식 기반도 아니고, 이름공간도 이상함
Python은 현대적인 언어라고 보기도 어렵고 1990년대에 머물러 있으며, 괜찮은 C-API가 있어서 안타깝게도 더 나은 언어들을 희생시키며 성장했을 뿐임 - Python의 함수형 프로그래밍은 꽤 약하고, 그 부분에서는 JavaScript가 오히려 더 낫다고 봄
꼬리 호출 최적화가 없기 때문에 일부 코드 연습은 Scheme과 완전히 다른 해법이 필요함
1:1로 번역한 코드가 실패하면 강사는 학생들에게 선택한 언어 때문에 그렇게 동작하지 않으니 그냥 믿으라고 해야 하나, 아니면 전부 스택을 외부화하는 문제로 보고 그렇게 풀어야 하나 싶음
SICP를 Python에 억지로 끼워 넣는 건 꽤 어리석어 보임 - ArsDigita University의 강의도 있음
사이트는 지금 오프라인이지만 archive.org에서 강의를 볼 수 있음
https://en.m.wikipedia.org/wiki/ArsDigita#ArsDigita_Foundati...
https://archive.org/details/arsdigita_01_sicp/
전체 커리큘럼이 담긴 USB 키를 팔았는데, 누군가 ISO를 올려준다면 정말 좋겠음
https://web.archive.org/web/20190222145553/aduni.org/drives/ - Python과 대부분의 비-Lisp 계열 언어의 문제는 프로그램을 데이터처럼 다루기를 쉽게 허용하지 않는다는 점임
Scheme으로는 간단히 표현되는 것들이 다른 언어에서는 복잡한 연습문제가 됨
학생들은 밑바탕 개념에 집중하는 대신, Scheme에서는 신경 쓰지 않아도 되는 구현 언어의 세부사항에 집중하게 됨
- 이 강의는 Python으로 Scheme을 구현한 뒤, 다시 Scheme으로 Scheme을 구현함
-
cons/car/cdr를 람다로 구현한 것을 처음 봤을 때는 마법 같았음
다만 결국 언어 런타임이 키/값 딕셔너리를 구현하고 있고, 그 구현을 빌려 다른 자료구조를 만들 수 있다는 걸 보여주는 것이라고 봄- Elixir의 구조 분해 로직이 훨씬 더 흥미롭고, ES6의 약화된 버전이 훨씬 더 실용적이라고 봄
Elixir에서는 원하는 만큼 앞에서 꺼낼 수 있음 - 키/값 딕셔너리가 아니라 포인터만 있으면 됨
동작이 없는 클로저는 닫힌 변수에 대한 포인터일 뿐이고, 포인터 2개를 가진 클로저는car와cdr를 얻을 수 있는 쌍임
런타임은 가리키는 대상을 정의 바깥에서도 사용할 수 있게 해야 하므로 탈출 분석, 가비지 컬렉션 등이 필요하지만, 딕셔너리는 필요 없음
- Elixir의 구조 분해 로직이 훨씬 더 흥미롭고, ES6의 약화된 버전이 훨씬 더 실용적이라고 봄
-
최근에 정리 증명에서
0 != 1같은 것을 증명하려면 Church 인코딩만으로는 안 되고 귀납적 자료형이 필요하다는 개념을 접했음
여기에 관련 내용을 대충 올려뒀고, SICP에 대한 별개의 비판도 같이 있음: https://intellec7.notion.site/Drinking-SICP-hatorade-and-why...
“모든 것이 그냥 함수”라는 관점의 한계를 더 잘 이해하고 싶음- 부등식에 대해 활용할 수 있는 다른 구체적 사실이 있다면
0 ≠ 1을 증명할 수 있을 것 같음
f = g -> f x = g x라는 정리에서 추론해 오른쪽에 부등식 사실을 만들고, 그 대우를 취할 수 있음
Church 숫자 사이의 부등식은 부등식에 대한 다른 사실 없이 직접 증명할 수 없다는 말은 맞아 보임
반면 귀납적 자료형에서는 증명 시스템이 같은 귀납적 타입의 두 구체 인스턴스에서 가장 바깥 생성자 적용을 재귀적으로 제거하며 동등성 또는 부등성을 직접 “관찰”할 수 있음 - 정리 증명에는 Sigma 타입과 Pi 타입, 그리고 어떤 형태의 동등성 개념이 필요함
Scott 인코딩이나 Church 인코딩으로 그걸 달성할 수 있는지 궁금함 - 고친 링크: https://goosetaco.notion.site/Drinking-SICP-hatorade-and-why...
- 부등식에 대해 활용할 수 있는 다른 구체적 사실이 있다면
-
책 자체는 이미 여기서 논의 중임: https://news.ycombinator.com/item?id=42157558
링크가 그 페이지의 시작이 아니라 아래쪽 논의로 가는 이유가 있는지 궁금함
이 글을 기존 논의에 합칠 수 있지 않을까 싶음 -
David Beazley는 Python 세계에서 꽤 전설적인 인물이고, 이 강의는 처음엔 놀라운 아이디어처럼 보였지만 2초쯤 생각해보니 완벽한 조합처럼 느껴져 다음 강의에 등록했음
핵심은 이런 형태가 앞으로의 소프트웨어 엔지니어 계속 교육 모습이 될 것 같다는 점임- 5장의 컴파일을 다루지 않았다면 SICP의 가장 좋은 부분을 다루지 않은 것임
-
“the substitution model” 섹션의 코드에 오타가 있음
("+", ("fib", ("-", "n", 2)), ("fib", ("-", "n", 1))),
정의된 것은fibonacci이고fib는 정의되지 않았으니, 두fib호출은 분명fibonacci여야 함
실제 GitHub 저장소의 코드는 맞음: https://github.com/savarin/pyscheme/blob/0f47292c8e5112425b5... -
SICP는 훌륭하다고 생각함
다만 더 배우고 수학을 더 공부할수록 관계가 더 근본적인 원시 개념이라는 결론에 가까워졌음
모든 함수는 제한된 형태의 관계로 표현할 수 있지만, 그 반대는 상당한 추가 장치를 넣지 않으면 성립하지 않음
관계형 데이터베이스와 SQL이 관계형 프로그래밍의 가장 잘 알려지고 성공적인 예이긴 하지만, 이 영역은 여전히 크게 미개척이라고 봄
지금 관심은 프로그래밍 언어 설계보다는 아주 어린아이들에게 수학의 기초를 가르치는 쪽에 더 가까움
이상하게도 “크다” 같은 술어를 1항 관계로, “보다 크다”를 2항 관계로 가르치는 편이 같은 개념을 함수로 잡아내려는 것보다 훨씬 쉬움 -
“모든 것이 함수”라는 관점은 지나치게 단순화되어 있고 자주 도움이 되지 않아 별로 좋아하지 않음
예를 들면 캐시, RAM, 디스크 등에 들어가지 않는 함수가 있고, N-wayJOIN이나 탐색/매칭처럼 빅오가 폭발하는 함수가 있으며, 비멱등성을 포함한 부작용 있는 함수도 있음
함수에 대한 부채널 공격을 생각하는 사람은 거의 없음
날짜, 시간, 지속 시간 등에 의존하는 비결정적 함수도 있고, 함수는 중간에 실패하거나 우아하게 실패하지도 않음
공유 자원 풀을 쓰는 다른 “함수”에 영향을 주는 자원을 소비하지 않는다고 가정하기도 어려움
함수 인자는 임의로 크거나 복잡할 수 있지만 현실에는 한계가 있고, 그러면 포인터가 필요해지고, 다시 웹이나 디스크 같은 원격 참조가 필요해짐
언제 멈추라고 말해주면 좋겠음, 계속 말할 수 있음- 지나친 단순화가 때로는 훌륭할 수 있음
이 경우 람다 계산 모델은 “모든 것이 그냥 함수” 접근의 기반이며, 매우 단순하면서도 튜링 기계 등에 비해 다루고 추론하기 쉬워서 좋은 계산 모델임
그래서 대부분의 컴퓨터 논리와 증명 시스템의 기반에 있음 - 그중 많은 것은 함수로 모델링할 수 있고, 실제로 그렇게 작성되기만 하면 됨
어떤 함수가 자원을 필요로 한다면 그 자원을 요구하게 만들면 되고, 날짜/시간에 의존한다면 날짜/시간에 의존하게 만들면 되며, 비결정적 값을 반환한다면 비결정적 값을 반환하게 만들면 됨
함수형 프로그래밍 접근이 빛나는 이유 중 하나는 이런 것들을 진지하게 다루도록 강제하기 때문임
암묵적으로 공유되는 자원을 쓰고 싶다면 그것을 모델링해야 하며, 암묵 공유 자원에 의존하는 “함수”는 실제 함수와 명시적으로 구분됨 - 여기서 함수라고 부른 것의 절반은 실제로는 함수가 아니며, 함수형 프로그래밍 세계에서도 많은 사람이 함수라고 부르지 않을 것임
오히려 절차에 가까움
함수는 절차이지만, 모든 절차가 함수인 것은 아님 - 이론적 목적에서는 그것이 유일한 희망이라고 봄
- 지나친 단순화가 때로는 훌륭할 수 있음