얼마 전 코딩도장에서 N-Queen 문제를 손대본 적이 있다. 유명하고 어려운 문제라서 많은 사람들이 도전했고, 그만큼 효율적인 코드도 많이 알려져 있다. ... 코딩도장에서는 나이브하게 Coffeescript로 만들어 보고, 집에 와서는 요즘 연습 중인 D로 다시 만들어 보았다.
속도를 위한 최적화는 별로 없고, 그냥 무난하면서 Out-of-memory를 띄우지 않을 정도의 코드만 만들고 끝내야지 ~ 했는데, 테스트가 계속 실패한다. 각 Row에서 퀸의 위치를 Bit flag로 나타내는 식으로 보드를 저장하고 있었고, 실패하는 코드는 "현재 Queen의 위치 리스트" + "이번 Row에 놓고 싶은 Queen의 위치" 를 받아서 유효한지를 돌려주는 함수 부분이다.
테스트는 그냥 가볍게 이번에 놓고 싶은 Queen의 위치 - Row, Col - 에서 위로 한칸씩 올라가며 3개씩 확인하는 방식으로 했다.
cur.reverse 가 뒤집힌 녀석을 리턴하는건지, 스스로를 뒤집는건지 잘 모르겠지만 cur 에 변화가 있다면 저기 달아놓은 in qualifier 가 못한다고 말해주겠지 - 라고 생각했다. in 은 const 를 강제하니까. 그리고 이녀석은 컴파일이 된다. 헤헤 안심 ~하고 다음으로 넘어가보자 했는데 ... 계속 테스트가 실패하네?
알아보니 reverse는, 1. 스스로를 뒤집고, 2. 그 뒤집은걸 리턴, 도 하는 녀석이다. 배열의 property 를 참조하면 배열 자체가 뒤집혀 버리는 상황. -_-; 뭐 이래? 거기까지면 괜찮은데 왜 컴파일이 된거야?. 어이가 없어서 위의 in 을 immutable 로 고치고 테스트 했는데, 역시 컴파일이 되고 실행도 된다. 아놔... ;; 결국 side-effect가 있는 property는 immutable, const 를 그냥 씹어 드신다는 뜻.
이 코드가 컴파일+실행된다는 사실을 발견. ... 실행하면 [1, 2, 3] [3, 2, 1] 을 차례로 찍는다. 뭐라굽쇼? property 에 대해서는 수정 불가 조건을 검사하지 않는 것 같은데, 그 property가 side-effect 만땅 ... 이라는 암울한 상황이다. IRC에 가서 물어봤더니 돌아오는 대답은,
Looks like a bug
Those properties like reverse, sort will be deprecated.
였다. 으으, 아직 D 두번째 스펙이 불안정하다고는 하지만 이건 좀 지나친 거 아닌가 싶기도 하고.
훌륭한 사람이 삽을 떠서 결과가 곧 나올 것 같은 경우에 이렇게 조바심을 내는 것은 부질없겠다. 하지만 그래도 급하다거나, 연습삼아 해보거나 할 수도 있으니 잠깐 생각만 해보자. 사용하기 위해 보는 thrift 라이브러리는 크게 runtime 과 compiler 로 나눌 수 있다.
compiler는 protocol definition파일을 읽어서 목적 언어로 쓰여진 "코드" 를 뱉어주는 일을 한다. 이렇게 생성된 코드가 해당 언어로 준비된 "런타임 환경" 에서 돌아간다.
D는 C 혹은 C++과는 어느 정도 인터페이싱이 가능하다. 그렇다면, 무임승차 혹은 거기까진 안되더라도 코드 재사용을 생각해볼 수 있다. thrift 의 여러 생성 옵션 중에서 c_glib, cpp 정도가 살펴볼 만 하다. 어느 쪽이건 간에 D쪽에서 만지기 위해서는 interface를 만들어야 한다. 최소한 *.h -> *.di 로의 컨버팅이 필요한 것.
c, c++ 양쪽은 별도의 런타임 환경이 준비되어 있으며 include 되는 헤더도 다르다. 어느 쪽에 손을 대더라도 해야 하는 일은 별로 달라지지 않는다.
a. Runtime environment
ㄱ. Make custom environment with D
그냥 C 혹은 C++쪽 코드를 보고 D로 구현해버린다. 직관적으로, 여기서 API를 잘 설계하면 Compiler 쪽 작업이 대단히 쉬워질 것 같다.
ㄴ. Make interface to c_glib or cpp runtime
c_glib 혹은 cpp 를 위해 준비된 환경으로 인터페이스를 만들어서 가져다 쓴다. 이 경우 손으로 *.h -> *.di 컨버팅을 해주는게 좀 귀찮은 작업이 될 수 있다. ... 하지만 그냥 새로 만드는거에 비하면( .. )
b. Compiler
t_c_glib_generator, t_cpp_generator 이런 애들이 있다. runtime 쪽에서 직접 구현한다를 선택한 경우에는 그냥 t_d_generator 를 만들면 된다. 다른 생성기를 참조하고, 런타임 API가 잘 설계되어 있으면 그렇게 어렵진 않을듯 하다. 만약 runtime을 가져다 쓴다는 결정을 했다면, t_d_generator를 직접 만들 수도 있지만 다른 선택도 가능하다. 기존 Compiler 에 끼어들어서 *.di/*.d 파일을 *.h/*.cpp 파일과 함께 만들어 버리는 것. 지저분하지만, 런타임을 공유한다면 API도 같기 때문에 이렇게 작업할 수도 있다. 하지만, 이건 thrift 버전업이 있거나, 혹은 contribute를 하고 싶다면 심각한 문제가 된다. 다른 사람과 공유하지 못하는 삽이라니 가슴 아프지 않은가. dirty and quick.
David Nadlinger의 제안에서는 Free-riding없이 직접 구현을 택했다. CTFE(Compile Time Function Execution)도 고려중인 듯. 이 경우 일반적인 타입의 serialize/deserialize는 별 문제가 아닌데, rpc server/client implementation 에서 비동기를 구현할 때 곤란할 수 있다(포기하고 그냥 스레드 만세 할 수도 있지만). 현재 latest인 D2.053 epoll|kqueue|iocp 등이 core.sys.posix|osx|windows 에 없다. D1을 고려한다면 tango를 볼 수도 있지만, 미래를 생각한다면 볼때 D1+Tango로 새 프로젝트를 시작하는건 삽질 ( ... )이라는게 중론이다. libevent의 D port도 아직은 없어뵈고... 그렇게 어렵진 않겠지만, 역시 이런 인터페이스를 뚫는 작업이 좀 지루할 수 있겠다.
결론적으로 무임승차는 못할 듯. 어느 쪽으로 가더라도 손이 가는 작업이 필요하다.
core.sys.posix.poll 도 있는데 core.sys.posix.epoll 정도 있어주면 어디가 덧나나. 이미 삽이 준비되지 않았으면 github 에 자리 하나 차리고 해보려 했으나 일단 기다리기로 결정함. 크릉크릉. thrift 와는 별도로 libevent 헤더들을 D interface 파일로 만들어 두는건 해볼만 할듯. 어렵지도 않고, 시간만 들이면 되는거고, 써먹을 데도 많다. 이미 있는데 못찾는건가 ... ... ...
debian testing 을 쓰고 있는데, 어느날 갑자기 css, js, images 기타등등을 못읽어와서 페이지가 안이뻐( .. ) 졌다. 뭐컨텐츠 자체는 문제가 없어서 그냥 뒀는데 - 안이쁘니까 괜히 찜찜한 상태 .. ..... ......... 간만에 귀차니즘을 이기고 약 10분을 투자해서 수정.
gitweb.css, gitweb.js, git-logo.png 등등을 읽지 못하게 되어서 생긴 문제이다. 이게 버전 업글이 되면서 기본 디렉토리나 설정이 바뀐건지는 잘 모른다 ... =_= 대체 언제부터 삑살났던거지... 혹시 나처럼 귀차니즘에 시달리는 사람이 있다면 참고를.
1. 못가져오는 파일들은 /usr/share/gitweb 여기에 있다.
2. ln -s /usr/share/gitweb /var/www/gitweb
3. Add Alias /r/ "/var/www/gitweb" to /etc/apache2/site-enabled/gitweb - 이건 사람 나름이지
Programming in Clojure 를 지르고 3일. 이건 뭐하는 녀석일까? 라는 세근세근한 기분으로 책을 한번 주욱 훑어보고, 몇 개의 짧은 코드를 만들어 보았다. 더불어 이맥스에도 붙여 보고 ... 그리고 천천히 다시 읽기 시작해서 절반 정도. 여기까지 보고 난 감상(?)을 정리한다. 나머지 절반을 더 읽고 나서는 지금의 생각이 번복될 수도 있다.
* 설탕문법으로 가득하다
이건 좋다/나쁘다로 나누기는 좀 어려운 부분이다. 전체적으로는 리습 방언에 항상 따라다리는 Bad-point 인(역시 사람차가 나지만) **괄호난무**에 대한 방비가 좀 있다. 예를 들어서,
(let ((a 1)
(b 2))
(+ a b))
이런 스킴(리습?)코드가 클로져에서는
(let [a 1
b 2]
(+ a b))
이렇게 풀린다... 잘 보면 괄호를 하나 덜 사용했다. 이것 말고도 괄호의 Depth를 줄이고자 한 노력이 엿보인다. ... 하지만 이로써 <코드 자체가 Parsing tree> 라고 하기에는 조금 어려운 듯도 하고, 아닌 듯도 하고 ... 책에서는 가끔 저 Bindings 부분을 이렇게 풀어놓곤 하는데, 처음 봤을때 헷갈려서 약 3분 정도 정신을 못차렸다.
(let [a 1 b 2] (+ a b))
이렇게 써놓으면 a<-1, b<-2 처럼 안보이잖아... ㅠ_ㅠ 당황했다규... ㅠ_ㅠ
그 외에도 "한 가지 일을 하기 위한 여러가지 설탕" 들이 존재한다. 내가 처음... 은 아니고 가장 오랫동안 만지고 있는 리습 방언인 스킴은, SICP를 통해서 접해서인지는 몰라도 "문법을 기억 할 필요가 없는 언어" 였는데, 클로져는 그렇지는 않다. 물론 교육용으로 디자인된 언어랑 비교하는거니 공정하진 않지만, 이렇게 기억할게 많은거(그래봐야 자바나 C++에 비하면...) 어려운데... 그래도 손에 조금만 익숙해지고 나면, 큰 어려움 없이 다른 리습 계열 언어에서 만들던 코드를 클로져로도 만들 수 있다.
주의할 사항은 - 책에서도 주의하라고 나온다!
(if '()
true
false)
빈 리스트가 nil 이 아니다, 빈 리스트는 참이다, 라는걸 주의해서 기억해야 한다.
그 외에, 스킴과 들여쓰기 컨벤션이 좀 다른 듯 한데 이거 은근히 신경쓰인다. ㅠ_ㅠ
* 자바와 이다지도 가깝다니...!
난 자바에 그다지 익숙하진 않지만, 클로저로 코드를 만들다가 - 어... 이거 빌트인이 있을 것 같은데 모르겠어...?? - 정도의 느낌이 들면 그냥 javadoc 을 띄워 찾은 다음에 자바쪽 API 를 사용해버릴 수 있을 정도로 쉽다. JVM위에 올라간다는 건 이런 것인가... 우왕 +_+
내가 자바에 조금 더 익숙했더라면, 이것 저것 많이 해볼 수 있었을텐데 - 자바랑 친하게 만들어놔도 내가 자바랑 안친해서 ... 그래도 방대한(맞지?) 자바쪽 라이브러리들을 쉽게 가져다 쓸 수 있다는 점은 매력적일듯. 거꾸로도 마찬가지이다. 클로져로 만든 함수들을 자바에서 가져다 쓰는 것도 쉬운 듯 하다.
-- Clojure's functions implement Runnable and Callable.
* Functional?
책에서는 몇 장에 걸쳐 장황하게 Sequence를 다루는 여러가지 빌트인들에 대한 이야기를 한다. 그 자체는 Lisp 방언이라는 관점에서 별로 특별한 것은 없고, 그 여러가지 Seq-able 아이들이 Java collection 과 어떻게 연결되는지에 대해 간간히 나오는 설명이 꽤 중요할 것 같다. ... 자바쪽의 피쳐를 잘 가져다 써야 클로져가 의미있는거잖아? - 자바와의 Integration 없이 언어 스펙만 보면 딱히 특별한 점은 그다지 ... - STM이 언어 스펙이라고 하면 좀 도드라져 보일 수도 있겠지만! ... 혹 이런 류의 개념들에 익숙하지 않다면 - Pure java/C/C++ programmers - 책에서 설명하는 sequence(Lazy evaluated!)에 대해 잘 읽어보고 감을 잡는 것도 좋을 듯 하다. 혹시나 파이썬 유저라면, itertools 패키지를 잘 뒤져보면 ... ...
* Rich Builtins/Libraries
(아마) SICP만 봐서 그런거겠지만, 언어에 딸려오는 기본적인 피쳐들이 (Scheme 에 비해서) 풍부하다는 사실에 감동. SICP에서 하나씩 직접 구현했던... 많은 것들을 그냥 쓸 수 있다. 사실 이건 클로져가 아니라 clisp 에서도 마찬가지일텐데(ㅠ_ㅠ), 그래도 좋은건 좋잖아.
게다가 - 위에서 한 이야기와 중복이겠지만 - 자바쪽 API가 거의 언어 빌트인 수준으로 노출되어 있어서 아예 레퍼런스로 javadoc을 띄워놓고 작업해도 될 정도라서, 우왕ㅋ 굿ㅋ.
남은 절반은 다음 주 중에 읽고 리뷰 2/2 를 후다닥 쓰자.
-----
클로져와 직접적인 연관이 있는 것은 아니지만, 책에서 기적억나는 구절을 하나만 뽑아본다면:
-- Don't write ugly code in search of speed. Start by choosing appropriate algorithms and getting your code to simply work correctly. If you have performance issues, profile to identify the problems. Then, introduce only as much complexity as you need to solve those problems. - 격하게 공감 =3=3
리팩토링, TDD 같은 류의 책에도 조끔씩 핀트는 다르지만 큰 맥락에서는 비슷한 얘기들이 많이 등장한다. Complexity는 보이지 않는 곳에 추상화시켜 던져 둘 수 있는 것이 좋고, 그렇다면 Simple and Naive code 를 나중에 바꿔치기 하는 접근이 어느정도 <추상화를 강제/유도> 할 수 있다.
실제로 해보면 이런 녀석이 남는다. 왜 그럴까? 참고로 - for x in numbers[:]:, 혹은 for x in list(numbers): 이렇게 for statement 를 고치면 의도대로 동작하긴 한다. 그렇다고 좋은 코드라는건 아니고 ...
무슨 언어에선가는 반복중인 컨테이너를 고치려고 하면 컴파일 타임에 알려준다고 하는 것 같았는데 ... 이런 류의 버그는 은근히 찾기가 어렵다. 간단한 (정말 기초적인) 테스트 코드 정도로도 조기 발견이 가능한 부분이고, 가능하면 테스트에서 잡히기 전에 코더의 본능이 먼저 경고해 주어야지.
그래도 STL 에는 "이 녀석은 반복자를 무효화시킵니다 ~ 조심하세요 ~" 라고 레퍼런스가 말해주긴 하는데, 파이썬 레퍼런스엔 그런 이야기가 없었던 것 같다. - 날로 읽어서 확실치는 않다.
... 사실 이건 SICP 3장이 떠올라서 주절거리는 글이다. 저 비슷한 느낌으로 스트림을 생성해가는 ㅂㅌ적인 코드들... OTL. compound but tricky 라는 느낌이랄까. 내가 수학을 못해서 그렇게 느끼는걸지도 모르지만.
아 피곤해. 두서없는 글이 되었구먼. 결론은 내가 부족하다는 거? (저건 내가 만든 버그는 아니다)
빠르다길래 믿고 그냥 가져다가 쓰고 있었는데, 테스트 코드를 돌릴 때는 모르다가 진짜 데이터( ... ) 들이 들어가기 시작하니 시스템 퍼포먼스가 말도 안되도록 떨어졌다. 여기서 한번 피 토해주고 (쿨럭) ... 아, 빠르다는건 <XML에 비해서>인 듯 하다. 이거 머야 대체! 하면서 프로파일링을 해봤는데, ... 응?
SerializeToString, ParseFromString
이녀석들이 나란히 1등, 2등?
패인은 repeated 에 있었다. 몇 가지 부가 정보와 함께 수십만(최대 백만 정도)개의 32bit integer 들을 빠르게 주고받는 시스템인데, 이걸 repeated uint32 id_list; 같은 식으로 만들어 사용했다. 이 녀석을 Serialize, Parse 하는 부분이 대박이었던 것. 테스트 할 때에는 적당히 수백개 정도만 넣어봐서 차이를 잘 못느꼈는데, 이게 커지니까 ... ... -_-a
그래서 결국에는, array.tostring() 을 사용해서 뽑은 문자열을 프로토콜 버퍼에 bytes 타입으로 붙여서 보낸 다음 (with packed_ prefix) 사용하는 시점에 array.fromstring()푸는 형태로 고쳤다. 일단 이렇게 속도는 잡았는데 ... ... 크흑, 귀찮아. 프로토콜 버퍼가 생성한 객체를 확장해서 써야 할 것 같다.
회사에서 새로 만드는 시스템을 크게 몇 개의 컴포넌트로 잘라서 디커플링을 시도했다. 시스템 전체를 흐르는 데이터가 거치는 각 단계마다 투입되는 자원량을 조절할 수 있어야 하며, 머신도 나뉘어야 ... 하고 코드 의존성도 님하 좀 ㅠㅠ
이 과정에서 함수 인자, 혹은 글로벌 자료구조 등으로 공유하던 데이터를 넘겨줄 방법 - 그러니까 IPC - 이 필요하게 되었고, 대부분의 정보 전달을 Queue with Persistent layer 로 대체할 수 있었다. 반응성보다는 단위 시간당 처리량이 중요한 시스템이라서 ...
여러개의 큐 서버가 만들어지고, 여기에 데이터를 넣고 빼고 하게 되는데 이 과정에서 딸려오는 귀차니즘이 소켓통신. 그리고 몇 가지 이유로 인하여 큐 서버를 포함하여 관련되는 코드들이 자바/C/C++/Python 들이 섞일 수 있는 가능성을 가지게 되었다. (사실 이게 디커플링의 의도중 하나이긴 한데 ...)
파이썬만 있다면야 pickling 이라는 편한 방법이 있지만, 다른 언어가 들어오면 조금 골치아파지고... 이런 상황에서 Google protocol buffer 가 떠올라서 가져다 사용. 가장 큰 특징은,
1. Code generation
- 유피넬의 누군가(...)가 세미나에서도 이야기했던 방법인데, Definition data structure -> Code to handle 방향으로 코드를 생성해 버린다. 코드 레벨에서 서버/클라를 포함한 이 데이터를 다루는 녀석들이 모두 공유할 수 있다.
- 코드 생성이기 때문에, 오타라던가 ( ... ) 양쪽 코드의 불일치 등으로 생기는 문제는 거의 제로
- 데이터 정의가 바뀌면 코드 생성->컴파일->링크 를 다시 해야 한다
- 이 툴(그러니까 google protocol buffer)를 도입함으로써 생기는 제약이 별로 없다 - 프레임웍을 가져다 쓰는거에 비하면야...
2. Structured data definition
- 일반적인 클래스 디자인과 비슷하게 데이터를 정의할 수 있다
- 만들어진 데이터 타입들을 조합하여 복잡한 블럭을 만들 수 있다
3. Language support
- 고려중인 Java/C++/Python 세가지 모두 코드를 찍어내준다
- 언어와 상관 없이 이 코드들은 서로 호환되는 데이터를 만든다 (Serialize/Parse 결과가 일치한다는 뜻)
그리고 부가적으로 빠르다, 같은게 있긴 하다. ... 하지만 아직 확인해보진 않았으니 뭐 ... 그래봐야 내가 직접 만든 코드보다 백만배 빠르진 않겠지... 훌륭한 점은 퍼포먼스 저하를 최소한으로 막고(과연 그럴까?는 코드를 까봐야 알겠지만...) 사용하기 편해졌어! 를 확보한 것, 이라고 생각할 수 있는데.
뭐 이 장점들은 확실히 강력한 것 같다. 단점은... Composite type 에 대한 assignment를 막아놓은게 좀 귀찮은데, 이건 내부 구현에 따라오는 이슈인 것 같다. 로직에서 다루는 데이터 타입과 데이터 주고받기에 쓰이는 타입이 다르면 - 로직에서 프로토콜 버퍼 객체를 사용하지 않으면 - Send/Recv 할 때 데이터의 복사가 한번 일어나게 되는데 이건 좀 곤란한 것 같다. ... 하지만, 잘 살펴보면 보내거나 받는 데이터를 Stream으로 만들어서 해결할 수 있을 것 같기도 한데, 지금은 튜토리얼만 슥 보고 대충 가져다 쓰는거라 뭐 ... 복사 없이도 처리가 가능할 것 같긴 하니까 이건 패스.
큰 코드조각은 아니지만, 유용한 걸 잘 만든다. 구현 자체는 논외로 하고, 컨셉이 좋지. (더불어 리카님도 굿 ㅋ) 어느 정도에서 Abstraction layer를 만들어야 사용성을 최소한으로 해치면서 Drawback을 줄일 수 있을지 - 에 대한 감이 좋아 보인다. 이런건 좀 배우자.
손에 잡혀서, 이번에는 - 이라는 생각으로 한번에 끝까지 읽었다. 대부분이 '보여주기' 이기 때문에, 쉽게 따라갈 수 있어서 시간은 얼마 안들었다.
결과 ... 를 말하기 전에 일단 확실히 할 것:
1. TDD와 Unittest는 다른거다
2. 테스트 먼저 쓴다고 TDD는 아니다
그리고 결과:
클린 코드는 테스트 주도적으로 만들어질 수'도' 있다.
Clean code that works - 가 만들고 싶다는 켄트백 씨는, 한줄짜리 테스트로부터 짧은 싸이클로 코드가 변해가는 과정을 보여주었다. 아, 그래, Impressive. 하지만 그 뿐. 예를 위해 극단적으로 짧은 주기를 가져가긴 했지만, 그렇지 않은 경우라고 해도 분명 Pencil and Paper 로 시작하는 것이 Clean code that works 로 가는 지름길이라고 생각한다. (아, 그래 그냥 내 생각이 그렇다는 거다)
유닛 테스트 자체의 효용성과, 테스트 코드가 코딩 처음 단계가 된다는 아이디어, 테스트 추가-컴파일-테스트 통과-리팩토링 의 사이클 ... 대부분의 내용은 좋은 내용(이고 공감도 하는 내용)이긴 한데. Test-driven 에 대해서는 글쎼(?????). 예가 좀 구질구질해서 그런가 책이 보여주는 예제(특히 첫번째 currency) 자체가 TDD에 집착하다 말아먹기 - 그러니까 It passes the test, so it might work. But I can't tell why - 딱 좋아 보인다.
물론 켄트백 아저씨가 그런 건 아니겠지만, 의도했건 안했건 TDD는 꽤나 Dogmatic(적절한 한글/한자어 표현을 못찾겠다. 독단적인... 이랑은 쫌 뉘앙스가 다른데 ...)한 분위기로 쓰여 있어서 읽고 쉽게 집착하게 되는 책이다. Limitation이 이런게 있어 라고 말해주고는 있지만, 신경 안쓰고 집착하기 쉽다는 이야기. 그리고 그 집착이 묘-하게 꼬이기 시작하면 문제가 시작된다. 도구는 도구일 뿐, 집착하지 말자. - 내 얘기는 아니고 ...
경우에 따라선 잘 맞을 때도 있겠지 뭐 ... (경우에 따라선 안 맞는 때도 있겠지 - 라고 하지 않았다는데 주목)
Unittest의 적극적 도입 - 이 개발 프로세스를 획기적으로(정말 획기적으로!) 바꾸었다고는 생각하지만, TDD라는 방법론 자체는 도구의 하나로 제시된 것이고 - 패러다임 Shift 을 야기할 정도로 강력한 것은 아니었다, 라는 내맘대로 총평. 위에서 말했듯이 (Unittest의 적극적 도입 != TDD)
... ... ...
여담으로, 이미 위에서 들키긴 했지만 디군은 PP(Pencil and Paper) Driven Developement 에 집착하는 것 같은데, 역시나 이것도 꽤 곤란한 경우가 있는 것 같다. 근데 다른 집착과 마찬가지로 (알면서도) 쉽게 안고쳐진다. 쿨럭쿨럭 ---
회사에서는 소스코드 저장소로 svn 을 사용하고 있다. 음... 음... 그런데, 내 로컬머신(...)과 개발서버의 내 홈디렉토리 양쪽에서 변경이 일어나는데 이걸 서로 싱크시키거나 할 때 꽤나 귀찮아진다. 심지어 귀차니즘이 절정에 달할 때에는 그냥 central repository에 커밋->저쪽에서 update 이런 식으로 소스코드를 공유(당연히 테스트 되지 않은 코드)하기도 하고 ...
그런데 어느덧 head revision이 2만3천을 넘어가기 시작하고, 이런 저장소 오염을 다른 사람들에게 보이기 부끄럽기도 하고 ... 하여 생각한 것이 git. 아, 분산 저장소를 만져볼 때가 됐구나, 라는 느낌표가 왔다. 지금까지는 튜토리얼 정도만 보고 필요성을 크게 느끼지 못해서 그냥 그런게 있지, 라고만 넘어왔는데, 이제 때가 됐다.
-----
약간의 삽질: git-svn 패키지를 설치했는데 git-svn 커맨드가 없다? git svn 이런 식으로 git 안의 서브커맨드로 쓰면 된다. ... 한참 헤맸다. ㅠㅠ
-----
이렇게 가져와서: git svn clone repository_path
이렇게 저장소와 동기화 시키고: git svn rebase
이렇게 커밋하면 된다고: git svn dcommit
이건 중앙 저장소와 이야기 할 때만 하면 되고, 그냥 내 작업은 바로바로 로컬에서 git 으로 할 수 있다.
진작 이럴껄...
-----
그런데 그냥 clone 가져오면 리비전 2만3천개를 차례대로 가져오게 되서 좀 시간이 걸린다. 헤드만 가져올 수도 있긴 한데 ... 뭐, 그냥 ...
ATI를 선호하는 디군이 왜 이번에 Geforce9500GT라는 허접한 물건을 샀는가? - 디군의 개인적인 판단임
CUDA API 를 만져보고 싶었기 때문이다.
그래서 어제 집에서 사용하는 우분투 머신에 CUDA 개발툴이라는 녀석을 깔았는데, 아주 약간( ... ) 삽질을 했다. 나중에 다시 이 짓을 할 수도 있으므로 정리해두자.
-----
1. 준비물
* CUDA 지원하는 NVIDIA 그래픽 카드 Geforce 8***, 9*** 이상은 모두 된다. 여기(http://kr.nvidia.com/object/cuda_learn_products_kr.html)에 자신의 그래픽카드가 있으면 된다. 나는 Geforce 9500GT 를 준비했다.
2. 설치할 때 귀찮은 점들
여기(http://kr.nvidia.com/object/cuda_get_kr.html) 서 시키는대로 Driver->Toolkit->SDK 순으로 설치하면 된다. 사실 순서가 중요한 것은 아니다. 디군의 경우는 삽질을 좀 해서 드라이버를 가장 나중에 했지만 별 문제는 없었다.
윈도우 환경이라면 (해보진 않았지만) 별 문제가 없을 것 같고, 리눅스 환경에서 약간 귀찮은게,
* NVIDIA 드라이버를 설치할 때 X 가 떠 있으면 안된다
* 반드시 사용중인 드라이버를 제거해야 한다
두가지이다. 나는 저 두번째 이유로, 삽질해서 드라이버를 마지막에 다시 설치했다.
- 우분투의 경우 시스템->관리->하드웨어드라이버, 에 가서 현재 사용중인 드라이버를 쉽게 deactivate할 수 있다.
- X 내리고 드라이버 설치: Ctrl+Alt+Fn 으로 세션을 옮겨다닐 수 있는데, 보통 1-6까지는 터미널이고 7번에 X가 떠 있다. 아직 예외는 본적이 없다. 이걸 이용해서 Ctrl+Alt+F1 로 1번 세션에 가서 로그인 한 다음
/etc/init.d/gdm stop
--- install driver as root
/etc/init.d/gdm start
해주면 된다. 그냥 깔끔하게 리붓해도 굿. 사용하던 드라이버를 제거해주지 않았다면, 여기서 커널이 "아썅 왜 버전 두개가 섞여있어!" 라면서 짜증을 내면서 디바이스를 못잡는다. 주의하자. 게다가 저 짜증을 바로 보여주는게 아니라 커널 로그를 까봐야 나온다. 겉으로 보기에는 "그냥 못잡네" 정도로 보이기 때문에 ... -_-
참고로, Driver, Toolkit, SDK 모두 executable이므로, 다운받고 나서 실행권한이 없다고 하면 chmod +x filename 혹은 sh filename 같은 식으로 실행해주면 된다. 이거 물어보는 녀석이 있더라고 ... -_-a
3. Toolkit, SDK 설치
그냥 다운받은 파일을 실행하면 된다. 아 쉽다. SDK는 기본 경로가 사용자 로컬이다. 모든 사용자를 위해 설치하고 싶다면, root 권한으로 어딘가 적당한 곳에 설치해주면 되겠지만 굳이 그럴 필요가 있을까 ... (있을지도)
4. Build SDK
SDK를 설치한 경로가 ~/NVIDIA_GPU_Computing_SDK 라고 하자. (기본이다)
cd ~/NVIDIA_GPU_Computing_SDK/C
make
이렇게 빌드하면 되고, 빌드 후 생기는 예제코드의 실행파일들은 ~/NVIDIA_GPU_Computing_SDK/C/bin/linux/release 에 생긴다. CUDA가 제대로 동작한다면,
dgoon@katy:~/NVIDIA_GPU_Computing_SDK/C/bin/linux/release$ ./deviceQuery
CUDA Device Query (Runtime API) version (CUDART static linking)
There is 1 device supporting CUDA
Device 0: "GeForce 9500 GT"
CUDA Driver Version: 2.30
CUDA Runtime Version: 2.30
CUDA Capability Major revision number: 1
CUDA Capability Minor revision number: 1
Total amount of global memory: 536150016 bytes
Number of multiprocessors: 4
Number of cores: 32
Total amount of constant memory: 65536 bytes
Total amount of shared memory per block: 16384 bytes
Total number of registers available per block: 8192
Warp size: 32
Maximum number of threads per block: 512
Maximum sizes of each dimension of a block: 512 x 512 x 64
Maximum sizes of each dimension of a grid: 65535 x 65535 x 1
Maximum memory pitch: 262144 bytes
Texture alignment: 256 bytes
Clock rate: 1.40 GHz
Concurrent copy and execution: Yes
Run time limit on kernels: Yes
Integrated: No
Support host page-locked memory mapping: No
Compute mode: Default (multiple host threads can use this device simultaneously)
A parameter represents a value that the procedure expects you to supply when you call it. The procedure's declaration defines its parameters.
Parameter는 당신이 프로시져를 호출할 때 함께 함께 주었으리라고 기대하는 값을 의미한다. 프로시져 선언에서 Parameter를 정의하게 된다.
An argument represents the value you supply to a procedure parameter when you call the procedure. The calling code supplies the arguments when it calls the procedure. The part of the procedure call that specifies the arguments is called the argument list.
Argument는 당신이 프로시져를 호출 할 때 Parameter에 전달하는 값을 의미한다. 호출하는 쪽의 코드에서 프로시져를 부를 때 Argument를 넘긴다. 프로시져 호출에서 Argument를 지정하는 부분을 Argument list라고 한다.
In practice, distinguishing between the two terms is usually unnecessary in order to use them correctly or communicate their use to other programmers
실제적으로, 이 용어들을 구분하는 것은 올바른 사용이라던가 다른 프로그래머들에게 그 용도를 전달하는 것 등에 별로 필요하지 않다.
the words actual and formal can be used to distinguish between an argument and a parameter, respectively. For example, the equivalent terms actual argument and actual parameter may be used instead of argument; and formal argument and formal parameter may be used instead of parameter.
actual 과 formal 이라는 단어가 argument, parameter 를 구분하는데 쓰일 수 있다. 예를 들어, actual argument, actual parameter 라는 용어를 argument 대신 쓸 수 있다. 그리고 Formal argument, formal parameter 라는 단어를 parameter 대신 쓸 수 있다.
이정도 되겠다.
요약하면, 함수 정의 등에서 넘어온 값을 받아서 함수 Scope 안에서 사용할 bound variable 을 파라미터(parameter, formal parameter, formal argument)라고 한다. 함수를 호출 할 때 함수 Scope 안에서 사용할 bound variable 에 넣어주세요 ~ 라고 주는 녀석들을 아규먼트(argument, actual argument, actual parameter) 라고 한다.
Caller의 눈으로 본 것이 Argument - Actual
Callee의 눈으로 본 것이 Parameter - Formal
번역할 때에는 파라미터 - 매개변수, 아규먼트 - 인수/인자/독립변수, 등으로 사용되는 듯 하다.
G.E.B. 에 나온 간단한 퍼즐. 더 읽기 전에 책에서 시킨대로 연습장에 끄적이며 좀 해봤다. 몇번 DAG을 그려보고 나서 "이건 안되는것 같은데?" 라는 생각이 들어서 증명, 혹은 납득할만한 설명을 만들어 보기 위해 고민. 음, 책을 더 읽기 전에 한번 써보고 넘어가고 싶음.
일단 규칙은, 이렇다.
처음, MI 라는 문자열를 가지고 있다.
I가 포함된 문자열은 뒤에 U를 붙일 수 있다.
Mx 형태의 문자열을 Mxx형태로 바꿀 수 있다.
문자열 안에 III 가 나오면 이를 U로 바꿀 수 있다. 반대 방향으로는 안된다.
문자열 안에 UU가 나오면 날려버릴 수 있다.
MU 를 만들 수 있겠는가?
MI -> MIU -> MIUIU -> ...
MI -> MII -> MIIII -> MUI -> ...
이런 식으로 가서 마지막에 MU 를 만들면 되는것인데... 불가능하다 - 고 생각한다. 왜 그렇게 생각하는지 한번 이 글을 읽는 당신을 납득시켜 보겠다.
문제의 constraint를 조금 느슨하게 바꾸어보자. 제약 조건이 더 약한 문제인데도 불가능하다면, 조건이 더 강한 문제는 당연히 불가능하다 - 자명하다. 자, 자, 문자열에 포함된 I의 갯수를 n 이라고 해보자.
시작할 때 n = 1 이다. n 은 0 보다 작아질 수 없다.
이건 자명하다. 그러면, 우리가 쓸 수 있는 규칙 4가지가 이 n을 어떻게 변화시키는지 보자.
1번 규칙: n -> n 2번 규칙: n -> 2n 3번 규칙: n -> n - 3 4번 규칙: n -> n
자, 조금 더 쉽게 바꾼 문제는 이렇다. n = 1 에서 시작해서 2를 곱하거나, 혹은 3을 빼는 것을 반복해서, n을 한번도 음수가 되지 않도록 유지하면서 n = 0 을 만들 수 있는가?
... 이렇게 바꾸면 바로 와닿지 않는다. 그래서 문제를 한번 더 바꿔 보았다. 이번에는 문자열 안에 있는 I의 갯수를 3 으로 나눈 나머지를 n 이라 해 보겠다. - 왜 하필 3인가? ... 어쨌든 마지막은 3을 빼서 n = 0 이 되어야 하므로(2를 곱해서 0을 만들려면 이전의 n=0이어야 한다 -_-) n은 반드시 3이 되어야 한다. 연쇄적으로 생각하면 일단 n 이 3의 배수가 되는 순간 0 을 만드는 방법은 자명해진다. 3을 계속 빼면 되니까. 그러니 3의 배수 만들기를 생각해본 것이다.
시작할 때 n = 1 이다, n 은 0, 1, 2 중 하나이다.
역시 이건 자명하다. 이제, n의 값을 각 규칙이 어떻게 바꾸는지 보자.
1번 규칙: (0, 1, 2) -> (0, 1, 2) 2번 규칙: (0, 1, 2) -> (0, 2, 1) 3번 규칙: (0, 1, 2) -> (0, 1, 2) 4번 규칙: (0, 1, 2) -> (0, 1, 2)
조금 더 이해가 쉽도록 중복을 제거하고 세로로 써볼까? 아래와 같은 두개의 mapping 이 가능하다.
0 -> 0 1 -> 1 2 -> 2
0 -> 0 1 -> 2 2 -> 1
이렇게 두 개의 mapping을 가지고 1을 0으로 보낼 수 있는가? 없다. 1, 2 사이에서만 왔다갔다 할 뿐. 즉, I 가 하나 포함되어 있는 문자열에서 I 가 3의 배수개(0, 3, 6, 9 ... 등) 들어있는 문자열은 만들어낼 수 없다는 것.
... 이정도면 스스로는 납득했음. 조금 더 formal 하게 쓰는 연습을 해도 좋겠지만 今日はここまで。
필요한 일이 있어서 MySQL 에 있는 데이터를 XML로 덤프하는 파이썬 코드를 만들었다. 약 30줄 정도 되는 간단한 코드인데, 눈으로 보기에는 문제 없어보이는 XML 파일이 계속 well-formed xml 이 아니라고 하는거다. XML library 를 가져다가 validation을 하면서 만든게 아니라 그냥 데이터만 템플릿에 맞게 찍어주며 덤프한거라서 ... 그렇다고 덤프 코드에 validation을 넣기도 귀찮고 해서... -0-
문제가 되는 라인은 <![CDATA[ ... ]]> 을 가지고 있다.
한국어, 일본어, 중국어 등이 섞여있는 데이터를 쓴다.
두가지로 미루어 추정해본 결과 CDATA에 넣어준 데이터가 있어서는 안되는 것이라 생각. 원래 CDATA가 이스케이핑 하기도 뭐하고, 이래저래 처리하기 귀찮은 애들을 그냥 때려박을때 쓰는 용도라고 생각해서 별 생각 안했는데, 그래도 xml 이 markup language라서 문제가 있을 수도 있을 것 같다. 어쨌든 텍스트를 넣는 것이니 텍스트에 태그를 닫는 문자셋이 나오면 그걸로 방법 아닌가!? 생각해보니 문제가 생길 소지가 다분하네... =_=
그래서 웹서핑 결과 이런 페이지를 발견. ... 어째 내가 하려던 것과 꼭 같은 일을 먼저 해보신 분이 적어둔 것이라 신빙성 짱! 그리하야,
def acceptable_char(c):
code = ord(c)
if code == 0x09 or code == 0x0a or code == 0x0d or \
(0x20 <= code and code <= 0xd7ff) or \
(0xe000 <= code and code <= 0xfffd) or \
(0x10000 <= code and code <= 0x10ffff):
return True
return False
def draw(start, center): current = start i = 1 while not close_to_center(current): pos = polar_to_euclid(current) stdscr.addstr(pos[1] + center[1], pos[0]*2 + center[0], "%s" % i, curses.A_NORMAL) current = get_next_coord(current) i += 1
코딩도장에서 풀고 있는 투명 필름 문제. UVA 에서 뽑아온 문제인데 ... 그쪽 스타일은 입출력이 귀찮아서 열심히 하기는 싫다. 그냥 내맘대로 문제를 줄여서(-_-a) scheme 으로 뚝딱. 아직 공부가 부족한가보다. ... 더 잘 할 수 있을텐데. 위키보다는 여기가 어울릴 것 같아서(문서가 아니라 코드!) 붙임. SICP 스타일로,
Representation 정하기 Selector 등 Abstraction layer 만들기 Logic 구현
의 순서를 밟았다. 실제로 Abstraction layer를 만들어둔게 디버깅 할때 꽤나 큰 도움이 되었다. <시작, 끝> 에서 시작이 끝보다 작은 필름의 경우 make-film을 고치는 것 만으로 해결된 것.
----- (load "utility.scm")
; My film system! (define (make-film s e t) (if (> s e) (list e s t) (list s e t))) (define (start-point film) (list 's (car film) (transparency film))) (define (end-point film) (list 'e (cadr film) (transparency film))) (define (start-point? point) (eq? 's (car point))) (define (end-point? point) (eq? 'e (car point))) (define (point-pos point) (cadr point)) (define (point-transparency point) (caddr point)) (define (transparency film) (caddr film))
C/C++ 에서는 대입문 - 이 사실 문(statement)이 아니라 식(expression)이다. 따라서 다음 코드가 동작한다.
#include <stdio.h>
int main() { int x = 10; printf("%d\n", x = 20);
return 0; }
실행하면 20 이란는 숫자가 콘솔에 찍힌다. 파이썬에서는 대입문이 맞다. 따라서
print x=10
이런 파이썬 코드는 유효하지 않다. 별 생각 없이 쓰고 있었는데, 파이썬에 대해 아무것도 모르고 있었구나 -_-; ... 뿐 아니라 프로그래밍 언어 자체에 대해 기초가 부족했다. 아 뼈아퍼라...
사실, 문과 식을 구분해서 쓰는 사람이 그다지 많지는 않다(고 느낀다). 나도 가끔 필요할 때만 확인해보곤 하는데 ... 자, 정확히 statement, expression 이 무엇인지 설명한다면? 이라고 하니까 바로 답이 튀어나오지 않는다. 그래서 위키에서 긁어왔다.
In computer programming a statement can be thought of as the smallest standalone element of an imperativeprogramming language. A program is formed by a sequence of one or more statements. A statement will have internal components (eg, expressions). - From wikipedia
An expression in a programming language is a combination of values, variables, operators, and functions that are interpreted (evaluated) according to the particular rules of precedence and of association for a particular programming language, which computes and then produces (returns, in a stateful environment) another value. The expression is said to evaluate to that value. As in mathematics, the expression is (or can be said to have) its evaluated value; the expression is a representation of that value. - From wikipedia
그냥 개인적인 생각이지만, 해보면 쉽다 쉽다 - 하는 함수형 언어를 사람들이 처음 접할때 많이 좌절하곤 하는데 그 이유가 이게 아닐까 한다. Imperative language에 익숙한 상황에서는 statement 의 나열로 문제를 해결하려고 하기 쉬운데, Functional language에서는 그게 쉽지 않다 - 기 보다는 제한적이거나 불가한 경우도 있다. 함수형 언어를 써보면 새로운 접근 방식을 익히게 된다고 하는데, 그 새로운 접근이 뭐냐? 라고 물으면 좀 난감했는데, 일단 이렇게 정리해볼 수 있으려나?
Expression oriented problem solving
Expression은 side-effect가 있을 수도 있고, 없을 수도 있다 - 고 한다. 이에 반해 Statement는 Side-effect를 만들어내기 위한 것이라고도 하는데 (statements do not return results and are executed solely for their side effects, - From wikipedia), side-effect를 지향/지양 하는 정도에 따라서도 접근이 달라질 수 있는 것 같다.
역시 기본부터 차근차근 다져야 하는데, 이런것도 확실히 정리 안해두고 코드를 만지고 있었다니 부끄럽다. :'( 일단 낙서 종료.
side-effect, referential transparency 등에 대해서도 살짝 정리해둬야겠다.
아 그런데 emacs 이녀석이 나를 계속 괴롭히는거다. case 를 앞으로 붙여버리는데, set-label-offset 을 주면 된다길래 해봤는데 left-brace({)가 아래로 내려오면 되는데, switch 뒤에 붙어있으면 안됨. ... 한참을 구글링을 해서 겨우 찾았다.
(c-set-offset 'case-label '+)
으로 해결가능. cc-mode 도 필요하다고 하지만, 이건 emacs 20 이상에서는 기본이라고 하니 그냥 무시. 위 한 줄을 ~/.emacs.d/init.el 같은 곳에 넣어주면 된다. ;-)
여기서부터는 cursor 를 가지고 놀면 된다. cursor.execute("show tables"), cursor.execute("create table test (id int)") 뭐 이런 식으로 ... 그래서 테스트를 위해서 엔트리 1억개를 insert 하는 코드를 만들었다. 대충 이런 모양이다.
... 29 # Generate a bunch of (from, to) pairs and insert into the table 30 values = [] 31 for x in xrange(0, VN): 32 from = x 33 digest.update(str(x)) 34 to = hash(digest.hexdigest()) % CN 35 values.append( (from, to) ) 36 if from % CHUNK_SIZE == CHUNK_SIZE-1: 37 query = "INSERT INTO info_test (from, to) VALUES " + ", ".join([str(x) for x in values]) 38 cursor.execute(query) 39 values = [ ] ...
그런데... 몇분쯤은 걸리겠지 하는 생각에 엔터를 눌렀는데, 실행이 순식간에 끝난다. 엉? 엔트리 1억개를 넣는건데 1초도 안걸리다니, 요즘 DB가 졸 짱쎄졌구나! 라는 생각에 터미널 창에서 select * from info_test limit 1000; 을 때려 보았다.
mysql> select * from info_test limit 1000; Empty set (0.00 sec)
mysql> _
엉? 뭐지? -_-; 그리고 삽질 시작. (cursor.close(), db.close() 도 밑에서 잘 해 주었다)
한시간쯤 뇌가 날아갔다가, 군것질을 하고 돌아오셨다. 그동안 몇몇 테스트를 해보면서 내린 결론은,
insert만 안된다
는 것. 왜 그럴까... insert는 쓰기 작업이고, 자주 일어나고, 연속적으로 일어나는 경우가 많으니까 아마도 어댑터가 나름 캐싱했을것 같은데 ... -> 빙고!
썅썅봐 -_-; db.close() 앞에 db.commit() 을 넣어주니까 제대로 들어간다. API에 보면,
commit(...)
Commits the current transaction
라고 달랑 한줄 나와있고, 나는 트랜잭션을 쓴 적이 없다. -_-; 아니 저게 없으면 방법당한다고 어디 한마디라도 써놓던가... ㅠㅠ 그리고 예의상(매너?) cursor.close() 나 db.close() 에서 들고 있는 캐시는 다 처리해 주어야 하는것 아님? -_-+
예전에 하둡님도 close 할때 flush 안해주셔서 초큼 난감했었는데, 이거 요즘 유행인가보다.
얼마전, SenA를 방에 들고와서 데비안을 새로 깔았다. 근데 왠걸, repository에 testing, unstable을 추가하니까 apt-get update 중에 Dynamic MMap out of room 이라는 메시지가 튀어나오며 비정상 종료가 되는거다. ㅠㅠ 그래서 그냥 stable 만 쓰지 뭐 ~ 라면서 랄랄 거리고 있었는데, 오늘 우연히 발견.
apt-get update 을 하면 패키지 리스트를 가져오는데 이때 사용하는 캐시 크기에 제한이 걸려서 "너무 커욤" 이러면서 뻑났던것. 이전에는 diff 만 가져오면 되니까 상관 없었는데, 새로 설치하고 나서, 추가하니 처음에 가져올게 꽤 큰가보다.
데비안 머신에서 /etc/apt/apt.conf 를 고치거나, 이 파일이 없다면 만들어주면 된다.
APT::Cache-Limit 536870912;
이 한 줄을 넣어주면 된다. Cache limit은 바이트 단위이니 적당히 ... =_= 난 512MB 로 했다. 잘 되네 ;-)
이전에 친구 블로그 덤프파일에서 스팸댓글(6천개쯤?)을 날려주는 스크립트를 만든적이 있다. 도저히 손으로 지울수는 없어서 블로그 덤프 내리고, 파싱해서 댓글 추출하고... 날릴 IP테이블 만들어서 뿅! 하는 거였는데... 그걸 이제 내 블로그 덤프에 쓰게 생겼구나.
라지만, 대충 보니 IP가 다양화? 된 것 같다. 이게 스팸인지 아닌지를 알아야 하는데, 그렇게 쉽진 않겠지. 몇달 전 스팸폭탄에서 뽑은 IP는 대충 600개 정도였다. 걔네들이랑 겹치는게 별로 없으면 낭패;; 오늘 저녁에 일단 내 블로그에 있는 스팸들 좀 가져다가 정리해보자... ㄷㄷㄷ
Ex 2.6 In case representing pairs as procedures wasn't mind-boggling enough, consider that, in a language that can manipulate procedures, we can get by without numbers (at least insofar as nonnegative integers are concerned) by implementing 0 and the operation of adding 1 as
Tis representation is knows as Church numerals, after its inventor, Alonzo Church, the logician who invented the λ calculus.
Define one and two (not in terms of zero and add-1) Give a direct definition of the addition procedure +.
=> 저게 왜 'zero'고 저게 대체 왜 '더하기 1'이냐 -_-; 라고 쭝얼거리길 몇분... ㄷㄷㄷ 한참을 뚫어져라 쳐다보니 알듯. 결국, nonnegative integer n은 입력 f를 받아서 fn(x) 라는 함수를 내보내는(입력과 출력이 함수인 함수, meta-function, procedure as data ... 뭐라 표현하건 간에 -_-;) 함수이다. 자, 그렇다면 시킨대로 one, two 와 이녀석들을 더할 수 있는 프로시져를 만들어 보자.
(define one (lambda (f) (lambda (x) (f x))))
(define two (lambda (f) (lambda (x) (f (f x)))))
(define (add a b) (lambda (f) (lambda (x) ((a f) ((b f) x)))))
음, 그리고 이게 제대로 동작하는지 확인하기 위해서 이렇게 만들어진 녀석들을 우리가 볼 수 있는 숫자 형태로 바꿔주는 프로시져도 추가로 만들자. n이 fn(x)으로 표현된다고 하니 f: x->x+1, 시작은 0으로 정의하면 해당 숫자를 얻어낼 수 있다.
(define (tonum n) ((n (lambda (x) (+ x 1))) 0))
음 add 프로시져가 여러개의 입력도 받을 수 있으면 좋겠다. car, cdr을 잘 써서 넣으면 될 듯 한데 이건 일단 오늘은 패스.