2019년 12월 5일 목요일

[java] CountDownLatch와 CyclicBarrier의 차이 (Phaser로 들어가기 전)

출처 : https://www.baeldung.com/java-cyclicbarrier-countdownlatch

개요

사실 오래 전에 배워서 사용했던 Phaser라는 기능이 가물가물해서 기억을 하고자 CountDownLatch와 CyclicBarrier를 비교하면서 복습을 해보았다.
하지만 대부분 Phaser보다는 CountDownLath,CyclicBarrier를 많이 사용하지 않을까 싶다.


CountDownLatch

일단 Latch(빗장이었나?)로 걸어잠그고 숫자를 다 세면 열어주는 것이다. 그래서 만약에 CountDownLatch에 카운트를 5로 지정하면 0이 될때까지 현재 스레드가 기다린다.

CyclicBarrier

CyclicBarrier는 스레드 그룹이 모두 wait하고 있는 지점에 도달 할 때까지 기다린다.
이 시점에서 Barrier가 깨지면서 일을 할 수 있게 된다. 뭐 일을 안해도 되고 해도 된다.

차이점

Task vs Treads

보면 알겠지만 CountDownLatch는 숫자를 세는데 중점을 두고, CyclicBarrier는 모두 다 오길 기다린다.
CountDownLatch는 동일한 녀석이 숫자를 두번 세도 상관이 없다.
CyclicBarrier는 한명당 하나의 카운트라고 생각하면 된다. 무조건 다와야 한다.

CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t = new Thread(() -> {
  countDownLatch.countDown();
  countDownLatch.countDown();
});

t.start();
countDownLatch.await();

System.out.println("code1");
System.out.println(0 == countDownLatch.getCount());
System.out.println("===");

CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
Thread t = new Thread(() -> {
  try {
    cyclicBarrier.await();
    cyclicBarrier.await();
  catch (InterruptedException | BrokenBarrierException e) {
    e.printStackTrace();
  }
});

t.start();

System.out.println("code2");
System.out.println(1 ==  cyclicBarrier.getNumberWaiting());
System.out.println(cyclicBarrier.isBroken());
System.out.println("===");


재사용성

CyclicBarrier는 wait()에 도달해서 다음으로 넘어가면 count값이 원래 값으로 바뀐다. 무한이 돈다
CountDownLatch에서는 한번 도달하면 끝이다.

List outputScraper = Collections.synchronizedList(new ArrayList<>());
CountDownLatch countDownLatch = new CountDownLatch(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
  es.execute(() -> {
    long prevValue = countDownLatch.getCount();
    countDownLatch.countDown();
    if (countDownLatch.getCount() != prevValue) {
      outputScraper.add("Count Updated");
    }
  });
}

es.shutdown();
System.out.println(outputScraper);
System.out.println(outputScraper.size() <= 7);
// https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/package-summary.html
// https://stackoverflow.com/questions/6916385/is-there-a-concurrent-list-in-javas-jdk 참고
// Collections.synchronizedList는 내가 임의로 넣은 것이다.
List outputScraper = Collections.synchronizedList(new ArrayList<>());
CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
  es.execute(() -> {
    int numberWaiting = cyclicBarrier.getNumberWaiting();
    if (numberWaiting >= 0) {
      outputScraper.add("Count Updated"); 
    }
    try {
      cyclicBarrier.await();
    } catch (InterruptedException | BrokenBarrierException e) {
      e.printStackTrace();
    }
  });   
}

es.shutdown();
System.out.println(outputScraper);
System.out.println(outputScraper.size() > 7);
cyclicBarrier.await();
다음에는 Phaser에 대해 알아보자.

2019년 11월 22일 금요일

[javascript] jQuery는 어떻게 new가 없이 생성되는 것일까?

언젠가 개발을 처음 시작할 때 나프다의 [정개발]이라는 분과 슬랙 대화를 한 적이 있다. 당시 나는 자바만을 배우고 있었음에도, 그분은 언젠가 jQuery소스를 공부해야 한다고 했었다.
그때는 자바 개발하기에도 바쁜데 자바스크립트에 제이쿼리를 사용하는 것도 모자라 소스를 공부?
라고 생각했었다.

지금은 자바스크립트를 배우는 것이 자바에도 도움되고 더 나아가 내가 어떤 언어를 짜던 잘나가는 오픈소스를 읽는 것이 중요하다고 생각한다. (하지만 제대로 소스를 판 적은...)
오늘은 제이쿼리에게서 한 수 배워보고자 한다.

오늘 주제는 jQuery는 어떻게 new를 없앴는가? 이다.
대부분 new를 없앤다는 것은 생성자를 사용하지 않는 다는 말과 같다. 매개변수가 필요하면 그 매개변수를 클로저로 덮어서 함수로 던지는 것이다. 그러면 그 함수의 스코프는 자기 함수 밖에 있는 변수(매개변수 포함)를 덮은(closure) 채로 세상 밖에 내던져진다.

하지만 jQuery는 그런 것 같지 않다. 어떻게 그렇지 않은지 아주 쉬운 방법으로 알 수 있다.


이처럼 __proto__안에 수 많은 함수들이 있는 것을 알 수 있다. 어쩌면 당연한 것이다. 수 많은 기능들을 사용할 때마다 각 Execution Context에 포함해서 던진다는 말은 사용할 때마다 동일한 기능들이 메모리에 올라간다는 말이니까. 한 두개면 괜찮겠지만 저게 몇 개인가!

new를 사용하는 것이 분명하다.

그렇다면 jQuery소스를 한번 구경해보자.

jQuery($)의 정체

제이쿼리 사이트에서 일반 개발용소스를 참고했다.(https://code.jquery.com/jquery-3.4.1.js)

일단 jQuery를 실행하면 무엇이 나오는가? 그것이 시작일 것이다. 일단 jQuery는 함수일 것이다. 그러니까 new가 없이 실행되는 것이다. jQuery라는 이름의 함수를 찾아보자.

var version = "3.4.1",
  // Define a local copy of jQuery
  jQuery = function( selector, context ) {
  // The jQuery object is actually just the init constructor 'enhanced'
  // Need init if jQuery is called (just allow error to be thrown if not included)
      return new jQuery.fn.init( selector, context );
  },
...


그렇다. jQuery를 실행하면 내부적으로 jQuery.fn.init이라는 생성자를 실행한다!

jQuery.fn.init의 정체

이제부터는 jQuery의 소스를 이용하면, 말이 많아지기 때문에, 제이쿼리의 간단한 구조를 간단히 설명하고자 한다. 조그만한 자바스크립트 라이브러리를 만들었다고 해보자.

/*
제이쿼리 구조를 설명하기 위해 만든 간단한 함수
*/
(function (global) {
  var mini = function(name) {
    return new mini.fn.init(name);
  }

  mini.fn = mini.prototype = {};

  mini.fn.sayHi = function() {
    console.log("hi " + this.name);
    return this;
  }

  mini.fn.sayBye = function() {
      console.log("bye " + this.name);
      return this;
  }

  mini.fn.init = function(name) {
    this.name = name;
  }

  mini.fn.init.prototype = mini.fn;
  global.mini = mini;
})(window);

var M = mini("Nam");
M.sayHi().sayBye();

녀석의 이름은 mini다. 즉시실행함수를 만들어서 스코프가 밖으로 나가지 못하도록 하였으며 글로벌에 mini라는 변수를 넣어야 하기 때문에 window를 매개변수로 넣었다.

jQuery에서는 이 global이라는 녀석을 받아서 jQuery에서 사용할 수 있는 녀석인지 유효성 체크도 한다.
그리고 mini를 실행하면 mini.fn.init 이라는 객체를 뱉을 것이다.
한번보자.

실행해보자.
var M = mini("nam");
M.sayHi().sayBye();

잘 보면 두 함수를 연속적으로 실행할 수 있는데, 이건 다른 라이브러리에서도 많은 보았을 것이다. 이렇게 함수를 연결하는 방법은 우리가 사용하는 객체를 계속 리턴하는 것이다.
그런데 왜 prototype이 아니라 fn이라는 녀석에 넣은 것일까?

아마 계속 쓰일 녀석이라서 fn으로 간편화 한게 아닌가 추측을 하지만 정확한 이유는 모른다.
이렇게 new가 없이 객체를 생성하는 방법을 알아보았다.

2019년 11월 20일 수요일

[java] 상속 대신 구성을 사용하자.

왜 이 글을 쓰는 걸까

나는 자바 개발자로 일을 하면서 주위 사람들과 계속 이야기 하였다. 하지만 다들 수긍을 하는 듯 하면서 절대 바뀌지 않는 것이 있었다. 바로 **상속 대신 구성**이라는 말이다. 꽤나 오랜시간 구성을 사용하자고 설득하였지만 아무도 변화시키지 못했다. 나의 능력이 부족해서 속상하면서도 한 편으로는 내 말을 듣지 않는 것 같은 동료들이 마냥 아쉬웠다.

그런 의미로 나의 주장을 정리하고 가다듬어야 겠다는 생각을 했다.

내가 처음 자바를 공부할 때부터 들어온 말이 있다. 재사용을 위해 상속을 사용하라. 코드의 재사용을 위해서는 상속을 사용해야 한다. 두 개의 코드가 중복되면 상속을 해라. 그런데 뭔가 이상한 것을 느꼈다. 한 명이 말 한 것이 아니며 나에게 자바를 처음 알려준 강사부터 일터에 나가서 까지 그들은 마치 개발의 시작과 끝은 extends라는 키워드만 알면 다 할 수 있는 것으로 설명했다.

마치 상속이라는 단어가 마법의 단어이며, 이것만 있으면 다른 것을 알 필요도 없고 객체지향이건 유지보수 가능한 구조이며 모든 것이 가능하다고 설파했다.
그런데 막상 시간이 지나면 "이건 뭔가 잘못된 구조야", "아! 이건 상속을 좀 잘못했군. 제대로 했어야지", "앗! 이건 못 고친다. 이거 고치면 다 바꿔야해!" 상속은 점점 사람들을 옥죄었고 무엇이 잘못되었는지 몰랐다.

다들 너무 착하다. 사람들은 마치 상속으로 재사용 도모하라는 책들의 말을 믿고 구조가 이상하게 되면 마치 자기 탓인 것 마냥 아파한다. 자기 머리를 쥐어뜯는다. 하지만 끝까지 상속의 잘못은 아니라고 한다.

하지만 이건 그들의 잘못이 아니다 상속이라는 상아탑을 맹신하게하는 분위기가 그들의 도전을 막는다. 상속은 왠지 클래스 사이에 절대 바뀌지 않을 거라는 약속을 하며 새끼손가락을 거는 것 같다. 장기처럼 한 번 두면 무를 수 없는 약속이었다. 나는 이런 약속을 하고 싶지는 않았다. 무조건 깨질 것인데
후에 이 상속 구조를 자유롭게 수정할 수 있을까? 정말 이 구조가 맞는 걸까? 나중에 이 구조가 틀려서 바꿔야 한다면 바꿀 수는 있는 것일까?

없을 것 같았다. 시간이 지나면 지날수록 상속으로 끈끈하게 만들어진 유대관계는 깨기가 힘들어 보였다. 상속계층의 위로 올라갈 수록 그 약속이 많아지기 때문에 수정되면 안된다. 가령 자바에서 가장 위에 있는 객체는 `Object`이다. 이 녀석은 왠만해서는 절대 바뀌지 않는다. 수많은 자식들이 자신을 쳐다보고 있다. 그 자식들(서브클래스)은 당신(Object)이 변하지 않는다는 것만 믿고 재사용을 하기로 한 것이다.

상속은 마치 서브 클래스가 부모 클래스를 위에다가 그대로 복사해서 쓰는 것과 비슷하다. 만약 부모가 바뀐다고 해보자. 서브클래스도 당연히 바뀐 것이다.


2003년도부터 혹은 그 이전부터 상속은 문제였다.

https://www.javaworld.com/article/2073649/why-extends-is-evil.html
someone : If you could do Java over again, what would you change?
Gosling : I'd leave out classes
고슬링은 클래스를 없애고 싶다고 했다. 덧붙이자면 클래스 자체에는 문제가 없으나 extends관계가 문제이며 `implements`관계를 선호하라고 한다.


어떤 개발자는 자신의 99%의 코드에 상속을 사용하지 않았다고 한다

https://codeburst.io/inheritance-is-evil-stop-using-it-6c4f1caf5117
위 블로그에 있는 내용을 인용하겠다.
Using inheritance is not the only way to extend a class behavior.
But definitely is the most dangerous and harmful one.

I do not use inheritance in the 99% of my code.
And I really mean 99%.

- use interfaces to define the contracts between your classes.
- use final classes to implement behavior for those interfaces.
- use composition (through constructor dependency injection) to put things
together and prevent complexity


[이펙티브 자바]에서는 상속 대신 구성을 사용하라고 권고한다.

자세한 건 책을 참고


[오브젝트]에서는 상속을 절대 재사용을 위해 사용하지 말라고 한다.

계층구조를 표현하기 위해 사용해야 한다고 말이다. 즉 타입을 상속하기 위해서 쓰라는 말이다.


C++창시자분도 유튜브로 상속이 없이 어떻게 개발해야 하는지 발표하는 시대가 왔다.

세상은 정말 많이 변했다. 비야넷 아저씨도 변한 세상에 따라 자신의 주장을 바꿨다.

내가 정리한 문제점

내가 생각하는 상속의 문제점은 아래와 같다.
1. equals 구현에 문제가 생긴다(추이성)
2. 상속을 할 때 부모클래스 중에 구현된 기능이 있다면, 그것을 제대로 알고 구현을 해야 한다. 이 말은 하나의 소스코드가 둘로 쪼개져 있는 것과 같다.
3. 부모클래스가 변경되면 자식클래스 또한 전부 점검해야 한다.
4. 자식클래스가 바뀔 때, 부모클래스를 안 볼 수가 없다.
5. 버그를 찾기가 힘들다. 누구의 책임인지 확인하기가 쉽지 않다. 특히 `super`를 함께 쓰게되면 마치 저 클래스 멀리 있는 코드가 내 코드처럼 행동하면서 핑퐁을 하기 시작한다. 위로갔다 내려왔다 롤러코스터를 타게 된다.

그러므로 나는 상속보다는 `Composition` 소위 구성이라는 기능이 기본이 되어야 한다고 생각한다. `Composition`은 상속과 다르게 내부 구현을 알 수 없다. 필요한 부분에만 책임을 위임한다. 내부 소스로직이 수정되었다고 다른 소스에 문제를 일으키지 않는다. 위임한 책임만 완수하면 어떻게 바뀌던 상관이 없다. 하지만 상속은 문제를 끼칠 수 있다. 구성은 그래서 수정에 자유롭다. 아니 아예 전부 뜯어 고쳐서 새로 넣어도 된다. 그렇게 하라고 의존성 주입이 있는 거니까.

방금 구성을 사용해야 하는 다른 이유가 나왔다. 구성은 런타임에 의존성이 바뀔 수 있다. 즉, 프로그래밍이 실행 중에도 내 할일을 다시 집어넣을 수 있다는 말이다. 원래라면 하늘 아래 부모클래스는 하나밖에 가질 수 없고, 그것은 컴파일 이후에는 운명처럼 거스를 수가 없는데 말이다.


결론

글을 쓰면서 내가 틀렸던 건 아닐까 하는 생각으로 정리를 해보았으나.
틀리지 않은 것 같다. 오히려 자신감이 생긴 것 같다.

상속을 쓰기 전에 구성을 할 수 있는지 확인하자. 어떻게 해서든지 구성을 사용하려고 해보자. 코드에 더 믿음이 갈 것이다. 클래스에 final을 붙여서 상속을 막아 보자. 나 자신뿐만 아니라 사람들이 그 코드를 믿게 될 것이다. 상속보다 구성으로 짠 코드를 볼 때 더 별거 아닌 코드라고 생각하는 사람들이 많다. 그것이 목적이다. 더 별거 아닌 것처럼 보이게 더 쉽게 읽을 수 있게 말이다.

2019년 10월 27일 일요일

[리뷰][맨먼스미신]을 읽고

이 책은 저자가 한 프로젝트를 진행하면서 겪었던 내용을 에세이로 적은 책이다.
이 책은 소프트웨어 개발자의 이야기라기보다 인간들의 이야기이다.
우리 인간들이 일을 함께 하면서 일어나는 일이다. 용어들만 바꾼다면 어떤 산업에서도 적용될 이야기이다.

이 책에서 그 유명한 대사가 나오는데 바로
여자 9명이 있다고 아기가 1개월만에 나오는 것이 아니다.

이 대사가 우리가 일을 할 때, 무엇을 생각하고 계획해야 하는지 알려주는 것이라 생각한다.

내용

이 책은 프로젝트를 어떻게 관리해야 하는지 설명한다. 그는 자신이 믿지는 않지만 개발자의 경력과 생산성에는 큰 차이가 없는 연구결과를 지속적으로 알려준다.(믿지않으면서 왜 지속적으로 보여주는 거지)
아마 우리가 생각하는 통념을 조금이나마 깨뜨리려고 하는 것이 아닌가.
나에게 가장 기억에 남는 것은 일반적으로 개발을 해서 앱을 만드는 것과 그 앱을 상용화하기 위한 제품으로 만드는 것에는 아주 큰 차이가 있다는 것이다.

생산성 측면에서 보면 적은 인원의 외과의(스타개발자)가 최고의 성능을 가져오면 버그율도 적다. 하지만 큰 프로젝트를 진행하려면 시간이 아주 오래 걸릴 것이다.
그래서 사람이 더 필요하게 되는데 사람이 더 많아질 수록 의사소통, 교육 등에 병목이나, 마찰율(책에는 없는 단어다 ㅋㅋ)같은 것이 발생하여 진행이 많이 떨어지게 된다는 점을 말한다.
하지만 상용제품은 그밖에도 다른 것들에 문제가 있는데, 바로 요구사항 적립의 체계화, 테스트, 문서화, 재무관리 등으로 실제 개발시간에 수배의 노력(책에서는 9배라고 나온 것으로 기억한다.)이 필요하다는 것이다.

외과 수술 팀이라는 형식으로 팀을 꾸릴 것을 제안한다. 외과의과 나머지라고 부를 수도 있겠다. 바로 집도하는 외과의를 필두로 그를 보조하는 사람들을 분야별로 나눠서 구성하는 것이다.
재무, 프로그래밍언어, 새로운 툴 등을 분야별로 나눠서 처리하여 외과의를 보조한다. 신선한 방식이다. 아직 개발을 하면서 이런 형태를 본 적이 없어서 어떨지는 잘 모르겠지만, 좋을 것 같긴한다.

그리고 뒷부분으로 갈수록 문서화에 대한 내용에 노력을 기울인다. 주목할만한 점 중에 하나는, 개발을 하는데 서로의 이슈나, 요구사항 변경내용을 모두 적어서 모든 담당자들이 보도록 해서 서로 무슨일을 하는지 공유하는 것이 중요하다고 했지만,
종단에는 시간이 지나서 자신이 틀렸다고 말한다. 서로가 무슨 일을 하는지 모르고 자신의 업무에 집중하며 연결되어야 하는 부분에만 집중하는 것이 더 효율적이라고 주장이 대두되었고, 그는 그에 동조한다.

또한 논란이 되었던 은 탄환은 없다는 소프으웨어 개발에 지름길이란 없다는 것을 서술한다.
소프트웨어가 복잡한 것은 그 내재된 성질이기에, 도구로 고칠 수 있는 것이 아니라는 것이다. 객체지향 언어나 다른 방법론들이 쏟아져 나오지만, 결국 복잡성은 밖에 문제가 아닌, 소프트웨어 개발 자체가 가진 성질이며, 그것들을 없애주는 은탄환은 없다고 말한다.

이 책은 개발보다는 소통에 관한 내용이 많다. 그래서 누구라도 읽어볼만 하다고 할 수 있다.

[리뷰][클린 아키텍처]를 읽고

클린 아키텍처는 밥 아저씨의 클린 시리즈에서 가장 최신작인 것 같다.
이 책은 이전에 읽었던 [클린코드]와는 달리 아주 멀리서 소프트웨어산업을 바라보고 자신의 생각을 담아내고 있다.

내용

초반에는 자신이 생각하는 소프트웨어에 대해 설명하며 무엇이 소프트웨어를 더욱 소프트하게 만드는지 알려준다.
소프트함이라는 것은 변하기 쉬워야 한다는 것이다. 변하기 어렵게 만들거였으면, 하드웨어로 만들었을 것이라는 것이 그의 주장이다.
극단적인 예로, 변하기 쉽지만 요구사항에 맞지 않는 프로그램과 요구사항에는 맞지만 변하기 어려운 프로그램을 주어지면 그는 주저없이 첫번째를 선택하겠다고 말한다.
변하기 쉬운 것은 요구사항에 맞출 수 있지만, 후자는 비즈니스 로직이 변하거나, 버그를 발견했을 때, 바꾸기 어려우므로 안된다는 것이다.
물론 전대 변할 수 없는 프로그램은 없다. 하지만 프로그램을 변화시켜서 기대되는 비용과 개발을 하는 비용의 수지타산이 맞지 않을 때, 변할 수 없는 프로그램이라 부를 수 있다면 그런 프로그램은 존재할 것이다.

중반부터는 어떻게 하면 안전한 코드를 만드는지 설명한다. 하지만 코딩자체를 말하지는 않는다. 오히려 컴포넌트나 서비스같은 단위로 설명한다. 하지만 이 모든 내용은 작게는 코드에 대한 내용과 별 다를 것이 없어보인다.
그는 객체지향에 대해 설명을 하지만, 객체지향에 어떤 것도 이전에 C로 코딩을 하던 개발자들을 더 나은 개발자로 만들지는 못했다는 것을 설명한다. 왜냐하면 당시 개발자들 모두 그렇게 개발을 했었기 때문이다.
상속, 캡슐화, 다형성 모두 C언어로 가능한 구현들이었다. 대부분 문법적으로 간단하게 해준 것이며, 그 마저도 어떤 것은 오히려 구현이 불안정하게 만들었다. 저자가 객체지향으로 넘어가면서 얻은 장점은 다형성이라고 주장한다.

그리고는 지속적으로 그래프를 보여주면서 컴포넌트의 의존성의 방향이 얼마나 중요한지 설명한다. 나는 처음 이런 것을 보면서, 저자의 깊이를 더 알게 되었고, 정말 나중에 한 번 더 읽어야 겠다는 생각을 했다.
더해서 DI라는 것이 어떻게 아키텍처에서 쓰이는지 알게 되었다. 아무때나 DI를 다 붙잉는 것이 아니라. 이 방향을 바꾸기 위해서 하는 것이라는 것을 알게 되었다.
더해서 컴포넌트를 분리할 때도 DI를 사용해서 A,B컴포넌트에 서로 로직들이 잘 분리되도록 하는 것이다.

확실히 저자는 다형성에 큰 의의를 두고 있었다.

또 기억나는 것은 바로 구현에 대한 결정은 나중으로 미루라이라는 것이다. 그는 지속적으로 플러그인 패턴을 강조했다. 예를들어, 출력되는 곳이 콘솔일지, 프린터일지, HTML일지 이런것은 나중에 결정하라는 것이다.
환경은 또 어떨까? 웹으로 할 것인지 아닌지도 나중에 결정할 수 있도록 해야한다. 영속성은? DB로 할지도 나중에 결정해야 한다. 단순히 오라클/MySQL/... 중에 뭘 고를지 정하는 것이 아니다.
처음에는 map자료구조에 메모리로 저장하여 개발함으로써 영속성의 결정을 미룬다. 그리고 파일로 저장해서 또 미룬다. 미루고 미룬 후에 결정하는 것이다. 저자는 때때로 DB를 사용하지 않고 파일로 저장하고 가져와서 로딩하는 방식을 사용했다.
왜냐하면 DB가 필요 없었기 때문이다. 파일로도 충분했다.

이 구현을 미루는 것은 아키텍처에게 유현함(soft)함을 가져오게 해주고, 이런 외부 사항들로과 비즈니스로직을 자연적으로 분리시켜주게 된다. 그는 software가 firmware가 되는 것을 싫어했다. 펌웨어는 한 하드웨어에 종속되어 움직이는 경우가 대부분인데
그의 관점에서는 특정 DB에 종속된 것, 아니면 DB에 종속된 것을 펌웨어라 간주한다. 왜냐하면 세상은 많이 복잡해졌고, 그 정도의 종속도 펌웨어라는 것이다.
HTTP에 종속된 것도 펌웨어이다. 그는 코어 로직이 어딘가에 종속되지 않기를 바란다.

예를 들어보면 아래와 같다.

Core <- DB-AccessObject -> DB
     <- Network-Object
여기서 화살표 방향은 해당 객체가 무엇을 사용하는지 알려준다. 왼쪽에 있는 Core컴포넌트는 아무것도 사용하지 않아야 한다. 그래야 플랫폼이 바뀌어도 사용할 수 있다. DB를 사용하기로 결정했어도 Core는 변경되지 않는다. 그런데 말이 되지 않는다. 만약 Network에서 요청이 오고 DB-AccessObject를 실행해야 하는데 어떻게 Core에서는 사용하지도 않는데 실행을 한다는 거지? 그 답은 아래 DI에 있다(!!) 아래처럼 화살표가 되어 있다고 해보자.
Core -> DB-AccessObject -> DB
     <- Network-Object
이렇게 구현하는 것이 자연스러울 것이다. 여기서 DI를 적용하면?!
Core -> I-Persistent(in Core Component) <- DB-AccessObject -> DB
     <- Network-Object
이렇게 I-Persistent를 이용하여 저장하고 조회하는 것이다. I-Persistent는 코어 컴포넌트 안에 있고, 영속성관리를 무엇으로 해도 상관이 없게 되었다.
이것이 DI의 힘이다. 나는 지금까지 재사용을 쓰기 위해서다. 구현이 바껴도 사용할 수 있어서 DI를 쓴다고 알고 있었다. 물론 맞는 말이긴 해도, 이정도의 깊이가 있는 사람이 동일한 말을 하는 사람과는 전혀 달랐다.
나는 이정도의 넓은 숲을 보면서 말하지는 않았다. 왜냐하면 이런 세상을 전혀 몰랐으니까...

방향을 바꿔서 컴포넌트의 분리를 꾀하고, 종단에는 코어를 (유연하게 하는 것)지키는 것.
일류 아키텍처의 속마음을 잠깐 들여다 볼 수 있는 순간이었다.
마지막으로 가면 갈수록 그는 이 DI를 더 강조하는 것처럼 보인다. 그리고 실제적인 예시들을 보여주면서 어떻게 해야 하는지 알려 준다. 사실 다 읽었지만, 완벽하게 이해했다고 생각되지는 않는다.

나에게 또한 많은 생각을 하게 한 내용은 바로 프레임워크다.
그는 프레임워크에 종속되면 안된다고 말한다. 물론 프레임워크 개발자는 당신이 종속되도록 요청한다. 아예 하나의 언어처럼 붙어살길 원한다. 하지만 당신은 그렇게 하면 안된다는 것이다.
당신은 프레임워크를 툴로 써야 하며, 언제든지 바꿀 준비가 되어있어야 한다. 도구로써 써야 한다. 마법과 같은 일을 할 수 있다고 계약하지 말라.
당신에게 분명 되돌아 온다. 프레임워크에 종속되어 펌웨어가 되지 말라고 한다.

예를들어 자바의 스프링프레임워크를 예를 들면, Autowired를 하지말라고 한다. 차라리 Main에 다가 종속성을 다 쓰라고 말한다. 당신이 사용하는 모든 클래스에 spring이 import되어 있다면
종속되어 있는 것이다. 어쩌면 플러그인 패턴을 사용하여 DI로 프레임워크를 분리해서 사용하는 것이 해답이 될 수도 있을 것 같다.

끝없이 소프트하게 하라, 그것이 소프트웨어 개발자가 해야 할 일인 것이다.

이 책은 분명 다시 읽어야겠다. 누군가가 개발자가 되었다면, 그리고 소프트웨어개발자라면, 언젠가는 읽어야 할 책이라고 소개하고 싶다.

2019년 10월 25일 금요일

[리뷰][let over lambda]를 읽고-1

이 책은 [on lisp]를 보다가 머리를 식힐 겸 구매한 책이다.
다들 [on lisp]를 먼저 보라고 추천을 했지만, 어느정도 [on lisp]로 쉬운부분(매크로 초반까지) 읽고 [let over lambda]로 가는 것이 더 좋지 않을까 라는 생각을 한다.
[on lisp]보다 쉽다고 개인적으로 생각한다. [on lisp]는 아직 끝내지도 못했다... 무서워서... (일단 [on lisp]는 예제 코드가 너무 무섭다. 너무 길고 하나하나 쓰면서 이해하는데 시간이 너무 든다. 하지만 분명 나중에 정복해야 하는 책임은 분명하다.)

이 글은 단순한 감상이며 나중에 좀 더 심도 있는 내용을 적어야 할 것같다.

줄거리

그나저나
왜 이름이 [let over lambda]인가?
이것은 리스프에서 아주 기본적으로 사용되는 패턴으로 let으로 lambda를 덮는 형태를 말한다.
;; common lisp
(let ((a 10))
  (+ a 1))
;= 11
그러므로 [let over lambda]를 클로저라는 이름을 리습세상의 언어를 이용하여 우아하게 표현한 것이다.
이 [let over lambda]가 중요한 이유는 pointer를 조작하는 또 다른 방식이 될 것이기 때문이다.
이 let에서 생성되는 포인터를 이용하여 리스프 세상에서는 더 멋지고 자유롭게 표현할 수 있는 여지를 가지게 된다.
이것은 특히 커먼리습에서 대두된다.

이 책은 단순히 클로저만을 소개하는 책이 아니다. 시작이다. 이 책은 매크로를 설명한다. 이 책은 매크로로 어디까지 할 수 있는지 조목조목 설명해준다.
특히 뒤로 갈 수록 그 난이도는 올라가지만, 더 흥미로움은 더욱 커진다.

가장 충격적인 것은 Name Capturing이다. 이름이 충돌하는 문제인데, 이것은 람다에서도 나오는 베타-리덕션을 하다가 동일한 이름을 쓰는 매개변수가 있는 경우 리덕션에 문제가 생기는 경우와 비슷하다.
람다 계산의 경우 알파-컨버젼을 이용하여 시퀀스를 뒤에 붙이는 경우와 비슷하다.

책에서는 (gensym)으로 컴파일러가 내부적으로 현재 사용하고 있는 이름들을 확인하고 그들과는 겹치지 않는 이름을 만들어서 충돌이 일어나지 않도록 하는 방법을 제시한다.
하여 아래 코드는 절대로 참이 나올 수 없다.
왜냐하면 이 코드를 확인 한 후에 겹치지 않도록 이름을 제시해줄 것이기 때문이다.
(equal (gensym) (gensym))

이 서적은 더 나아가서, Name Capturing을 일부러 만드는 방식을 제시한다.
마치 큰 파도가 밀려오면 키를 돌려 정면으로 돌파해야 하는 것처럼.
Name Capturing을 피하는 것이 아닌, 정면 돌파하여 더 우아한 언어로 만들어주는 것이다.
마치 원래 그 언어가 있었 던 것처럼...
나중에 다뤄야 겠다.

후기

[let over lambda]는 이전에 람다계산 관련 책에 이어서 이번해에 읽은 최고의 책 중에 하나로 선정하겠다.
이번해에도 꽤나 많은 책을 읽었는데
기억에 남는 책은 이 두 권이라니... 나의 선택능력은 꽝인 것 같다.

꼭 읽어야 하는 책이다. 이 책을 읽기 위해서 common lisp, scheme, clojure 같은 언어를 둘러 본다해도
시간이 아깝지 않을 것이다.

2019년 10월 2일 수요일

[리뷰] lambda calculus에 대해 알아보기

https://www.notion.so/tombox/Lambda-Calculus-aa7f35c6a6ec4ecb9288e2bd33493dc8
lambda calculus에 관하여 이 책을 일독하였으나 기억이 가물가물해져서
다시 읽으면서 Notion에 적어보고 있다.

다시 읽어도 새롭다. 엄청난 책임이 틀림없다. 그리고 시간이 나면 Combinator에 대한 책도 읽고자 한다.

2019년 9월 11일 수요일

[clojure programming] 2. Functional Programming - 로깅파일 만들기

출처 : Clojure programming
Building a Primitive Logging System. with Composable Higher-Order Functions

(defn print-logger [writer]
  #(binding [*out* writer]
     (println %)))

(def *out*-logger (print-logger *out*))
(*out*-logger "hello")

버퍼를 만들어 보자.
(def writer (java.io.StringWriter.))
(def retained-logger (print-logger writer))
(retained-logger "hello-1")
(retained-logger "hello-2")
(str writer)

파일에 써보자.
(require 'clojure.java.io)
(defn file-logger [file]
  #(with-open [f (clojure.java.io/writer file :append true)]
      ((print-logger f) %)))

(def log-file (file-logger "message.log"))
(log->file "HELLO FILE")
지금까지는 아주 적은 코드로 서로 다른 곳에 쓸 수 있는 방법을 알아봤다.
만약, 여러곳에 써야 한다면 어떻게 해야 할까. doseq를 쓴다.
(defn multi-logger
  [& logger-fns]
    #(doseq [f logger-fns]
       (f %)))
(def log (multi-logger
           (print-logger *out*)
           (file-logger "message.log")))
(log "hello console and file")

마지막 예제, 진짜 로깅인 것처럼 시간 포맷까지 앞에 붙여보자.
Adding a piece of "logging middleware" to include a timestamp with each log message
(defn timestamped-logger
  [logger]
  #(logger (format "[%1$tY-%1$tm-%1$ts %1$tH:%1$tM%1$tS] %2%s" (java.util.Date.) %)))

(def log-timestamped (timestamped-logger
                      (multi-logger
                        (print-logger *out*)
                        (file-logger "message.log"))))
(log-timestamped "goobye, now")

12. Generalized variable

챕터8에서 말했듯이 매크로의 장점 중 하나인 매개변수의 변화이다.
setf매크로를 보자. 이 챕터는 setf에 숨겨진 것들을 본다. 그리고 예지로 무엇을 할 수 있는지도.

setf를 이용하여 매크로를 만드는 건 어렵다. 섹션1에서 틀린 방식을 보여줄 것이고,
다음섹션에서 무엇이 문제인지, 어떻게 바꾸는지 본다.
마지막 섹션에서는 어떻게 setf의 반전(inversion)을 만드는지 보자.

12.1 The Concept
setf 매크로는 setfq의 일반화 버전이라 생각하자, setf에서는 단순변수가 아니라. 호출식일 수 있다!
(setf lst '(a b c))
(setf x y)는 x가 y로 평가되도록 처리해라(see to it that x evaluate y)로 이해될 수 있는데,
매크로에서, setf는 위 문장이(일반적으로) 참이 되도록 하기 위해서 매개변수의 내부를 볼 수 있다.(보아야한다)

첫번째 매개변수가 심볼이면, setf는 setq로 확장한다.
하지만 queryf라면 setf는 그 쿼리와 연관된 assertion(단정?)으로 확장한다.
두번째 매개변수는 상수가 당연하니까 아래처럼 될 것이다.
(progn (rplaca lst 480) 480)
이렇게 쿼리에서 assertion(rplaca)로 변화하는 것을 inversion이라 한다.
커먼리습에 정의된 대부분 access함수들은 이미 정해진 inversion이 있다.
car, cdr, nth, aref, get, gethash...
setf의 첫번째 매개변수에 들어올 수 있는 표현식을 generalized variable이라 부른다.
이 setf를 이용한 매크로 예를 보자.
(defmacro toggle (obj) ;; 틀림
  `(setf ,obj (not ,obj)))
이 녀석ㅇ느 generalized variable을 toggle하는 매크로다.
(let ((lst '(a b c)))
  (toggle (car lst))
  lst)
(NIL B C)
다음으로 넘어가자. 작은 마을에서 거주자들끼리 관계를 DB에 저장하고자 한다.
사람들의 친구를 기록하는 테이블을 만들어보자.
(defvar *friends* (make-hash-table))
; 이 hash-table 안에 각 사람들의 친구들이 있는데 이것 또한 hash-table이다.
(setf (gethash 'mary *friends*) (make-hash-table))
; John을 Mary의 친구로 만들자.
(setf (gethash 'john (gethash 'mary *friends*)) t)
마을은 이제 두 파벌로 나눴다(친구가 있다 없다). 이 말은 양쪽 일수도 둘다 아닐 수도 없다.
둘 중 하나이다. (왠지 toggle이 유용해보임)
친구가 이다/아니다를 toggle할 것이다.
(setf (gethash x (gethash y *friends*))
      (not (gethash x (gethash y *friends*))))
; 꽤나 복잡한데 setf가 없이 더 편리하게 만들 수 있다.
; 일단 아래처럼 db로 접근하는 매크로를 만든다면
(defmacro friend-of (p g)
  `(gethash ,p (gethash ,g *friends*)))
(toggle (friend-of x y))
; 이렇게하니 꽤나 이쁘다. 그런데 문제가 있다고? 아래 섹션을 보자.

12.2 다중평가문제
toggle을 다시보자
(defmacro toggle (obj) ; wrong
  `(setf ,obj (not ,obj)))
이 문제는 10.1에서 대중평가와 동일하다.
사이드이펙트가 있는 경우 두번실행되면서 문제가 대두됨.
(toggle (nth incf i) lst)
; 여기에서 incf가 여러번 일어나면 그 숫자는 달라질 것!
(setf (nth (incf i) lst)
      (not (nth (incf i) lst)))
; i가 두번 증가한다. 하여!
(let ((lst '(t nil t))
      (i -1))
  (toggle (nth incf i) lst))
  lst)
(T NIL T)
?? 바뀐게 없는데 왜 그러지? 자세히 보자.
(setf (nth 0 lst)
      (not (nth 1 lst)))
두번째 값의 toggle을 첫번째에 덮은 것이다...

우린 이제 표현식 내부에서 뭘 하는지 봐야한다. 하나하나 분리해서 사이드이팩트가 있는지 보고 따로 평가해야한다.
이걸 쉽게하기 위해 커먼리습에서 define-modify-macro를 제공하고 얘가 대신 해준다.
이녀석은 3개의 매개변수를 받는데
1. name of macro
2. its additional parameters (after the generalized variable)
3. name of the function (which yeilds the new value for the generalized variable)
이제 toggle 매크로를 다시 정의하자
(define-modify-macro toggle () not)
; toggle : 이름
; ()  매개변수 (두번째 매개변수 부터라고 생각하면 된다)
; 함수 (generalized variable을 가져와서 새로운 값을 리턴하면 그것으로 재세팅됨)
여기 코드의 뜻은
(toggle place)를 평가하려면, place에서 특정 위치를 찾고, val이라는 값이 있으면 (not val)로 넣어서 그 리턴값을 세팅해라.
다시 평가하면 잘 나올 것.

이 toggle을 좀 더 일반화하자. (여러 매개변수를 넣을 수 있다면??!!)
; fig 12.1 Macros which operate on generalized variables.
(defmacro allf (val &rest args)
  (with-gensyms (gval)
    `(let ((,gval ,val))
       (setf ,@(mapcan #'(lambda (a) (list a gval))
                       args)))))

(defmacro nilf (&rest args) `(allf nil ,@args))

(defmacro tf (&rest args) `(allf t ,@args))

(defmacro toggle (&rest args)
  `(progn
     ,@(mapcar #'(lambda (a) `(toggle2 ,a))
               args)))

(define-modify-macro toggle2 () not)

12.3 New Utilities
이제 generalized variable을 이용한 유틸들을 보자.
Fig 12.1에 nilf가 특이하다
(setf x nil y nil z nil)
; 이녀석은 아래와 같다
(nilf x y z)
그림 12.2 리스트의 마지막을 파괴적으로 수정하는 매크로들이다.
Section 3.1에서 (nconc x y)보다는 (setq (nconc x y))를 쓰라고 한다.
둘 다 같은 일을 하지만 뒤에 내용이 명시적으로 잘 보이기 때문인가 보다.
이제 이 형식을 매크로로 만든 것이 12.2의 concf다.
(define-modify-macro concf (obj) nconc)
; concf 이름
; (obj) 매개변수 (기본 매개변수 place뒤에 obj, (concf place obj)이렇게 쓰는 것
; nconc 가져온 값을 이거로 치환
; obj는 여기서 y로 보임. obj는 generalized variable 뒤에 오는 객체랬으니까
; y역할을 하는 것이 맞다. 왜 이렇게 나눠놓았을까 생각해봤는데
; 아마 generalized variable(값이 바뀌어야 하는 변수)는 따로 다루는게 아닌가 싶고
; 당연히 다루려고 해당 매크로를 쓰는 거니까 상관없다고 생각했을 수도.

;; 그리고 conc1f는 list 뒤에 요소 하나를 추가하는 녀석이다.
(define-modify-macro conc1f (obj)
    (lambda (place obj)
            (nconc place (list obj))))
; 역시 두개를 받는 람다를 만들어서
; nconc를 했다. 그리고 place 뒤에 (list obj)를 붙였다.
; 저절로 place에 저장될 것!

(define-modify-macro concnew (obj &rest args)
    (lambda (place obj &rest args)
            (unless (apply #'member obj place args)
                (nconc place (list obj)))))
; concew는 conc1f와 같지만 멤버가 없을 때만 넣는 것.
; args는 member를 위한 다른 equals 방식이라던가 그런 설정값이다.

[on lisp] 11.5~6 클로저와 매크로의 차이

mvpsetq는 3개의 유틸리티 함수를 사용한다.
(defun mklist (obj)
  (if (listp obj) obj (list obj)))

(defun group (source n)
  (if (zerop n) (error "zero length"))
  (labels ((rec (source acc)
             (let ((rest (nthcdr n source)))
               (if (consp rest)
                   (rec rest (cons (subseq source 0 n) acc))
                   (nreverse (cons source acc))))))
    (if source (rec source nil) nil)))
(defun shuffle (x y)
  (cond ((null x) y)
        ((null y) x)
        (t (list* (car x) (car y) 
                  (shuffle (cdr x) (cdr y))))))
shuffle을 한번 써보자
(shuffle '(a b c) '(1 2 3 4 5))
(A 1 B 2 C 3 4 5) 
mvpsetq로 그림 11.13의 mvdo를 정의할 수 있다.
condlet처럼 이 매크로는 mapcar대신 mappend를 쓰는데 이유는 오리지널 매크로 호출의 변경을 바지하기 위함이다.
mappend-mklist 는 트리를 1레벨로 펼친다
(defun mappend (fn &rest lsts)
  (apply #'append (apply #'mapcar fn lsts)))

(mappend #'mklist '((a b c) d (e (f g) h) ((i)) j))

mvpsetq를 보자.
(defmacro mvpsetq (&rest args)
  (let* ((pairs (group args 2))
         (syms  (mapcar #'(lambda (p)
                            (mapcar #'(lambda (x) (gensym))
                                    (mklist (car p))))
                        pairs)))
    (labels ((rec (ps ss)
               (if (null ps)
                   `(setq
                     ,@(mapcan #'(lambda (p s)
                                   (shuffle (mklist (car p)) 
                                            s))
                               pairs syms))
                   (let ((body (rec (cdr ps) (cdr ss))))
                     (let ((var/s (caar ps))
                           (expr (cadar ps)))
                       (if (consp var/s)
                           `(multiple-value-bind ,(car ss) 
                                                 ,expr
                              ,body)
                           `(let ((,@(car ss) ,expr))
                              ,body)))))))
      (rec pairs syms))))
이걸 한번 매크로확장해보자.
(print 
 (macroexpand
  '(mvpsetq a 10 b 20 c 30 d 40)))
(LET ((#:G1 10))
 (LET ((#:G2 20))
  (LET ((#:G3 30))
   (LET ((#:G4 40)) (SETQ A #:G1 B #:G2 C #:G3 D #:G4))))) 
이렇게 LET으로 (gensym)을 하고 한번에 setq를 한다.

그런데 psetq랑 뭐가 다른거지?
(macroexpand
  ' (psetq a 1 b a c 3))
(LET ((#:PSETQ-1 1) (#:PSETQ-2 A) (#:PSETQ-3 3)) ; 밸류들
  (SETQ A #:PSETQ-1) (SETQ B #:PSETQ-2) (SETQ C #:PSETQ-3) NIL) 

(setf a 10)
(psetq a 1 b a c 3)
(print b) ; 10

그런데 어떻게 확장되는 걸까
1. args를 2그룹으로 나눈다.
2. 갯수만큼 (gensym)을 만든다.
(group '(a 10 b 20 c 30 d 40) 2)
((A 10) (B 20) (C 30) (D 40)) 

(mapcar #'(lambda (p)
    (mapcar #'(lambda (x) (gensym))
                (mklist (car p))))
    '((A 10) (B 20) (C 30) (D 40)))
((#:G3210) (#:G3211) (#:G3212) (#:G3213)) 
아래 함수를 이해해보자.
#'(lambda (p s) (shuffle (mklist (car p)) s))
mklist는 리스프로 만드는 함수 리스트면 그대로 뱉고 리스트가 아니면 감싼다.
shuffle은 위에 사용법이 있다 지그재그로 첫번째,두번째 매개변수를 합친다
p는 pairs이며, syms는 심볼인듯.(gensym)값
즉 pairs와 syms를 받아서 p의 첫번째 값을 gensym에 하나씩 넣어준다.
(labels ((rec (ps ss) ;; 내부함수에 label로 이름 지정, 매개변수 지정
  (if (null ps) ; ps가 이제 없다면
    `(setq ; (SETQ A #:G1 B #:G2 C #:G3 D #:G4) 부분을 만듬.
       ,@(mapcan #'(lambda (p s) (shuffle (mklist (car p)) s))
                 pairs syms))
     (let ((body (rec (cdr ps) (cdr ss))))
       (let ((var/s (caar ps))
             (expr (cadar ps)))
       (if (consp var/s)
         `(multiple-value-bind ,(car ss) 
                               ,expr
                               ,body)
         `(let ((,@(car ss) ,expr))
               ,body)))))))
 (rec pairs syms))
이제 다음으로 넘어가자. mvdo는 mvdo*를 봤으니 넘어가자.

11.6 Need for Macros
이렇게 평가를 잠시 미루도록 할 수 있는 건 매크로만의 전유물은 아니다.
함수를 closure로 감싸는 방법이 있다.
조건절 혹은 반복 평가들 모두 이런 걸로 가능하게 된다.
예를들어 if매크로를 아래처럼 바꿀 수 있다.
(defun fnif (test then &optional else)
  (if test
      (funcall then)
      (if else (funcall else))))
이걸 사용하려면 기존처럼 사용하면 안된다.
(if (rich) (g-sailing) (rob-bank))
여기서
(fninf (rich)
  #'(lambda () (go-sailing))
  #'(lambda () (rob-bank)))
이렇게 쓰면 된다. 만약 우리가 원하는 것이 조건평가만이라면 매크로는 전혀 쓸모 없다.
이것들은 그저 프로그램을 깨끗하게 하는 녀석들일 것이다. 하지만 매크로는 우리가 아규먼트 폼을 빼낼때나, 매개변수를 바인딩 한단거나 하는 일에서 필수이다.
반복에도 동일한 논리가 적용된다. 오로지 반복만을 위해서는 매크로는 필요 없다.
dolist 같은 걸 보자. 이걸 대체하려면 mapc와 lambda를 이용하면 된다.
(dolist (b bananas)
  (peel b)
  (eat b))

(mapc #'(lambda (b)
           (peel b)
           (eat b))
      bananas)
forever도 함수로 적을 수 있다.
(defun forever (fn)
  (do ()
      (nil)
      (funcall fn)))
이렇게 바디도 감싸서 보낼 수 있다.
하지만 반복을 생성할 때는, 그저 반복만을 위해 만들지 않는다.(as forever does)
이것들은 대게 binding와 iteration의 조합을 위해서 쓴다.
함수에서는 이런 개의 유틸리티 함수를 사용한다.
(defun mklist (obj)
  (if (listp obj) obj (list obj)))

(defun group (source n)
  (if (zerop n) (error "zero length"))
  (labels ((rec (source acc)
             (let ((rest (nthcdr n source)))
               (if (consp rest)
                   (rec rest (cons (subseq source 0 n) acc))
                   (nreverse (cons source acc))))))
    (if source (rec source nil) nil)))
(defun shuffle (x y)
  (cond ((null x) y)
        ((null y) x)
        (t (list* (car x) (car y) 
                  (shuffle (cdr x) (cdr y))))))
shuffle을 한번 써보자
(shuffle '(a b c) '(1 2 3 4 5))
(A 1 B 2 C 3 4 5) 
mvpsetq로 그림 11.13의 mvdo를 정의할 수 있다.
condlet처럼 이 매크로는 mapcar대신 mappend를 쓰는데 이유는 오리지널 매크로 호출의 변경을 바지하기 위함이다.
mappend-mklist 는 트리를 1레벨로 펼친다
(defun mappend (fn &rest lsts)
  (apply #'append (apply #'mapcar fn lsts)))

(mappend #'mklist '((a b c) d (e (f g) h) ((i)) j))

mvpsetq를 보자.
(defmacro mvpsetq (&rest args)
  (let* ((pairs (group args 2))
         (syms  (mapcar #'(lambda (p)
                            (mapcar #'(lambda (x) (gensym))
                                    (mklist (car p))))
                        pairs)))
    (labels ((rec (ps ss)
               (if (null ps)
                   `(setq
                     ,@(mapcan #'(lambda (p s)
                                   (shuffle (mklist (car p)) 
                                            s))
                               pairs syms))
                   (let ((body (rec (cdr ps) (cdr ss))))
                     (let ((var/s (caar ps))
                           (expr (cadar ps)))
                       (if (consp var/s)
                           `(multiple-value-bind ,(car ss) 
                                                 ,expr
                              ,body)
                           `(let ((,@(car ss) ,expr))
                              ,body)))))))
      (rec pairs syms))))
이걸 한번 매크로확장해보자.
(print 
 (macroexpand
  '(mvpsetq a 10 b 20 c 30 d 40)))
(LET ((#:G1 10))
 (LET ((#:G2 20))
  (LET ((#:G3 30))
   (LET ((#:G4 40)) (SETQ A #:G1 B #:G2 C #:G3 D #:G4))))) 
이렇게 LET으로 (gensym)을 하고 한번에 setq를 한다.

그런데 psetq랑 뭐가 다른거지?
(macroexpand
  ' (psetq a 1 b a c 3))
(LET ((#:PSETQ-1 1) (#:PSETQ-2 A) (#:PSETQ-3 3)) ; 밸류들
  (SETQ A #:PSETQ-1) (SETQ B #:PSETQ-2) (SETQ C #:PSETQ-3) NIL) 

(setf a 10)
(psetq a 1 b a c 3)
(print b) ; 10

그런데 어떻게 확장되는 걸까
1. args를 2그룹으로 나눈다.
2. 갯수만큼 (gensym)을 만든다.
(group '(a 10 b 20 c 30 d 40) 2)
((A 10) (B 20) (C 30) (D 40)) 

(mapcar #'(lambda (p)
    (mapcar #'(lambda (x) (gensym))
                (mklist (car p))))
    '((A 10) (B 20) (C 30) (D 40)))
((#:G3210) (#:G3211) (#:G3212) (#:G3213)) 
아래 함수를 이해해보자.
#'(lambda (p s) (shuffle (mklist (car p)) s))
mklist는 리스프로 만드는 함수 리스트면 그대로 뱉고 리스트가 아니면 감싼다.
shuffle은 위에 사용법이 있다 지그재그로 첫번째,두번째 매개변수를 합친다
p는 pairs이며, syms는 심볼인듯.(gensym)값
즉 pairs와 syms를 받아서 p의 첫번째 값을 gensym에 하나씩 넣어준다.
(labels ((rec (ps ss) ;; 내부함수에 label로 이름 지정, 매개변수 지정
  (if (null ps) ; ps가 이제 없다면
    `(setq ; (SETQ A #:G1 B #:G2 C #:G3 D #:G4) 부분을 만듬.
       ,@(mapcan #'(lambda (p s) (shuffle (mklist (car p)) s))
                 pairs syms))
     (let ((body (rec (cdr ps) (cdr ss))))
       (let ((var/s (caar ps))
             (expr (cadar ps)))
       (if (consp var/s)
         `(multiple-value-bind ,(car ss) 
                               ,expr
                               ,body)
         `(let ((,@(car ss) ,expr))
               ,body)))))))
 (rec pairs syms))
이제 다음으로 넘어가자. mvdo는 mvdo*를 봤으니 넘어가자.

11.6 Need for Macros
이렇게 평가를 잠시 미루도록 할 수 있는 건 매크로만의 전유물은 아니다.
함수를 closure로 감싸는 방법이 있다.
조건절 혹은 반복 평가들 모두 이런 걸로 가능하게 된다.
예를들어 if매크로를 아래처럼 바꿀 수 있다.
(defun fnif (test then &optional else)
  (if test
      (funcall then)
      (if else (funcall else))))
이걸 사용하려면 기존처럼 사용하면 안된다.
(if (rich) (g-sailing) (rob-bank))
여기서
(fninf (rich)
  #'(lambda () (go-sailing))
  #'(lambda () (rob-bank)))
이렇게 쓰면 된다. 만약 우리가 원하는 것이 조건평가만이라면 매크로는 전혀 쓸모 없다.
이것들은 그저 프로그램을 깨끗하게 하는 녀석들일 것이다. 하지만 매크로는 우리가 아규먼트 폼을 빼낼때나, 매개변수를 바인딩 한단거나 하는 일에서 필수이다.
반복에도 동일한 논리가 적용된다. 오로지 반복만을 위해서는 매크로는 필요 없다.
dolist 같은 걸 보자. 이걸 대체하려면 mapc와 lambda를 이용하면 된다.
(dolist (b bananas)
  (peel b)
  (eat b))

(mapc #'(lambda (b)
           (peel b)
           (eat b))
      bananas)
forever도 함수로 적을 수 있다.
(defun forever (fn)
  (do ()
      (nil)
      (funcall fn)))
이렇게 바디도 감싸서 보낼 수 있다.
하지만 반복을 생성할 때는, 그저 반복만을 위해 만들지 않는다.(as forever does)
이것들은 대게 binding와 iteration의 조합을 위해서 쓴다.
함수에서는 이런 바인딩에서는 제한적이다.
만약 당신이 연속적인 리스트 요소를 하나씩 차례대로 반복하면서 바인딩 하고 싶다면, mapping 함수를 쓸 수 있다.
하지만, 만약 요구사항이 이것보다 조금만 복잡해지면, 매크로를 써야 한

[on lisp] 12.3 new utilites

;; 12.3 New Utilities
; 이번에는 generalized variables에 관련된 새로운 유틸리티들을 볼 예정.
; 인수를 그대로 [setf]에 전달하려면 매크로 여야함.
; Fig 12.1는 setf을 이용한 예제를 보여줌.

(setf x nil y nil z nil)
;; 이럴거면 이제
(nilf x y z)
;; 이렇게 쓸 수 있다.
;; 이 4개의 매크로는 꽤나 중요한 점을 보여준다. 바로 값을 할당하는 연산자를 보여준 것이다.
;; Even if we only intent to use an operator on ordinary variables, it's worth wrting it to expand into a [A] instead of a [B]
; 일반 변수에만 연산자를 사용하려는 경우에도, setq보단 setf로 확장하도록 작성하는 것이 좋다.
; setq는 set qoute의 약자며 setf는 set function의 약자다
; q는 저장될 첫번째 매개변수로 이름만 올 수 있지만, setf는 함수도 올 수 잇다.

; Fig 12.2 에는 리스트의 끝을 파괴적으로 수정하기위한 3개의 매크로가 포함되어 있다.
; nconc란 사이드이펙트를 일으키는 conc (합치기) 첫번째 매개변수에 값이 더해지는 방식 (새로운 객체를 리턴하는게 아니라)

;; Fig 12.2 List operations on generalized variables.
(define-modify-macro concf (obj) nconc)

(define-modify-macro conc1f (obj)
(lambda (place obj)
(nconc place (list obj))))

(define-modify-macro concnew (obj &rest args)
(lambda (place obj &rest args)
(unless (apply #'member obj place args)
(nconc place (list obj)))))

; 우리는 이전에 (nconc x y)가 사이드이팩트를 일으킨다고 이야기 했다. 그리고 아래처럼 작성해야 한다.
(setq x (nconc x y))
;; 이런 형태를 매크로로 만드는 것이다. 그것이 concf다.
; conc1f는 새로운 요소를 리스트 뒤에 붙인다.
; concnew는 conc1f와 같지만 오로지 멤버에 없을 경우만이다.

;; 섹션 2.2에서 함수는 lambda-expression일 뿐만 아니라 symbol이 될 수 있다 했다.
; Thus it is find to give a whole lambda-expression as the third argument to [define-modify-macro], as in the definition of [conc1f]
; 따라서 define-modify-macro의 세번째 매개변수로 람다표현식 전체를 넣어도 된다.
; conc1 (45페이지)의 내용을 쓰자.
; page45 (Fig4.1 Small functions which operate on lists)
(defun conc1 (lst obj)
(nconc lst (list obj)))

(define-modify-macro conc1f (obj) conc1)
; conc1f라는 이름에
; define-modify-macro는 3개의 매개변수를 받는다.(다시씀)
; 1, name of the macro
;2. its additional parameters (after the generalized variable),
;3. the name of the function which yields the new value for the generalized variable

[on lisp] 12.2 The Multiple Evaluation Problem

;gnu clisp 2.49

(print "Hello, world!")

(defmacro toggle (obj)
    `(setf ,obj (not ,obj)))


(print
;; 빡치게 두번 실행됨. 한번만 실행되어야 하는데
;; 토글 안에서 obj를 두개 넣기 때문임.
(let ((lst '(t nil t))
      (i -1))
     (toggle (nth (incf i) lst))
     lst)
 )
(T NIL T) ; 안됨 첫번째 T가 NIL이 되어야함.
;; 형태를 더 보면
(setf (nth (incf i) lst)
      (not (nth (incf i) lst)))
;; 이렇게 (incf i)를 두번 부르게 된다.

;; 'toggle'의 표현식 인수를 단순히 'setf'의 첫 번째 인수로 삽입하는 것만으로는 충분하지 않다.

; 우리는 들어온 표현식의 내부를 확인해봐야 한다.
; 하위 양식이 포함 된 경우, 부작용이있는 경우 하위 양식을 분리하여 개별적으로 평가해야합니다.

; 쉽게 만들기 위해. Common Lisp는 setf에서 제한된 매크로 클래스를 자동으로 정의하는 매크로를 제공.
; 이 매크로가 setf의 제한된 매크로 클래스 인 'defined-modify-macro'.
; 이름, 추가적인 매개변수(after the generalized variable), 그리고 함수이름 3개의 매개변수를 받는다.

; Using define-modify-macro, we could define toggle as follows:
; (define-modify-macro toggle () not)
; 이 코드는 "to evaluate an expression of the form (toggle place), find the location specified by place,
; and if the value stored there is val, replace it with the value of (not val)"이다.
; (toggle place)형태의 표현식을 평가하려면, place로 특정되는 위치를 찾고, 값이 있다면 그걸 val로 하자, 그 val을 (not value)로 치환하라.
(define-modify-macro toggle () not)
;; 이렇게만 하면 된다. 테스트해보자.
(let ((lst '(t nil t))
      (i -1))
     (toggle (nth (incf i) lst))
     lst)
(NIL NIL T) ; 잘됨

;; 좀더 일반화 할 수 있다.
;setf와 setq는 임의의 갯수의 인수를 취할 수 있으므로 toggle도 마찬가지로 가능할 것이다.
; modify-macro 위에 다른 매크로를 정의하여이 기능을 추가 할 수 있습니다.
;Fig 12.1로 더 자세히 보자.
(defmacro allf (val &rest args)
    (with-gensyms (gval)
        `(let ((,gval ,val))
              (set ,@(mapcan #'(lambda (a) (list a gval))
                             args)))))

(defmacro nilf (&rest args) `(allf nil ,@args))

(defmacro tf (&rest args) `(allf t ,@args))

(defmacro toggle (&rest args)
    `(progn
         ,@(mapcar #'(lambda (a) '(toggle2 ,a))
                   args)))
(define-modify-macro toggle2 () not)
;; 이건 이전에 만든 토글
(defmacro toggle (obj)
    `(setf ,obj (not ,obj)))

[on lisp] 11.5 Iteration with Multiple Values (mvdo*)

11.5 Iteration with Multiple Values
fig 11.10을 보자
(defmacro mvdo* (parm-cl test-cl &body body)
  (mvdo-gen parm-cl parm-cl test-cl body))

(defun mvdo-gen (binds rebinds test body)
  (if (null binds)
      (let ((label (gensym)))
        `(prog nil
           ,label
           (if ,(car test)
               (return (progn ,@(cdr test))))
           ,@body
           ,@(mvdo-rebind-gen rebinds)
           (go ,label)))
      (let ((rec (mvdo-gen (cdr binds) rebinds test body)))
        (let ((var/s (caar binds)) (expr (cadar binds)))
          (if (atom var/s)
              `(let ((,var/s ,expr)) ,rec)
              `(multiple-value-bind ,var/s ,expr ,rec))))))

(defun mvdo-rebind-gen (rebinds)
  (cond ((null rebinds) nil)
        ((< (length (car rebinds)) 3)
         (mvdo-rebind-gen (cdr rebinds)))
        (t
         (cons (list (if (atom (caar rebinds))
                         'setq
                         'multiple-value-setq)
                     (caar rebinds)
                     (third (car rebinds)))
               (mvdo-rebind-gen (cdr rebinds))))))
mvdo*를 써보자
(mvdo* ((x 1 (1+ x))
        ((y z) (values 0 0) (values z x)))
       ((> x 5) (list x y z))
  (princ (list x y z)))
(1 0 0)(2 0 2)(3 2 3)(4 3 4)(5 4 5)
(6 4 5)
와... 이렇게 여러개 반복이 멋지게 된단 말이다. 이런 식의 반복은 꽤나 유용하다. 그래픽 프로그램에서는 종종 좌표나 지역 같은 다중 값들을 한번에 다뤄야 한다. 단순한 대화형(반응형) 게임을 작성한다 할때, 목표는 두 물체 사이에 찌그러지지 않도록 하는 것이다. 만약 두 물체가 동시에 당신을 맞추면 지는 것. 혹은 두 물체가 서로를 부신다면 이기는 것. 그림 11.11 에서 이런 게임을 만들 때 mvdo*가 얼마나 유용할지 보여줄 것.
(mvdo* (((px py) (pos player) (move player mx my))
        ((x1 y1) (pos obj1)   (move obj1 (- px x1)
                                         (- py y1)))
        ((x2 y2) (pos obj2)   (move obj2 (- px x2)
                                         (- py y2)))
        ((mx my) (mouse-vector) (mouse-vector))
        (win     nil          (touch obj1 obj2))
        (lose    nil          (and (touch obj1 player)
                                   (touch obj2 player))))
       ((or win lose) (if win 'win 'lose))
   (clear)
   (draw obj1)
   (draw obj2)
   (draw player)) 
; (pos obj) return two values x, y representing the position of obj. ?Initially, the three objects have random positions
; (move obj dx dy) moves the obj depending on its type and the vector(dx,dy). returns two values x,y indicating the new position.
; (mouse-vector) returns two values dx, dy indicating the current movement of the mouse.
; (touch obj1 obj2) returns true if obj1 and obj2 are touching
; (clear) clear the game region
; (draw obj) draws obj at its current position
아래는 코드를 분석해본 것
(defmacro mvdo* (parm-cl test-cl &body body)
  (mvdo-gen parm-cl parm-cl test-cl body))

(defun mvdo-gen (binds rebinds test body)
  (if (null binds) ; binds가 없다면(재귀 다돌면)
      (let ((label (gensym)))
        `(prog nil
           ,label
           (if ,(car test)
               (return (progn ,@(cdr test))))
           ,@body
           ,@(mvdo-rebind-gen rebinds)
           (go ,label)))
      (let ((rec (mvdo-gen (cdr binds) rebinds test body)))  ;; binds가 있다면. (mvdo-gen (cdr binds) rebinds test body)를 rec로 부른다. (재귀할건가봄)
           (let ((var/s (caar binds)) (expr (cadar binds)))  ;; binds의 첫번째의 심볼값을 var/s라 하고 그 옆에 있는 녀석을 expr로 넣는다.(초기값)
          (if (atom var/s)  ; var/s가 하나라면 (다중이 아니라면)
              `(let ((,var/s ,expr)) ,rec) ; (,var/s ,expr) 로 값을 바인딩함 그 후 rec를 실행 (mvdo*는 순서대로 실행되어야함 바인딩이 순서가 있음)
              `(multiple-value-bind ,var/s ,expr ,rec)))))) ; 여러개면 multiple-value-bind를 쓴다.  (multiple-value-bind (...) (...) rec) rec에서 let과 muliple-value-bind가 계속 만들어짐
; 결국 재귀를 하다가 (if (null binds)...) 에 다다르면
; (prog를 쓰고 (return (progn ,@(cdr test)))로 리턴할 값을 리턴함.
; cadar는 cdr후 car한것
; 데모
'(mvdo* ((x 1 (1+ x))
        ((y z) (values 0 0) (values z x)))
       ((> x 5) (list x y z))
   (princ (list x y z)))
))
;(LET ((X 1))
; (MULTIPLE-VALUE-BIND (Y Z) (VALUES 0 0)
;  (PROG NIL #:G3210 (IF (> X 5) (RETURN (PROGN (LIST X Y Z)))) ; PROG NIL(바인딩없음) #:G3210 고투문을 위한 이름
;   (PRINC (LIST X Y Z)) ; ,@body
;   (SETQ X (1+ X))  ; ,@(mvdo-rebind-gen rebinds)
;   (MULTIPLE-VALUE-SETQ (Y Z) (VALUES Z X)) (GO #:G3210))))  ; (go ,label)))


; rebinds가 nil이면 nil
; (< (length (car rebinds)) 3) 길이가 3보다 작으면(2개면 값설정만 3개면 루프돌때마다 업데이트) (mvdo-rebind-ged (cdr rebinds))
; 
(defun mvdo-rebind-gen (rebinds)
  (cond ((null rebinds) nil)
        ((< (length (car rebinds)) 3)
         (mvdo-rebind-gen (cdr rebinds)))
        (t
         (cons (list (if (atom (caar rebinds)) ; 바인딩될 값이 atom이면 setq
                         'setq
                         'multiple-value-setq) ; 여러개면 multiple-value-setq
                     (caar rebinds)  ; caar로 첫번째(현재바인딩문)의 첫번째 빼냄.(심볼)
                     (third (car rebinds))) ; 업데이트문 빼냄.
               (mvdo-rebind-gen (cdr rebinds)))))) ; cdr로 다음타자 재귀로 부름. 마지막에 cons로 합침.
; 데모
(print (macroexpand
        (mvdo-rebind-gen '((x 1 (1+ x))
        ((y z) (values 0 0) (values z x))))))
; ((SETQ X (1+ X)) (MULTIPLE-VALUE-SETQ (Y Z) (VALUES Z X))) 

'(print (caar '((y z) (values 0 0) (values z x))))

[on lisp] 11.1 ~ 11.4 Classic Macro (다시 정리)

(defun mappend (fn &rest lsts)
  "maps elements in list and finally appends all resulted lists."
  (apply #'append (apply #'mapcar fn lsts)))

(defmacro condlet (clauses &body body)
  (let ((bodfn (gensym))
        (vars (mapcar #'(lambda (v) (cons v (gensym)))
                      (remove-duplicates
                        (mapcar #'car 
                                (mappend #'cdr clauses))))))
    `(labels ((,bodfn ,(mapcar #'car vars)
                 ,@body))
       (cond ,@(mapcar #'(lambda (cl)
                           (condlet-clause vars cl bodfn))
                       clauses)))))

(defun condlet-clause (vars cl bodfn)
  `(,(car cl) (let ,(mapcar #'cdr vars)
                (let ,(condlet-binds vars cl)
                  (,bodfn ,@(mapcar #'cdr vars))))))


(defun condlet-binds (vars cl)
  (mapcar #'(lambda (bindform)
              (if (consp bindform)
                  (cons (cdr (assoc (car bindform) vars))
                        (cdr bindform))))
          (cdr cl)))
(print 
  (macroexpand-1 '(condlet (((= 1 2) (x (princ 'a)) (y (princ 'b)))
                          ((= 1 1) (y (princ 'c)) (x (princ 'd)))
                          (t       (x (princ 'e)) (z (princ 'f))))
                  (list x y z))))
(`((#:G3210 (Y X Z) (LIST X Y Z)))
 (COND
  ((= 1 2)
   (LET (#:G3211 #:G3212 #:G3213)
    (LET ((#:G3212 (PRINC 'A)) (#:G3211 (PRINC 'B)))
     (#:G3210 #:G3211 #:G3212 #:G3213))))
  ((= 1 1)
   (LET (#:G3211 #:G3212 #:G3213)
    (LET ((#:G3211 (PRINC 'C)) (#:G3212 (PRINC 'D)))
     (#:G3210 #:G3211 #:G3212 #:G3213))))
  (T
   (LET (#:G3211 #:G3212 #:G3213)
    (LET ((#:G3212 (PRINC 'E)) (#:G3213 (PRINC 'F)))
     (#:G3210 #:G3211 #:G3212 #:G3213)))))) 

(print 
  (macroexpand '(condlet (((= 1 2) (x 1) (y 1))
                          ((= 1 1) (y 2) (x 2)))
                  (list x y ))))

(LABELS ((#:G3210 (Y X) (LIST X Y))) ; condlet
 (COND ; condlet
  ((= 1 2) ; condlet-clause
   (LET (#:G3211 #:G3212) ; condlet-clause
    (LET ((#:G3212 1) (#:G3211 1)) ; condlet-clause, conlet-binds
      (#:G3210 #:G3211 #:G3212)))) ; condlet-clause
  ((= 1 1) ; condlet-clause
   (LET (#:G3211 #:G3212) ; condlet-clause
    (LET ((#:G3211 2) (#:G3212 2)) ; condlet-clause, condlet-binds
      (#:G3210 #:G3211 #:G3212)))))) l condlet-clause
condlet에서 vars는 각 심볼별로 gensym을 만든다. 심볼은 하나만 gensym으로 만들면 되니까 remove-duplicates으로 겹치는 것을 없앰
(setf my-val     
    (mapcar #'cdr
        ((lambda (clauses) 
            (mapcar #'(lambda (v) (cons v (gensym)))
                (remove-duplicates
                    (mapcar #'car
                            (mappend #'cdr clauses)))))
         '(((= 1 2) (x (princ 'a)) (y (princ 'b)))
           ((= 1 1) (y (princ 'c)) (x (princ 'd)))
           (t       (x (princ 'e)) (z (princ 'f)))))))
((Y . #:G3210) (X . #:G3211) (Z . #:G3212)) 
1. conlet : 필요한 심볼들에 (gensym)을 적용하고 (cond ...)의 형태를 만든다. ... 안에 넣을 형태를condlet-clause에게 만들도록 위임 (mapcar로 하나씩)
label을 붙이기 위해 bodfn에도 gensym을 만듬.

이제부터 중요한 것은 심볼대신 gensym 값들을 가지고 놀아서 매크로를 조작한다. 정말 꽤나 어렵다. 자세히보자.
바인딩이 끝났으면,label을 이용하여 함수를 하나 만든다. 바로 쓰지는 않고 이 label에 붙여진 (gensym)인 bodfn을 다음 함수에 던진다.
이 다음함수(condlet-clause)가 바인딩 후 람다를 실행하는 일까지 한다.

2. condlet-clause (조건절을 만들고 그 뒤에 필요한 심볼의 gensym을 LET에 넣고, 다음 LET에 선택된 심볼에 바인딩될 값을 condlet-binds에서 위임
조건절은 아래와 같다.
,(car cl)
필요한 심볼의 gensym값을 모두 넣는다.
(let ,(map #'cdr vars) 
   ...)
선택된 심볼에 들어갈 값을 conlet-binds가 하도록 한다.
; vars는 (x #:G1 y #:G2) 형태
; cl는 (조건절 바인딩) 형태
(let ,(condlet-binds vars cl) 
그 후 넘겨받은 bodfn(라벨함수)를 실행하고 그 매개변수로 바인딩된 심볼의 (gensym)을 넣는다.
(,bodfn ,@mapcar #'car vars)...

3. condlet-binds : 어떻게 바인딩 되는지 선택하고 그 형태를 만든다.
일단 mapcar를 이용하여 lambda를 실행하는 형태인데, (let ...) 안에 들어가는 리스트를 뱉으면 된다.
아래와 같은 형태다.
((#:G3211 2) (#:G3212 2))
먼저 assoc이 뭔지 보자
(setq values '((x . 100) (y . 200) (z . 50)))
(print (assoc 'y values))
(Y . 200) 

이제 조건절은 썼으니 필요없고 바인딩될 표현식만 필요하다.
mapcar의 두번째 매개변수로 이것들만 들어갈 것이다. 그리고 이것은 lambda에서 bindform이라는 이름으로 들어간다.
(cdr cl)
bindform으로 들어온 녀석이 cons인지(리스트인지) 확인한다. 대부분 cons일 것이나 아니면 nil을 뱉는것 같다.
#'(lambda (bindform)
    (if (consp bindform)
        ...
하이라이트다. 여기서는 bindform은 (x 100) 뭐 이런 형태로 이루어져 있을 것이다. 위 예제를 보자.
clauses에서 cl하나는 ((= 1 2) (x 1) (y 1)) 이걸 말한다.
여기서 (car cl)은 (= 1 2)
(cdr cl)은 ((x 1) (y 1))이다.
여기서 mapcar 에 람다로 들어오는 값은 각각 (x 1) 와 (y 1)가 따로 들어온다.
그렇다면 (car bindform)은 뭘까
(car '(x 1)) ; x 심볼을 가져옴
(assoc (car bindform) vars) ; 심볼을 가져와서 vars에 해당심볼(x)를 가진 리스트를 찾아낸다.
(cdr (assoc (car bindform) vars) ; (x #:G12) 형태를 받을 것인데 거기서 cdr로 (gensym)값을 해시맵처럼 가져온다.
(cons (cdr (assoc (car bindform) vars)) ; cons로 gensym값과 바인딩될 벨류를 연결하여 리스트를 만든다.
                  (cdr bindform)))) ; bindform은 (x 1)의 형태로 가져온다. (cdr bindform)은 1
; (#:G1 10) 뭐 이런 형태로 들어간다.
즉 mapcar의 개별 값을 알았으니 전체 값을 돌리면
'((#:G1 10) (#:G2 30))
이런 형태를 만들어질 것이다.

해석 끝

11.2 The with- Macro
두번째로 컨텍스트를 다루는 타입은 with-를 사용하는 매크로다. 넓은 시야로 보면, 컨텍스트는 하나의 세상 안에 있는 상태들이다. 이 상태들에는 특별한 변수들의 값, 자료구조의 내용물, 리스프 바깥 세상의 상태값들을 말한다.
이런 종류의 컨텍스트를 만들고자 한다면 매크로로 해야 한다. 그렇지 않으면 코드의 body들이 closure로 전부 감싸야 한다.
(with-open-file (s "dump" :direction :output)
  (princ 99 s))
표현식의 평가가 끝나면 "dump"파일은 저절로 닫힌다.
이런 연산자는 매크로로 정의되어야 한다. 왜냐하면 s를 바인딩 하기 때문이기도 하지만, 어짜피 새로운 컨텍스트에서 form(body)가 평가되어야 한다.
일반적으로 컨텍스트를 생성하는 매크로는 코드블록 안에다가 확장을 한 다음, 코드 앞 뒤로 할 일을 더한다.
일반적으로 코드가 body 뒤에 실행되면, 그 목적ㅇ느 시스템의 일관된 상태(실행이전 상태가 대부분일 듯)로 정리하는 것이다.
예를들어 with-open-file은 열어둔 파일을 닫아야 한다. 이런 경우, 일반적으로 unwind-protect로 context-creating매크로가 확장된다.

unwind-protect의 목적은 실행 중에 인터럽트가 발생해도 특정 표현식은 평가되도록 한다.
하나 이상의 매개변수를 받아서 순서대로 평가한다. 만약 모두 잘 진행되면 첫번째 매개변수를 리턴한다.
prog1처럼 prog1과의 차이는 error가 나더라도 나머지 매개변수가 평가되는 것이다.
(setq x 'a)
A

(unwind-protect
  (progn (princ "What error?")
         (error "This error."))
  (setq x 'b))
What error?
>> Error: This error.
자 setq가 실행되기 전에 에러를 던지게 했다.그런데 한번 x가 어떻게 되었나 보자.
x
B
with-open-file이 unwind-protect로 확장되기 때문에, 파일은 실행중에 에러가 나도 클로즈가 된다.

컨텍스트생성 매크로들은 대게 특정 앱을 위해 작성되어 진다. 예를 들어 다중원격 DB를 다루는 프로그램을 작성한다 하자.
프로그램은 한번에 한 DB랑 대화하며, 그 DB는 글로벌 *db*에 있다. DB에 연결하여 일을 하기 전에 락을 먼저 걸어야 한다. 그래야 다른 녀석이 동시에 사용할 수 없을 것이다.
만약 디비(db)에서 쿼리(q)에서 값을 원한다면, 아래처럼 짤 것이다.
(let ((temp *db*))
  (setq *db* db)
  (lock *db*)
  (prog1 (eval-query q)
         (release *db*)
         (setq *db* temp)))
매크로로 이 모든 장부를 숨길 수 있다. 아래 코드들을 보자.
; pure macro
(defmacro with-db (db &body body)
  (let ((temp (gensym)))
    `(let ((,temp *db*))
       (unwind-protect
         (progn
           (setq *db* ,db)
           (lock *db*)
           ,@body)
         (progn
           (release *db*)
           (setq *db* ,temp))))))

; Combination of macro and function:
(defmacro with-db (db &body body)
  (let ((gbod (gensym)))
    `(let ((,gbod #'(lambda () ,@body)))
       (declare (dynamic-extent ,gbod))
       (with-db-fn *db* ,db ,gbod))))

(defun with-db-fn (old-db new-db body)
  (unwind-protect
    (progn
      (setq *db* new-db)
      (lock *db*)
      (funcall body)) ; with-db-fn은 함수라서 일부러 람다로 보냄
    (progn
      (release *db*)
      (setq *db* old-db))))
복잡성이 올라가면 2번째 방식이 더 실용적이다.
CLTL2 COMMON LISP에서, dynamic-extent 선언은 효율적인 할당을 위해 body를 가지는 closure를 허용한다. (CLTL1은 안됨)
우리는 with-db-fn을 실행하는 동안만 closure가 필요하며, 이 선언으로 컴파일러가 스택에 이것을 위한 공간을 할당할 수 있도록 허용함.
이 공간은 가비지 컬렉터가 회수하지 않고 let식이 끝나면 자동으로 회수됨.

11.3 조건평가
조건절에 따라서 평가를 안하고 끝내는 수가 있다( 좋은 것)
(if t
    'a
    (/ x 0))
0으로 나누는 것은 에러이지만 'a를 뱉으면서 잘 동작하고 끝난다.
아래 if3는 if문에서 nil값도 따로 보는 것. if3에서 (nil)로 감싼 이유는 nil은 꽤나 모호한 의미를 가지기 때문이다.
; Fig 11.5 : Macros for conditional evaluation
(defmacro if3 (test t-case nil-case ?-case
  `(case ,test
     ((nil) ,nil-case)
     (?     ,?-case)
     (t     ,t-case)))

(defmacro nif (expr pos zero neg)
  (let ((g (gensym)))
    `(let ((,g ,expr))
       (cond ((plusp ,g) ,pos)
             ((zerop ,g) ,zero)
             (t ,neg)))))
nif(numeric if)는 숫자에 따라 리턴된다.
(mapcar #'(lambda (x) (nif x 'p 'z 'n))
        '(0 1 -1))
(Z P N)

그림 11.6은 조건평가를 이용한 다양한 매크로를 보여준다.
매크로 in은 효율적으로 값이 있는지 확인한다. 만약 in을 확장하면 아래처럼 보일 것이다.
(let ((x (foo)))
  (or (eql x (bar)) (eql x (baz))))

;; 하지만 member를 이용하면 아래처럼 이뻐찐다.
(member (foo) (list (bar) (baz)))
;; 하지만 효율적인가?
member는 두 측면에서 비효율적이다.
1 member가 검색하기 위해선 임시 리스트가 필요하다 (만드는 리소스)
2 이 임시 리스트를 만들기 위해 모든 리스트는 평가되어야 한다. 비록 몇 개는 필요 없어도 (필요없는 평가)
(foo)가 (bar)라면 (baz)는 평가할 필요가 없다.
우리는 이거 대신 좀 더 효율적인 추상화를 만들 수 있다. or을 이용하는 효율이면서 member와 같은 추상을 사용하는 매크로!
그것이 in 이다
(defmacro in (obj &rest choices)
  (let ((insym (gensym)))
    `(let ((,insym ,obj))
       (or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
                     choices)))))

(defmacro inq (obj &rest args)
  `(in ,obj ,@(mapcar #'(lambda (a) `',a)
                      args)))

(defmacro in-f (fn &rest choices)
  (let ((fnsym) (gensym)))
    `(let ((,fnsym ,fn))
       (or ,@(mapcar #'(lambda (c) `(funcall ,fnsym ,c))
                     choices)))))

(defmacro >case (expr &rest clauses)
  (let ((g (gensym)))
    `(let ((,g ,expr))
       (cond ,@(mapcar #'(lambda (cl) (>casex g cl))
                       clauses)))))

(defun >casex (g cl)
  (let ((key (car cl)) (rest (cdr cl)))
    (cond ((consp key) `((in ,g ,@key) ,@rest))
          ((inq key t otherwise) `(t ,@rest))
          (t (error "bad >case clause")))))
이제 in을 사용해보자
(in (foo) (bar) (baz))

(let ((#:g25 (foo)))
  (or (eql #:g25 (bar))
      (eql #:g25 (baz))))
inq(in queue)를 보자
(inq operator + - *)
;; expands into
(in operator '+ '- '*)

(member x (list a b) :test #'equal)
; can be duplicated by
(in-if #'(lambda (y) (equal x y)) a b)

(some #'oddp (list a b))
; becomes
(in-if #'oddp a b)

11.4Iteration 반복
때로 문제는 매개변수가 항상 평가되서가 아니다, 오히려 한번만 평가되는 경우가 문제인 경우가 있다.
표현식의 바디가 반복하려면 매크로 써야함
(defmacro forever (&body body)
  `(do ()
       (nil)
     ,@body))
단순한 반복예시로 일단 맛보자
; Fig 11.7 : Simple iteration macros.
(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

(defmacro till (test &body body)
  `(do ()
       (,test)
     ,@body))

(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ ,var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))
이제 본편으로 들어가자. 그림 11.8에서 하나만 잘라서 봐보자
(defmacro do-tuples/o (params source &body body)
  (if parms
      (let ((src (gensym)))
        `(prog ((,src ,source))
           (mapc #'(lambda ,parms ,@body)
                 ,@(map0-n #'(lambda (n)
                               `(nthcdr ,n ,src))
                           (1- (length parms))))))))

(do-tuples/o (x y) '(a b c d)
  (princ (list x y)))
(A B)(B C)(C D)
NIL
이렇게 연결되는 것이다. 신기하다. 어덯게 펼쳐지는지 이야기 해보자.
; 
(macroexpand
  (do-tuples/o (x y) '(a b c d) (princ (list x y))))

(BLOCK NIL
 (LET ((#:G3215 '(A B C D)))
  (TAGBODY
   (MAPC #'(LAMBDA (X Y) (LIST X Y)) (NTHCDR 0 #:G3215) (NTHCDR 1 #:G3215)))))
TAGBOY는 라벨같은 거다. GOTO문처럼 이동을 하게 해주는 녀석인데 여기서는 안쓰는 듯하다. 생기기만 하고
NTHCDR은 첫번째 매개변수의 숫자만큼 까고 나머지 cdr 리스트를 리턴한다.

내가 단계단계 연필로 확장해본 걸 아래 적어놓겠다.
; 1
(if '(x y)
  (let ((src #:G1))
    `(prog ((#:G1 '(a b c d))
       (mapc #'(lambda (x y) (princ (list x y)))
             ,@(map0-n #'(lambda (n) '(nthcdr ,n ,src))
                       (1- (length parms)))))))
; parms 이 있는 경우
; src (gensym)으로 생성

; 2
(let ((src #:G1))
  `(prog ((#:G1 '(a b c d))
     (mapc #'(lambda (x y) (princ (list x y)))
           ,@('(a b c d) '(b c d)))...)

; 3
`(prog ((#:G1 '(a b c d))
   ...
   (princ (list a b))
   (princ (list b c))
   (princ (list c d))
  nil)
정말 매크로를 만드는 작업은 하나의 예술과도 같다.

[on lisp] 10 Other Macro Pitfalls

10.1 Number of Evaluations

; Fig 10.1 : Controlling argument evaluation.
; A correct version
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ ,var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))

;; subject to multiple evaluations:
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var)))
       ((> ,var ,stop))
     ,@body))

;; Incorrect order of evaluation:
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,gstop ,stop)
          (,var ,start (1+ ,var)))
         ((> ,var ,gstop))
       ,@body)))
2번째 for문에는 버그가 있다.
; 무한에 가까운 출력
(let ((x 2))
  (for (i 1 (incf x))
    (princ i)))
stop 매개변수로 들어온 녀석이 각 루프를 돌 때마다 평가된다.
이 말은 (incf x)가 매번 평가되면서 끝나지 않는 것이다.
어떻게 다른애들은 괜찮고 얘는 괜찮지 않을까?
다른 애들은 어떻게 한번만 실행되고 안되는 것일까?
이 생각은 일단 접어두고

매개변수로 들어온 stop에 side-effect가 있다면, 결과값은 위처럼 문제가 생길 수 있다는 것이다.
for같은 매크로를 만들 때, 꼭 알아야 할 것이 있다. 매크로는 값을 받아서 실행하는 녀석이 아니다. 그럴거면 함수를 만들었을 것이다.
매크로는 표현식을 받는다. 그것도 자신의 리스트 자료구조로 말이다.
그리고 이 표현식이 확장시 있는 곳에 따라, 평가는 한번 이상 평가될 수 있다.

이 경우 값을 바인딩 해놓는 것이다. stop으로 들어오는 표현식을 미리 평가하여 값으로 받은 후 loop에는 해당 값이 들어가도록 하는 것이다.
위에 내용들 보면 gstop이 그런 역할을 하는 녀석.

매크로는 반복을 위한 녀석임이 확실하지 않으면, 매크로 호출에 나타나는 횟수만큼 정확하게 평가되도록 해야 한다.
이 경우가 적용되지 않는 명백한 사례가 있다: 커먼리습의 or에서 들어온 매개변수가 모든 평가되어야 한다면 꽤나 쓸모없는 녀석일 것이다.
그러나 그런 경우 유저는 얼마나 많은 평가가 일어날지 예상할 수 있는지 안다.
하지만 for의 두번째 경우는 그렇지 않다.
사용자는 stop표현식이 두번 이상 평가된다고 가정할 이유가 없으며, 실제로 평가할 이유도 없다. (그런 내용이 매크로를 호출할 때 보이지 않는다는 것이다)
바로 이 두번째 for문 매크로가 가장 실수를 많이하는 경우일 것이다. 단서가 보이지 않으니까.

setf를 이용한 매크로의 경우 Unintended multiple evaluation(의도치 않은 다중 평가)라는 아주 어려운 문제를 직면하는 경우가 많다.
커먼리습은 이런 매크로를 만드는 것이 쉽도록 여러가지 유틸리티를 제공한다. (챕터 12에서 보여줌)

10.2 Order of Evaluation(평가의 순서)
표현식 평가의 횟수만이 문제가 아니라, 평가의 순서가 문제를 일으킬 수 있다.
커먼리습 함수 호출에서, 매개변수는 왼쪽에서 오른쪽 순서로 평가된다.
(setq x 10) ; 10
(+ (setq x 3) x) ; 6
이렇게 다르다. 매크로도 동일한 일이 일어난다.
매크로는 일반적으로 매크로 호출을 할 때 보이는 순서와 동일한 순서로 내부적으로 평가되어야 한다.
아래 for문이 그 법칙을 무시한 녀석이다. 잘 보면 var이 첫번째 매개변수고 stop이 두번째 매개변수인데
뒤바뀌어 있다. 호출할 때는 이렇게 된다는 힌트가 전혀 없다. 다들 평가가 매개변수의 순서대로 될 것이라고 생각할 것이다.
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,gstop ,stop)
          (,var ,start (1+ ,var)))
         ((> ,var ,gstop))
       ,@body)))
하여 for문에는 작은 버그가 있는데, stop이 start보다 먼저 평가되는 것이다.
(let ((x 1))
  (for (i x (setq x 13))
    (princ i)))
13
NIL
보면 알겠지만, 123456789010111213 이렇게 보여야 하는데 (setq x 13)이 먼저 세팅되면서
루프가 돌 기회가 없게 된 것이다.

10.3 Non-functional Expanders
리스프에서는 매크로로 확장된 코드가 순수 함수적이길 바란다. 확장코드(expander code)는 매개변수말고는 영향을 받지 말아야 하며 리턴값 외에는 다른 영향을 주면 안된다.
컴파일된 코드의 매크로 호출은 런타임에 다시(또) 확장되지 않는다고 봐도 무방하다.
만약 그렇게 되지 않으면 커먼리습은 이 확장이라는 것이 언제, 어디서, 얼마나 자주 매크로 호출이 확장되는지 알 수 없게 된다.
이런 것들 중 하나로 매크로의 확장이 달라지면 에러로 간주한다. (상관없어야 한다)
아래를 보자.
(defmacro nil! (x)  ;;wrong
  (incf *nil!s*)
    `(setf ,x nil))
글로벌 값 *nil!s*가 정말 매크로가 호출될 때 1번씩 호출되서 카운팅 해줄까? 그렇게 생각하면 오산이다.
주어진 호출은 한번이상 확장될 수 있고, 종종 그렇게 된다.
소스코드를 변환을 수행할 프리프로세서는 일단 변환을 하기 전에 평가를 먼저 해서 변환을 할지 안할지를 결정한다.

다시 말하지만, 일반적으로 확장자코드는 인수 외의 것에 의존하면 안된다.
예를들어, 문자열에서 확장을 빌드하는 매크로가 있다하자.
이 매크로가 확장시 패키지가 무엇인지 그런건 가정하지 않도록 하자.
(defmacro string-call (opstring &rest args) ; wrong
  `(,(intern opstring) ,@args))

(defun our+ (x y) (+ x y))
OUR+
(string-call "OUR+" 2 3)
5
보면 intern을 이용하여 문자열을 가져와 연관된 심볼를 리턴한다. 하지만 우리가 optional package argument을 안쓰면(제거하면), 이건 현재 패키지에서 일어날 것이다.
따라서, 확장이 생성될 때 패키지에 의존한다. 해당 패키지에 our+가 표시되지 않으면 해당 내용은 nil을 리턴할 것이다.
이 말은 환경에 따라 값이 달라질 것이란 말임..

Miller and Benson's Lisp Style and Design에선 expander code안에 side-effect가 있는 것을 특별히 멍청한 예로 제시했다.

또 다른 문제가 있는데 그것은 &args에서 온다.
&rest 파라미터에 들어오는 값은 새로 만들어진 거라고 개런티할 수 없다.
결론만 먼저 말하면, &rest 파라미터로 오는 값을 파괴적으로 수정하면 안된다(값을 바꾸면 안대!)
이런 경우는 함수와 매크로 모두에게 해당하는 말이다. 함수로 쓸 때는 apply와 함께 쓸 때 문제가 나타난다.
(defun et-el (&rest args)
  (nconc args (list 'et 'al)))

(et-al 'smith 'jones)
; (SMITH JONES ET AL)
하지만 여기서 apply를 이용해서 호출하면 어떻게 될까. 이미 존재하는 자료구조를 바꾼다.
(setq greats '(l m))
(apply #'et-el greats)
; (l m et el)
greats
; (l m et el)

매크로에서는 문제가 더 심각해진다. 매크로에서 &rest를 변경한다면 매크로 호출을 변경하는 일이 생기게 된다.
이 말은 의도치 않게 매크로를 자기자신이 재작성하는 일이 벌어진다.
여기서 문제는 더 심각해진다. 이 일은 실제로 이미 존재하는 구현 위에서 일어난다.
만약 nconc를 &rest매개변수에 적용하는 매크로를 정의한다고 해보자.
(defmacro echo (&rest args)
  `',(nconc args (list 'amen))) ; `',(foo) is equivalent to `(quote ,(foo)).
그리고 이제 함수를 정의하고 호출한다.
(defun foo () (echo x))

(foo) ; (X AMEN AMEN)
(foo) ; (X AMEN AMEN AMEN)
뭐지 계속 foo가 바뀐다. 왜냐하면 각 매크로 확장이 foo의 정의를 변경하는 것이다.
바꿔보자.
(defmacro echo (&rest args)
  `'(,@args amen))
@(comman-at)은 append와 같기 때문에 파괴적이지 않다. 그러므로 안전하다.

그런데 이걸로 끝일까.
매크로에서는 &rest이것만 조심한다고 끝나는 것이 아니다.
아무 매크로 매개변수가 그렇게 될 수 있다. 그 녀석이 리스트라면
(defmacro crazy (expr) (nconc expr (list t)))

(defun foo () (crazy (list)))

(foo) ; (T T)

10.4 Recursion
함수를 재귀적으로 만드는 것은 자연스러운 일이다.
(defun our-length (x)
  (if (null x)
      0
      (1+ (our-length (cdr x)))))
(defun our-length (x)
  (do ((len 0 (1+ len))
       (y x (cdr y)))
      ((null y) len)))
첫번째는 재귀적이고 두번째는 아니다.
그런데 매크로에서는 그저 backquotes나 commas를 넣는다고 쉽게 재귀가 되지 않는다.
; Fig 10.2 : Mistaken analogy to a recursive function.
; this will work
(defun ntha (n lst)
  (if (= n 0)
      (car lst)
      (ntha (- n 1) (cdr lst))))

; this won't work
(defmacro nthb (n lst)
  `(if (= ,n 0)
       (car ,lst)
       (nthb (- ,n 1) (cdr ,lst))))
왜 nthb가 문제가 되는 걸까. 바로 확장할때마다 자신을 가지고 있기 때문인데
함수의 경우 확장을 하면 매개변수의 값이 평가가 되기 때문에 언제 멈출지 안다. 하지만 매크로는 형태만 있기 때문에 계속 무한 확장하는 수가 있다.
; (nthb x y) 를 확장하자.
(if (= x 0)
    (car y)
    (nthb (- x 1) (cdr y)))
; 위 녀석은 다시 확장된다.
(if (= x 0)
    (car y)
    (if (= (- x 1) 0)
        (car (cdr y))
        (nthb (- (- x 1) 1) (cdr (cdr y)))))
그럼 어떻게 해야 하나. 일단 재귀적으로 풀지 않으면 된다. 아래처럼
(defmacro nthc (n lst)
  `(do ((n2 ,n (1- n2))
        (lst2 ,lst (cdr lst2)))
       ((= n2 0) (car lst2))))

아래 다른 예시가 있다.
(defmacro nthd (n lst)
  `(nth-fn ,n ,lst))

(defun nth-fn (n lst)
  (if (= n 0)
      (car lst)
      (nth-fn (- n 1) (cdr lst))))

(defmacro nthe (n lst)
  `(labels ((nth-fn (n lst)
              (if (= n 0)
                  (car lst)
                  (nth-fn (- n 1) (cdr lst)))))
     (nth-fn ,n, lst)))
보면 알겠지만 재귀는 함수로 만들어서 그걸 사용하는 것이다.
혹은 labels를 이용하여 로컬 함수를 정의하여, 그 녀석을 실행하는 것이다.
결국 두 경우 모두 함수와 매크로를 더해서 실행하는 방식이다.
물론 매크로가 전부 확장 할 수는 없지만 어쨋든 재귀는 값을 평가하긴 해야 한다.
그러므로 재귀적으로 평가는 가능하게 만들 수는 있는 것이다.

매크로의 확장함수가 일반 리스프 함수이니까, 우리는 이걸로 재귀적으로 만들 수 있을 것이다.(그러니까 매크로의 확장이 매크로일 수 있겠냐 라는 것이다. 지금까지 안되었는데)
built-in or함수를 정의하면서 봐보자.
macro ora는 or-expand라는 재귀함수를 호출하여 확장을 생성한다.
orb도 같은 일을 한다. orb는 특이하게 값이 아니라 매크로에 대한 인수에서 반복한다 (??)
이렇게 되면 무한으로 펼쳐질 것 같지만, 하나의 매크로 확장단계에서 생성된 orb에 대한 호출은 다음 단계에서 let으로 대체된다(?) 하여 최종 확장에서는 let들의 중첩들에 지나지 않는다.
아래를 보자.
; (orb x y) 확장
(let ((g2 x))
  (if g2
      g2
      (let ((g3 y))
        (if g3 
            g3 
            nil))))
;; 여기서 마지막 nil이 있는데 거기서 재귀가 잘 멈춘것.
;; 아래는 코드
(defmacro ora (&rest args)
  (or-expand args))
(defun or-expand (args)
  (if (null args)
      nil
      (let ((sym (gensym)))
        `(let ((,sym ,(car args)))
           (if ,sym
               ,sym
               ,(or-expand (cdr args)))))))

(defmacro orb (&rest args)
  (if (null args)
      nil
      (let ((sym (gensym)))
        `(let ((,sym ,(car args)))
           (if ,sym
               ,sym
               (orb ,@(cdr args)))))))

[on lisp] 18. destructuring - 1

이 매크로를 이해하는데 꽤나 오랜 시간이 걸렸다...
정말 폴그레이엄 미친 사람이다. 어떻게 이런책을 쓰고 이런 기술을 무료로 풀 수가 있는 거지?
이정도의 기술은 아무것도 아니라는 것인가?

그 머리를 가지고 싶다.
다른 onlisp내용은 스킵을 해도 이렇게 어렵게 이해한 내용은 적어놔야겠다.
커먼리습으로 평생 코딩을 하지 않더라도 이런 내용을 익힌다는 것은 참으로 재미 있다.

한가지 문제는 챕터 18. destructuring이 이제 시작이라는 점이다.
이 챕터는 특이하게 내용이 너무 많다...
;; '(dbind (a b c) (list 1 2 3) (list a b c))
;; pat = (a b c), seq = (list 1 2 3), body = (list a b c)
;; 바인딩할 값들을 gseq로 설정한 후 
;; dbind-ex으로 표현식을 만드는데, 바인딩하는 형태는 destruc가 한다.
(defmacro dbind (pat seq &body body)
  (let ((gseq (gensym)))
    `(let ((,gseq ,seq))
       ,(dbind-ex (destruc pat gseq #'atom) body))))

; let 안에 들어갈 리스트를 생성한다.
; 1. pat이 없으면 nil를 던진다(재귀의 베이스케이스) 아마 (cons ... nil)로 생성될듯
; 2 rest를 바인딩한다. 자세한 내용은 아래 잇음.
; 3 rest가 하나만 리스트가 아닌 경우만 rest가 걸리는데
;   걸리면 `((,rest (subseq ,seq ,n))) 이거로 한번에 다! 연결해버린다. rest가 원래 그런거니까 다음 내용 다 바인딩하는 녀석
;   ((( (subseq (list 1 2 3) 1)) => ((A (2 3)) 
; 4.rest가 없으면 일반적인 바인딩이 시작된다. (재귀 시작)
; 5. 이제 (car pat)로 첫번째 심볼을 꺼낸다.
; 6. (rec (destruc (cdr pat) seq atom? (1+ n))) 로 재귀로 다음 녀석들을 재귀로 만들어 낸다.
;    그 다음에 뭐 일단 첫번째 녀석을 먼저 손봐보자 재귀를 하는 곳이 6번만이 아니다.
; 7. p가 하나가 아니라면? 첫번째 가져온 바인딩될 녀석이 또! 리스트라면? (destructuring중이다)
; 8. 일단 p가 하나인 경우를 보자. 
; 9. (cons `(,p (elt ,seq ,n)) rec) 중요한 것은 (cons A rec) 에서 A다 여기가 이번 호출에서 하는 일이다.
; (,p (elt ,seq ,n)) 라고 해보자. 첫번째 재귀라면 (A (elt (list 1 2 3) 0)) -> (A 1) 이렇게 될 것이다. 
; 두번째는 (B (elt (list 1 2 3) 1)) -> (B 2)가 될 것이다. elt가 위치의 값을 가져온다.
; 10. 그 값을 cons로 뒤에 재귀값과 더하는 것이다.
; 11. 드디어 왔다. 만약 p가 리스트라면!!
; 여기 새로운 seq를 바인딩하여 캡처링을 보호해야 한다. (var (gensym)) 으로 임의의 심볼을 만든다.
; 12. (cons (cons `(,var (elt ,seq ,n))
;                  (destruc p var atom?))
;           rec
; cons가 두개 인걸 보니 바인딩 리스트 안에 리스트를 또 만든 듯. 
; `(,var (elt ,seq ,n))는 (#:G2 (elt seq n))으로 현재 위치에 있는 바인딩 값을 바인딩 해서 재귀로 쓸 때 보낸다. 
; 아래테스트 코드에도 설명이 되어 있다.
(defun destruc (pat seq &optional (atom? #'atom) (n 0))
  (if (null pat) ; 1
      nil
      (let ((rest (cond ((funcall atom? pat) pat) ; 2 처음들어오면 여긴 아님.
                        ((eq (car pat) '&rest) (cadr pat)) ; 3 일단 예시에서는 없음
                        ((eq (car pat) '&body) (cadr pat)) ; 4 여기도 아님.
                        (t nil)))) ; 대부분 리스트로 들어올테니 여기 nil이 된다.
       (if rest ; pat가 atom 이거나 &rest,&body가 아니라 '(a b c)로 들어오면 nil임.
           `((,rest (subseq ,seq ,n))) ; ((A (subseq (list 1 2 3) 1)) => ((A (2 3)) 
           (let ((p (car pat)) ; 4.5 리스트의 첫번째 녀석을 
                 (rec (destruc (cdr pat) seq atom? (1+ n)))) ; 6
             (if (funcall atom? p) ; 
                 (cons `(,p (elt ,seq ,n)) ;9  여기가 중요하다. p는 파라미터 심볼 하나고 elt로 seq의 n번재 녀석을 가져온다는 뜻이다. 그러므로 여기서는 바인딩만 가져오는 것.
                       rec) ; 10
                 (let ((var (gensym))) ; 11
                   (cons (cons `(,var (elt ,seq ,n)) ; 12
                               (destruc p var atom?)); 
                         rec))))))))

; destruc로 만든 코드를 dbind-ex에 binds라는 이름으로 들어온다.
; 바인드가 있으면,
; (let ...)안에 들어가게 되는데 바로 들어가는 것은 아니고
; #'(lambda (b) (if (consp (car b)) (car b) b)) 를 한번씩 거친다.
; (car b)가 리스트면 그대로 보내고 (car b)가 리스트가 아니라면 b를 놓는다.(리스트 형태를 그대로 유지한다. 해당 심볼은 nil이 될듯 바로 (b nil)로 바인딩된다.
; 왜냐하면 (b nil) == (cons b (cons nil))이니까...
; 특이한 점은 리스트가 여기서 다 바인딩 되는 것이 아니다. 바깥에 바인딩 된 녀석들만 바인딩이 되는데
; 중요한 것은 여기서 바인딩 안에 들어갈 새로운 seq의 gensym 값도 바인딩이 된다. 
; 그리고 그녀석이 다음 재귀에서 만들어질 녀석들이 사용할 seq값이다.
; 다 돌면 body를 푼다.
(defun dbind-ex (binds body)
  (if (null binds)
      `(progn ,@body)
      `(let ,(mapcar #'(lambda (b)
                         (if (consp (car b))
                             (car b)
                             b))
                     binds)
        ,(dbind-ex (mapcan #'(lambda (b)
                               (if (consp (car b))
                                   (cdr b)))
                           binds)
                   body))))


;(print
; (macroexpand '(dbind (a b c) (list 1 2 3) (list a b c))))
;(LET ((#:G3210 (LIST 1 2 3)))
; (LET ((A (ELT #:G3210 0)) (B (ELT #:G3210 1)) (C (ELT #:G3210 2)))
;  (PROGN (LIST A B C))))
; #:G3210 binding에 연결.
; (ELT가 뭐지... ELT로 해당 앨리먼트의 0번째 1번째 2번째를 가져오는 것 같다.)
; 그 후 (PROGN에 바디를 바인딩한다.

;(print
; (destruc '(a b c) (list 1 2 3) #'atom)
; )
; ((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2))) 


; 리스트 안에 리스트가 있는 경우
'(print
 (macroexpand '(dbind (a (b c)) (list 1 (list 2 3)) #'atom)))
;(LET ((#:G3210 (LIST 1 (LIST 2 3))))
; (LET ((A (ELT #:G3210 0)) (#:G3211 (ELT #:G3210 1)))
;  (LET ((B (ELT #:G3211 0)) (C (ELT #:G3211 1))) (PROGN #'ATOM)))) 
;이걸보면 알 수 있는 것이. 바인딩 되는 심볼이 깊이가 있으면 그 갯수만큼 (gensym)이 있게 된다.
;그 이유는 바인딩할 seq(list 1 (list 2 3))이 달라지기 때문이다.
;위 코드에서 두번째 코드가 신기한데. 일단 새롭게 바인딩할 값 또한 리스트의 리스트로 들어가야 하는 상황인 것이다.
;(#:G3211 (ELT #:G3210 1))
;이 코드인데 (#:G3211 (elt (list 1 (list 2 3)) 1)이 되는 것이다.
;그러면 (#:G3211 (list 2 3))이 되면서 리스트안으로 들어가게 된다.
;

'(print
 (destruc '(a (b c)) (list 1 (list 2 3)) #'atom))
; ((A (ELT (1 (2 3)) 0))
; ((#:G3210 (ELT (1 (2 3)) 1)) (B (ELT #:G3210 0)) (C (ELT #:G3210 1)))) 

(print
 (dbind-ex '((A (ELT (1 (2 3)) 0))
              ((#:G3210 (ELT (1 (2 3)) 1)) (B (ELT #:G3210 0)) (C (ELT #:G3210 1)))) 
            (list 1 (list 2 3))))
;(LET ((A (ELT (1 (2 3)) 0)) (#:G3210 (ELT (1 (2 3)) 1)))
; (LET ((B (ELT #:G3210 0)) (C (ELT #:G3210 1))) (PROGN 1 (2 3)))) 

(print
 (dbind-ex '((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2))) '(list a b c)))
;(LET ((A (ELT (1 2 3) 0)) (B (ELT (1 2 3) 1)) (C (ELT (1 2 3) 2)))
;  (PROGN LIST A B C)) 

;subseq가 뭔지도 몰랐다.
'(print (subseq (list 1 2 3) 0)) ; (1 2 3)
'(print (subseq (list 1 2 3) 1)) ; (2 3)
'(print (subseq (list 1 2 3) 2)) ; (3)


2019년 9월 2일 월요일

[on lisp] 감상평

on lisp를 3분의2를 읽고 이제 잠시 다른 책을 읽을까 한다. Land Of Lisp의 번역본을 읽다 만 적이 있는데, on lisp를 보다보니 읽을 수 있을 것만 같다.

on lisp는 대단한 책이다. 한명이서 이렇게 대단한 글을 쓸 수 있는지 놀랍다. 아주 간결하고, 무섭다. 뒤로가면 갈 수록, 꽤나 어렵기는 하지만 쉽게 쓰는 것은 그가 할 일이 아닌 것 같다.

일단 이제 on lisp에 대한 내용은 적지 않고 눈팅만 할까 한다.

사실 이 책으로 커먼리습의 개념정도만 훑고 매크로에 대해서 clojure라는 언어랑 어떻게 다른지 그정도만 알려고 했으나, 끌리듯 매크로의 내용까지 읽게 되었다.
매크로의 내용은 꽤나 흥미로웠다.
많은 부분 배울 점이 많았다. 영어의 장벽으로인해 읽는 속도가 느려짐에 따라 내 머리가 터지는 느낌을 받았다.

그러다가 커먼리습의 매크로부분을 한 반정도 읽고선 더 이상은 보류를 해야 겠다고 생각했다.
정리를 할 시간이 필요하다. 다른 책을 보면서 쉬고, 다시 읽던가 해야할 것 같다.

커먼리습은 clojure랑 궁극적으로 다른 것이 있는데 그것이 mutability이다. 커먼리습은 값을 마음대로 바꿀 수 있다. clojure에서는 그것이 불가능하다.
커먼리습은 그렇기 때문에 매크로에서 더욱더 많은 일을 할 수 있으며, 더욱더 많은 문제를 일으킨다.
그 많은 내용들을 읽다가, 나는 대충 훑어 넘길 수 밖에 없었는데, 일단 나에게는 너무 먼 이야기처럼 느껴졌다.
피부에 느껴질때 읽어도 늦지 않겠다고 생각했다.

그리고 함수형을 기반으로 프로그래밍을 한다면 해당 문제는 많이 사라질 것이라고 생각했다.
할 수 있는 일이 줄어든다고, 못하는 것은 아니니까.
화공은 화선지 안에서 자유를 느낀다고 했던가.
조그만한 네모 종이 위에서 큰 자유를 느낀다고 한다. 그 자유는 아마 자신을 그 하얀색 안으로 단절시키고, 바깥 세상에 더럽혀지지 않은 순수한 무언가를 만들 수 있는 것 때문 아닐까 싶다.

다음 책은 Land Of Lisp를 읽어야 겠다.
그리고 한 번 Let Over Lambda라는 책을 읽어볼까 한다.
얼마나 어려운 책인지.

얼마나 무서운 책인지 알고 싶다.
더 나아가 매크로가 어디까지 갈 수 있는지 알고 싶다.

멋진 책이다. lisp의 개론서로는 딱이다. 매크로의 내용은 인내심을 가지고 보면 멋진 내용이 기다릴 것이다.
하지만 이 책을 읽는 것은 시간이 필요한 일이다.
느긋하게 보았으면 좋겠다.

나도 좀 더 느긋하게 볼 생각이다.

2019년 9월 1일 일요일

[on lisp] 11 Classic Macros


11.1 Creating Context
Context here has two senses. One sort of context is a lexical environment.
The let special form creates a new lexical environment; the expressions in the body of a let will be evaluated in an environment which may contain new variables.

If x is set to a at the toplevel, then
(let ((x 'b)) (list x))
with nonetheless return (b), because the call to list will be made in an environment containing a new x, whose value is b.

; Fig 11.1 : Macro implementations of let.
; let 만들어보자.
; 1 binds는 바인딩 되는 리스트 body는 이제 표현식
; 2 mapcar로 binds에서 (car x)로 키 값만 가져온다.
; 2.1 cons가 아니면 x로 
; 2.2 그리고 그것은 매개변수에 들어간다. (lambda 매개변수 ..)
; 2.3 body를 ,@body로 리스트를 벗긴다.

; 3 이제 람다 안에 binds 안에 값을 넣는다.
; 3.1 mapcar로 binds에서 (cdr x)로 밸류만 가져온다.
; 3.2 cons가 아니면 nil로 바인딩
(defmacro our-let (binds &body body)
  `((lambda ,(mapcar #'(lambda (x)
                         (if (consp x) (car x) x))
      binds)
    ,@body)
  ,@(mapcar #'(lambda (x)
                 (if (consp x) (cadr x) nil))
      binds)))
보면 알겟지만, let은 매크로 안에서 lambda에서 만든다.
(our-let ((x 1) (y 2))
  (+ x y))
;; 아래로 확장
((lambda (x y) (+ x y)) 1 2)

그림 11.2는 lexical 환경을 만들어서 바인딩하는 3개의 새로운 매크로를 제공한다.
; Fig 11.2 : Macros which bind variables.
(defmacro when-bind ((var expr) &body body)
  `(let ((,var ,expr))
     (when ,var
    ,@body)))

(defmacro when-bind* (binds &body body)
  (if (null binds)
      `(progn ,@body)
   `(let (,(car binds))
      (if ,(caar binds)
       (when-bind* ,(cdr binds) ,@body)))))

(defmacro with-gensyms (syms &body body)
  `(let ,(mapcar #'(lambda (s)
                     `(,s (gensym)))
     syms)
  ,@body)) 
when-bind는 섹션 7.5에서 리스트 파라미터 구조분해로 쓰임. 그러므로 이 매크로는 94페이지에서 이미 설명이 됨
(when-bind (input (get-user-input))
  (process input))
;; expand
(let ((input (get-user-input)))
  (when input
    (process input)))
더 나아가서 when-bind*는 symbol expression(바인딩 될 심볼과 함수)쌍의 리스트를 받는다. -- let의 첫번째 매개변수와 같다.
If any expression returns nil, the whole when-bind* expression returns nil.
이중에 하나라도 nil을 뱉으면 when-bind* 모두 nil임. nil 아니면 body가 평가되게 된다. 그리고 각 symbol들은 let*으로 묶인다.
(when-bind* ((x (find-if #'consp '(a (1 2) b)))
             (y (find-if #'oddp x)))
  (+ y 10))
11
마지막으로 with-gensyms 매크로다. 매크로를 작성할 때 쓰이는 녀석.
많은 매크로들이 gensyms를 생성하는 것으로 시작한다.
종종 이게 많을 때가 있다. page 115에 with-redraw는 5개나 gensym로 만든다
(defmacro with-redraw ((var objs &body body)
  (let ((gob (gensym))
        (x0 (gensym)) (y0 (gensym))
        (x1 (gensym)) (y1 (gensym)))
  ...))
;; 이제 이렇게
(defmacro with-redraw ((var objs) &body body)
  (with-gensyms (gob x0 y0 x1 y1)
    ...))

만약 변수들을 바인딩하려할 때, 조건절에 따라서 다르게 평가를 한다면?
let안에 조건절을 사용하자.

하지만 반대로는? 조건에 따라서 바인딩이 달라져야 한다면
(defmacro condlet (clauses &body body)
  (let ((bodfn (gensym))
        (vars (mapcar #'(lambda (v) (cons v (gensym)))
                      (remove-duplicates
                        (mapcar #'car
                                (mappend #'cdr caluses))))))
    '(labels ((,bodfn ,(mapcar #'car vars)
                 ,@body))
       (cond ,@(mapcar #'(lambda (cl) 
                           (condlet-clause vars cl bodfn))
                       clauses)))))

(defun condlet-clause (vars cl bodfn)
  `(,(car cl) (let ,(mapcar #'cdr vars)
                (let ,(condlet-binds vars cl)
                  (,bodfn ,@(mapcar #'cdr vars))))))

(defun condlet-binds (vars cl)
  (mapcar #'(lambda (bindform)
              (if (consp bindform)
                  (cons (cdr (assoc (car bindform) vars))
                        (cdr bindform))))
          (cdr cl)))
그림 11.3이 그런경우를 보여주는 것이다.
(condlet (((= 1 2) (x (princ 'a)) (y (princ 'b)))
          ((= 1 1) (y (princ 'c)) (x (princ 'd)))
          (t       (x (princ 'e)) (z (princ 'f))))
  (list x y z))
CD
(D C NIL)

11.2 The with- Macro
with-형태의 매크로를 만들어서 컨텍스트를 생성한다.
(with-open-file (s "dump" :direction :output)
  (princ 99 s))
이러면 저절로 "dump"파일을 닫히고 99가 써있게 될 것이다.
; pure macro
(defmacro with-db (db &body body)
  (let ((temp (gensym))
    `(let ((,temp *db*))
       (unwind-protect
         (progn
           (setq *db* ,db)
           (lock *db)
           ,@body)
         (progn
           (release *db*)
           (setq *db* ,temp))))))
; with function
(defmacro with-db (db &body body)
  (let ((gbod (gensym)))
    `(let ((,gbod #'(lambda () ,@body)))
       (declare (dynamic-extent ,gbod))
       (with-db-fn *db* ,db ,good))))

(defun with-db-fn (old-db new-db body)
  (unwind-protect
    (progn
      (setq *db* new-db)
      (lock *db*)
      (funcall body))
    (progn
      (release *db*)
      (setq *db* old-db))))
다른 매크로도 보자.
; Fig 11.5: Macros for conditional evaluation.
(defmacro if3 (test t-case nil-case ?-case)
  `(case ,test
     ((nil) ,nil-case)
     (?     ,?-case)
     (t     ,t-case)))

(defmacro nif (expr pos zero neg)
  (let ((g (gensym)))
    `(let ((,g ,expr))
       (cond ((plusp ,g) ,pos)
             ((zerop ,g) ,zero)
             (t ,neg)))))

11.3 Conditional Evaluation 조건평가
;; fig 11.6 : Macros for conditional evaluation.
(defmacro in (obj &rest choices)
  (let ((insym (gensym)))
    `(let ((,insyn ,obj))
       (or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
                     choices)))))

(defmacro inq (obj &rest args)
  `(in ,obj ,@(mapcar #'(lambda (a) '' ,a)
                      args)))

(defmacro in-if (fn &rest choices)
  (let ((fnsym (gensym)))
    `(let ((,fnsym ,fn))
       (or ,@(mapcar #'(lambda (c)
                         `(funcall ,fnsym ,c))
                     choices)))))

(defmacro >case (expr &rest clauses)
  (let ((g (gensy)))
    `(let ((,g ,expr))
       (cond ,@(mapcar #'(lambda (cl) (>casex g cl))
                       clauses)))))

(defun >casex (g cl)
  (let ((key (car cl)) (rest (cdr cl)))
     (cond ((consp key) `((in ,g ,@key) ,@rest))
           ((inq key t otherwise) `(t ,@rest))
           (t (error "bad >case clause")))))

11.4 Iteration 반복
; 11.7 : Simple iteration macros.
(defmacro while (test &body body)
  `(do ()
       ((not ,test))
      ,@body))

(defmacro till (test &body body)
  `(do ()
       (,test)
     ,@body))

(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ ,var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))

[on lisp] 9. 매크로 캡처링문제

; 9.1 variable capture

;wrong
(defmacro for ((var start stop) &body body)
    `(do ((,var ,start (1+ ,var))
          (limit ,stop))
         ((> ,var limit))
         ,@body))

; seems work fine
(for (x 1 5)
     (princ x))
;error 이렇게 limit을 넣으면 문제가 된다.
;(for (limit 1 5)
;     (princ limit))
;why
; (> limit limit) 이름 충돌 여기도 변수충돌이 일어난다.
;(do ((limit 1 (1+ limit))
;     (limit 5))
;    ((> limit limit))
;    (princ limit))

; 9.2 Free Symbol Capture
; 덜 빈번하게, 매크로 정의 자체에서 매크로가 확장 될 때 environment에 실수로 바인딩을 하는 기호를 포함할 수도 있다. (환경변수가 어쩌다 걸려버린것)
; 어떤 프로그램을 소개한다, warning을 출력하는 대신, 나중에 확인하려고 리스트에 add하기를 원한다고 해보자.
; 한 개발자가 매크로 gripe를 작성한다. 경고를 글로벌 리스트(w)에 넣는 다.
(defvar w nil)

(defmacro gripe (warning)
    `(progn (setq w (nconc w (list ,warning)))
         nil))

; 그런 다음 다른 사람이 sample-ratio라는 함수를 만들고 그 안에 gripe를 넣었다.
; 이 함수는 매개변수 v,w의 길이의 비율을 확인한다. 거기서 비율이 2보다 작으면 해당 내용을 로그리스트(w)에 추가하고
; nil을 리턴한다. 아래 코드를 보다.

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
    (if (or (< vn 2) (< wn 2))
        (gripe "sample < 2")
        (/ vn wn))))
; [sample-ratio]가 w=(b)로 호출된다면, w의 매개변수는 2개 이하 이기 때문에 워닝을 할 것이다.
; 하지만 gripe이 확장되는 순간, sample-ratio 안에 정의된 것처럼 보이게 된다.

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
     (if (or (< vn 2) (< wn 2))
         (progn (setq w (nconc w (list "sample < 2")))
                 nil)
         (/ vn wn))))
; 문제는 gripe매크로가 sample-ratio안으로 들어가면서 w가 글로벌 변수 w를 쓰는 것이 아니라.
; sample-ratio안에 있는 로컬변수에 바인딩 되는 것이다. 원래는 free variable이 되서 글로벌로 범위가 커지는 것인데
; 이제는 free-variable이 아니라 로컬변수처럼 된 것이다.
; warning은 이제 글로벌 리스트에 저장되는 것이 아니라, 로컬변수에 들어간다.
; 이제는 warning출력값만 잃은 것이 아니라. 매개변수로 들어온 W=(b)값 마저 잃게 된다.
(print (let ((lst '(b)))
  (sample-ratio nil lst)
   lst))
; (B "sample < 2") lst가 이렇게 바뀐 것이다.
(print w)
; nil : w에는 아무것도 들어가지 않았음


; 9.3 When Capture Occurs (언제 캡처가 일어나나)
; 변수 캡처는 미묘한 문제이며 캡처 가능한 기호가 프로그램에서 장난을 일으킬 수있는 모든 방법을 예상하려면 약간의 경험이 필요하다.
; 다행스럽게도, 캡처로 인해 프로그램이 잘못 될 수 있는 방법에 대해 생각 할 필요없이 매크로 정의에서 캡처 가능한 기호를 감지하고 제거 할 수 있다.
; 캡처 가능한 변수를 정의하는 규칙은 먼저 정의해야하는 일부 하위 개념에 따라 다릅니다.

; Free : 기호 [s]는 해당 표현식에서 변수로 사용될 때 표현식에서 사용 가능하지만 표현식은 이에 대한 바인딩을 작성하지 않습니다.
; s가 변수로 들어오면 사용하지만, 표현식 안에서 바인딩을 생성 하지는 말라는 말인가. 아래를 보자
; (let ((x y) (z 10))
;   (list w z x))
; w,x,z모두 list표현식에서는 free variable이다, 바인딩이 없는 것이다.
; 하지만 let으로 둘러싸면 x,z에 대한 바인딩을 적용한다. 좋아 let 안 전체를 보면 y,w는 free variable이 되는 것이다.
; 또 알아야 하는 것은
; (let ((x x))
;   x)
; 여기서 (x x)가 있는데 두번째 x만 free다 이 녀석은 x를 위해 바인딩 된 새로운 스코프에서 온 녀석이 아니다.

; Skeleton : 매크로 확장의 골격은 전체 표현식이며, 매크로 호출에서 매개변수 부분을 뺀 것이다.
(defmacro foo (x y)
  `(/ (+ ,x 1) ,y))
; 그리고 호출해보자
(print
  (macroexpand-1
    '(foo (- 5 2) 6)))
; (/ (+ (- 5 2) 1) 6) 
; 여기서 스켈레톤은 확장된 전체 표현식 중에서 파라미터 x y를 뺀 녀석이다.
; (/ (+         1)  )
; 이 두 가지 개념을 정의하면, 캡쳐 가능한 기호를 탐지하기 위한 간결한 규칙을 진술하는 것이 가능하다.

; Capturable : 심볼은 몇몇 매크로 확장에서 캡처될 수 있다.
; (a) free variable이 매크로 확장의 스켈레톤 안에 있던가
; (b) 매개변수로 들어온 변수가 (바인딩하거나 평가하는) 스켈레톤의 부분에 바인딩 되는 경우 (덮어씌어지는 경우)
(defmacro cap1 ()
  '(+ x 1))
; 여기서 x는 capturable이다. 왜냐하면 free variable이 스켈레톤 안에 있다. gripe에서도 본 버그이다.
(defmacro cap2 (var)
  `(let ((x ...)
         (,var ...))
     ...))
; 여기서 x는 capturable이다. x가 매개변수로 들어오는 x도 바인딩 해버린다. (9.1 for에서 확인했다.)
; 반면에 아래 두 매크로를 보자.
(defmacro cap3 (var)
  `(let ((x ...))
     (let ((,var ...))
       ...)))

(defmacro cap4 (var)
  `(let ((,var ...))
     (let ((x ...))
       ...)))
; 모두 x가 capturable이다. 하지만, x가 바인딩 된 곳 안에 문맥(context)가 없고, 매개변수로 들어온 녀석이 없다면, cap3/cap4 모두 사용할 수 있다.
(defmacro safe1 (var)
  `(progn (let ((x 1))
            (print x))
          (let ((,var 1))
            (print ,var))))
; 여기서 x는 capturable이 아니다. 
; 잘보면! let를 쓰고 그 안에서 x만 쓴다. 매개변수 var를 쓰고 있지 않다. 게다가 환경변수 같이 밖에 있는 문맥(context)를 끌어들이지 않았다.
; 이처럼 모든 스켈레톤 안에 있는 bound variables가 위험하지는 않다.
; 하지만 만약 매개변수가 스켈레톤에 의해 설정된 바인딩 내에서 평가되는 경우는,
(defmacro cap5 (&body body)
  `(let ((x ...)
     ,@body))
; 그러면 바인딩된 변수는 캡처링 될 위험이 있다. cap5에서 x는 캡처위험이 있다.
(defmacro safe2 (expr)
  `(let ((x ,expr))
     (cons x 1)))
; x는 캡처위험이 없다, 왜냐하면 expr에 전달된 매개변수를 평가할 때, 새로 바인딩 된 x는 보이지 않기 때문이다.
; 또한 우리가 걱정해야 하는 것은 the binding of skeletal variables(스켈레톤 변수의 바인딩)이다. 아래 매크로를 보자.
(defmacro safe3 (var &body body)
  `(let ((,var ...))
     ,@body))
; 어떤 심볼도 의도치 않게 갭처될 위험이 없음.
; 이제 캡처 가능한 기호를 식별하는 새로운 규칙에 비추어 'for'의 원래 정의를 살펴보자.
(defmacro for ((var start stop) &body body) ;; wrong
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; 이제 위에 정의는 두 가지 방식으로 취약하는 것을 알게 되었다.
; 1. limit 이 for의 첫번째 매개변수로 들어올 수 있다.
(for (limit 1 5)
  (print c limit))
; 2. 그런데 limit이 loop의 body에 들어가는 것도 위험하다.
(let ((limit 0))
  (for (x 1 10)
    (incf limit x))
  limit)
; 여기서 for문은 끝나지 않을 것이다.
; 이 섹션에서 보여준 룰들로 전부 잡을 수 있는 것은 아니다. 
; capture문제는 보는 것에 따라 모호하게 정의된다. 예를 즐다
(let ((x 1)) (list x))
; 우리는 (list x)를 평가할 때 그게 오류라고 하지는 않는다. x가 새로운 변수에 들어간다고 문제라고 생각하지는 않을 것이다.
; 캡처링 문제를 알아내는 룰도 부정확하다.
; 이런 캡처링 룰을 패스하는 매크로를 만들 수 있지만, 그럼에도 의도치 않은 캡처링 문제를 직면할 수 있다.
; 아래 예를 보자.
(defmacro pathological (&body body)
  (let* ((syms (remove-if (complement #'symbolp)
                          (flatten body)))
         (var (nth (random (length syms))
                   syms)))
    `(let ((,var 99))
        ,@body)))
; 매크로가 호출 될 때, body 안에 표현식은 progn처럼 평가될 것이다.
; but one random variable within the body may have a different value.
; 하지만 body안에 있는 random 변수는 다른 값을 가질 것이ㅏ.

; 9.4 Avoiding Capture with Better Names (더 나은 이름으로 캡처링을 피한다?)
; 첫번째 두 섹션에서 캡처링을 두 가지 타입으로 나눴다.
; argument capture, 매크로 스켈레톤에 매개변수가 들어가는 거
; free symbol capture, free symbol이 매크로 확장 안에 있어서 예기치 못한 곳에 캡처링
; 이름을 *warning* 이렇게 글로벌로 넣으면서 캡처링이 되지 않도록 하자는 것 같다.
; 좋은 생각 같지는 않다.

; Avaoiding Capture by Prior Evaluation
; 때로 매개변수 캡처링은 매크로확장에서 만들어진 바인딩 밖에서 위험한 매개변수를 확장하면서 간단히 해결된다.
; Fig 9.1 Avoiding capture with let.
;; vulnerable to capture
(defmacro before (x y seq)
  `(let ((seq ,seq))
     (< (position ,x seq)
        (position ,y seq))))
; currect version
(defmacro before (x y seq)
  `(let ((xval ,x) (yval ,y) (seq ,seq))
     (< (position xval seq)
        (position yval seq))))

; 9.2 Avoiding capture with a closure.
; vulnerable to capture
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; correct version
(defmacro for ((var start stop) &body body)
  `(do ((b #'(lambda (,var) ,@body))
        (count ,start (1+ count))
        (limit ,stop))
       ((> count limit))
     (funcall b count)))
; 이렇게 사용할 녀석들을 전부 let으로 다시 바인딩해서 사용하는 걸 원하나보다.


; 9.6 Avoiding Capture with Gensyms * (gensyms이용하기)
; gensyms는 같은 값이 없는 것을 약속하는데
; 이 건 각 패키지에서 모든 심볼의 이름을 추적한다.
; 이 심볼들은 패키지 안에 interned된다고 말한다.
; gensym을 호출하면 uninterned unique한 심볼을 리턴한다.
; 그러므로 
(eq (gensym) ...)
; 이렇게 테스트를 해보려고 하면 절대 일어날 수 없는 일이 될 것이다.
(gensym) ; #G:47 뭐 이런식으로 보여준다. 이름을 딱히 중요하지 않다. 

;9.3 Avoiding capture with gensym.
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; A correct version
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))
; 그림 9.3을 보면 gensyms를 이용한 정의가 있다.
; 이제 limit 혹은 stop 뭐 이런걸로 심볼이 충돌나지는 않을 것이다.

; 9.7 Avoiding Capture with Packages. (패키지로 캡처링 피하기)
; 어느정도는 매크로를 패키지 않에 넣는 것으로 캡처링을 피하는 것이 가능하다.
; 만약 매크로들의 패캐지를 따로 만들어서(macros라고 하자) 그 안에 for매크로를 만든다면 초반에 만든 매크로 마저 쓸 수 있게 된다.
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
       ((> ,var limit))
     ,@body))
; 같은 패키지는 아니고 다른 패키지에서 사용한다고 해보자.
; mycode라는 패키지에서 macros패키지를 사용한다고 해보자.
; limit이라는 이름으로 첫번째 매개변수를 사용한다 하더라도!
; mycode:limit이 되고 macros::limit이 되어 충돌이 되지 않을 것이다.
; 문제는 뭐냐 1. 패키지는 이런식으로 쓰라고 만들어 진 것이 아니다.
; 2. 같은 패티지에서는 문제가 생긴다.

; 9.8 Capture in Other Name-Spaces (다른 네임스페이스에서의 캡처)
; 지금까지는 variable capture이지만, 커먼리습에서는 name-space가 문제될 수 있다.
; 함수도 또한 로컬에서 바인딩 될 수 있으며, 함수 바인딩은 변수바인딩과 같이 문제를 일으키기 쉽다.
(defun fn (x) (+ x 1))
(defmacro mac (x) `(fn ,x))
(mac 10) ;; 11
(labels ((fn (y) (- y 1)))
  (mac 10) ;; 9
; 위치에 따라 fn이 다르게 캡처링 되면서 리턴값이 달라진다.

2019년 8월 31일 토요일

[on lisp] 8. When to Use Macro

최근에 갑상선 암으로 수술을 하였다. 술도 잘 안먹고, 담배도 피지 않는 내가 왜 암이라는 것에 걸리는 걸까. 라는 생각을 하게 된다.
아직도 목 부근이 아프다. 힘도 안난다. 운동을 강하게 할 수 는 없을 것 같다. 평생 약을 먹어야 하는 것도 걱정이다. 아니 좀 짜증이라고 해야 하나...
하지만, 뭐 어쩔 수 없다고 생각하게 되었다. 내가 인자약이라 생각하고, 음식을 하나하나 제한하기로 했다.

그리고...
오랜만에 on lisp를 다시 공부하고 있다.
커먼리습을 잘 모르기에 읽는데 꽤나 힘들다. 잘 몰라도 내 시야를 넓혀주는 느낌을 준다.

이런 공부 정말 시간이 오래걸리고 내가 영어능력이 후달려서 제대로 읽고 있는지도 잘 모르지만,
꽤나 재미있다.

이제 겨우 8장을 넘겼는데, 언제 마지막까지 갈까 한숨이 나온다.
끈질기게 계속 나아가야 겠다.

; 8 When to Use Macro
; 8.1 When Nothing Else Will Do
; 일반적으로 비슷한 코드를 프로그램에서 찾는 다면
; 서브루틴을 만들고 비슷한 일련의 코드들을 없애고 서브루틴을 호출하는 것으로 바꿀 것이다.
; 이 원칙을 리스프 프로그램에 적용하면, 
; 우리는 이 "서브루틴"이 함수여야 할지 매크로여야 할 지 고민해야 한다.

; 어떤 경우에는 매크로밖에 할 수 없어서, 매크로로 결정해야 하는 경우가 있다.
; 1+ 함수 같은 경우 함수/매크로 둘다 만들 수 있다.
(defun myfun-1+ (x) (+ 1 x))
(defmacro mymacro-1 (x) `(+ 1 ,x))

; 하지만 while같ㅇ느 경우 매크로밖에 못한다.
(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))
; while은 body 표현식을 쪼개서(splice) do의 body부분에 넣는다.
; 게다가 평가는 test변수가 오로지 nil일 경우다.
; 함수는 이런거 못한다 함수는 호출되기 전에 매개변수가 모두 평가 호출된다.
; 이걸 알자 매크로는 함수가 못하는 4가지를 할 수 있다.
; 1. Transformation(변형)
; 커먼리습에서 setf 매크로는 평가 전에 인수를 구분(평가전에 매개변수를 다룬다)하는 매크로 클래스 중 하나다.
; 내장 액세스 함수는 종종 그 반대(역)를 가진다. 이 녀석의 목적은 내장 엑세스 함수가 조회하는 것을 설정한다(set 반대로 한다는 말)
; 예를 들어 car는 첫번째를 가져오는데 replaca는 첫번째를 설정한다.
; setf 를 함께 이용하면 이런 access function(car, cdr)를 이용하여 변수를 set할 수 있다.
; (setf (car x) 'a) 이렇게 말이다. 이 값은 (progn (rplaca x 'a) 'a)로 확장될 수 있다.
; 이 트릭을 수행하려면 setf는 첫 번째 인수를 살펴 봐야한다. 그래야 뭘로 바꿀지 알 수 있다.
; 위의 경우 replaca가 필요하다는 것을 알기 위해 setf는 첫 번째 인수가 car로 시작하는 표현식임을 확인할 수 있어야함.
; 따라서 setf 및 인수를 변환하는 다른 연산자는 매크로로 작성해야합니다.
; 그러니까 매개변수에 들어오는 표현식이 평가되지 전에 확인해야 하는 것이 있는 것이다.
; 나의 예측으로는 setf의 첫번째 매개변수의 표현식이 car이라면 replaca로 cdr이면 rplacd로 변환(transformation)할 것으로 보인다.
; 그러므로 이럴 때는 함수를 쓸 수 없다. 왜냐하면 매개변수로 들어가면 이미 평가가 되고 나서 함수에 들어오기 때문에다.
; 표현식을 볼 수 없다.

; 2. Binding(바인딩, 변수바인딩인듯)
; Lexical variables must appear directly in the source code.
; 렉시컬 변수는 소스 코드에 직접 나타나야합니다.
; 예를 들어 setq에 대한 첫 번째 인수는 평가되지 않으므로 setq에 빌드 된 것은 호출하는 함수가 아니라 setq로 확장되는 매크로 여야합니다.
; 비슷한 연산자 let을 보자, 이녀석에게 오는 매개변수들은 lambda 표현식의 매개변수로 바뀐다.(매크로를 풀면)
; do 매크로는 letdmfh ghkrwkdehlsek. 
; 어떤 연산자든 매개변수의 lexical binding을 손보려면 매크로여야 한다.
; 이름이 평가되기 전에 lambda로 감싼 후 매개변수로 보내서 lexcial binding을 만들어야 한다.

; 3. Conditional evaluation.(조건에 따른 평가)
; 함수의 경우 모든 매개변수는 평가된다.
; when을 예로 들면 특정 조건에 맞을 때만 body가 평가된다.
; 이런 유연성은 매크로에서만 가능하다.

; 4. Multiple evaluation.(여러번 평가, for문 처럼)
; 함수에는 매개변수는 모두 평가될 뿐만 아니라 정확히 한.번. 만 평가된다.
; do 매크로 처럼 여러번 실행되게 하려면 매크로가 필요하다.

; 다음은 inline expansion of macros의 장점이다.
; 이제 알려줄 세 가지 장점 중 두 가지는 lexical context를 다루는 점 때문에 나타난다.
; 5. Using the calling environment. 
; 매크로를 매크로를 호출하는 컨텍스트에 바인딩된 변수를 포함한 확장 코드를 생성할 수 있다.
(defmacro foo (x)
  `(+ ,x y))
; depends on the binding of y where foo is called.
; 이건 기본적으로 나쁜 스타일이다.
; 하지만, 환경값을 가져오기 위해 아주 드물게 필요한 기술이다.

; 6. Wrapping a new environment.
; A macro can also cause its arguments to be evaluated in a new lexical environment.
; 매크로는 또한 새로운 lexical environment에서 인수가 평가되도록 할 수 있습니다.
; let을 말하는 것! 페이지 144에 let 매크로 구현예제가 있다.
; (let ((y 2)) (+ x y)), 에서 y는 새로운 변수를 가리킨다.

; 7. Saving function calls. 
; 매크로 확장의 인라인 삽입의 세 번째 결과는 컴파일 된 코드에서 매크로 호출과 관련된 오버 헤드가 없다는 것입니다.
; 런타임에서, 매크로 호출은 그 확장코드로 치환된 상태를 실행하는 것이다.
; (함수를 인라인으로 만드는 것과 비슷한 원리)

; 5,6번은 의도되지 않았을 경우(제대로 알지 못한 경우) , variable capture문제를 겪게 된다.
; 이 문제는 macro를 가장 위협하는 존재다. 이 문제는 챕터9에서 심도있게 다룰 것.
; Instead of seven ways of using macros, it might be better to say that there are six and a half.
; In an ideal world, all Common Lisp compilers would obey inline declarations,
; and saving function calls would be a task for inline functions, not macros.
; An ideal world is left as an exercise to the reader.

; 8.2 Macro or Function?
; 8.1의 내용은 쉬웠다. 대부분 연산자는 매개변수가 바로 평가되면 안된다. 매크로만 할 수 있는 건 매크로로 만들면 되는 것이었다.
; 둘다 만들어질 수 있는 경우, 우린 어떻게 선택해야 하는가? 어떤 장단점이 있는지 제대로 알아야 한다.
(defun avg (&rest args)
  (/ (apply #'+ args) (length args)))
(defmacro avg (&rest args)
  `(/ (+ ,@args) ,(length args)))
; 여기서 함수버전은 각 avg호출에 length가 불필요하게 실행됨.
; 컴파일 타임에 우리는 인수의 값을 알지 못할 수도 있지만 얼마나 많은지 알기 때문에 'length'에 대한 호출도 가능합니다.
; 뭔 말이야, 몇개인지 어떻게 안다는 거지...
; 뭐 여튼 컴파일 타임에 인수의 갯수를 알 수있으니 length호출을 매번 할 필요는 없다는 말인가?


; 여기서 함수를 쓸지 매크로를 쓸지 선택을 도와줄 요소들이 있다.
; 1. Computation at compile-time(컴파일타임에 계산이 된다.)
; 매크로 호출에서 계산은 두번 일어난다 : 매크로가 확장 될 때와 확장이 평가 될 때.
; 리습에서 모든 매크로 확장은 프로그램이 컴파일 될 때 일어나며, 모든 
; 컴파일 타임에 계산이 완료된 비트들 중 1비트도 프로그램이 실행될 때 속도를 늦추지 않을 것이다.
; If an operator could be written to do some of its work in the macroexpansion stage, it will be more efficient to make it a macro
; 만약 한 연산자가 매크로 확장 단계에서 일분 작업을 미리 수행할 수 있게 할 수 있다면, 매크로로 만들어 더 효율적이게 만들 수 있는것이다.
; 왜냐하면 어떤 아무리 똑똑한 컴파일러가 와도 스스로 할 수 없다. 함수는 런타임에 수행되어야 하기 때문이다.
; Chapter 13에서 avg 매크로가 어떻게 확장하는지 보여준다.

; 2. Integration with Lisp(Lisp와의 통합)
; 때때로 함수 대신 매크로를 사용하면 프로그램이 Lisp와보다 밀접하게 통합 될 수 있다.
; 특정 문제를 해결하기 위해 프로그램을 작성하는 대신! 매크로를 사용하여 Lisp가 이미 해결 방법을 알고있는 문제로 변환 할 수 있다.
; 이 방법은, 가능하면, 일반적으로 프로그램을 더 작고 효율적으로 만든다.
; 더 작아지고, 리스프가 우리 일부 작업을 수행해주기 때문에, 더 효율적으로, 프로덕션 리스프 시스템은 일반적으로 유저 프로그램보다 fat sweated(땀투성이가 된다?)하기때문에
; (아마 리스프 시스템이 유저가 만드는 프로그램보다 더 커지기 때문에? 더 일을 많이 하기 때문에, 유저는 프로그램을 만들 때 효율적으로 만든다는 건가)
; 이런 장점은 대부분 embedded languages에서 나타난다. 19챕터에서 보여줄 것임.

; 3. Saving function calls.(함수호출을 줄인다)
; 매크로 호출은 호출이 되는 그곳에 바로 확장된 내용이 코드 안으로 들어간다(치환된다).
; 따라서 자주 사용하는 코드를 매크로로 작성하면 사용될 때마다 함수 호출을 절약할 수 있다.
; Lisp의 초기 방언에서 리습 프로그래머는 이 매크로 속성을 활용하여 런타임에 함수 호출을 절약했다.
; 일반적인 Lisp에서 이 작업은 인라인으로 선언 된 함수에 의해 수행됩니다. (inline으로 함수를 만든다는 듯, 이름이 없음)
; 함수를 [인라인]으로 선언하면 매크로와 마찬가지로 호출 코드로 바로 컴파일되도록 요청합니다.
; 그러나 여기에는 이론과 실제 사이에 차이가 있다.
; CLTL2 (p.229)는 "컴파일러는 이 선언을 무시해도됩니다"라고 말하고 일부 Common Lisp 컴파일러는 그렇게합니다.
; 이러한 컴파일러를 사용해야하는 경우 매크로를 사용하여 함수 호출을 절약하는 것이 여전히 합리적 일 수 있습니다.(인라인을 무시하는 컴파일러)

; 어떤 경우에는 효율성과 Lisp와의 통합이 매크로 사용에 대한 강력한 논거가 될 수 있다.
; 19 장의 쿼리 컴파일러에서 보면, 런타임에 계산되어야할 녀석들이 컴파일 타임에서 계산이 되니까(또 그 양이 많아서), 
; 전체 프로그램을 하나의 거대한 매크로로 전환하는 것을 정당화 할 수 있다.
; 이렇게 전체가 거대한 매크로가 되는 전환은 속도를 빠르게 하기도 하지만 프로그램을 리스프에 더 가깝게 하기도 한다. (domain specific language가 되는듯)
; 이렇게 바뀐 후에는 쿼리 내에서 Lisp 표현식 (예 : 산술 표현식)을 사용하는 것이 더 쉽다.

; THE CONS 단점
; 4. Functions are data, while macros are more like instructions to the compiler.
; 함수는 데이터지만 매크로는 컴파일러에게 보내는 명령어들과 비슷하다.
; 함수는 리턴하거나 매개변수를 받거나 자료구조 안에 저장될 수 있지만, 매크로는 아무것도 못한다.
; 람다표현식 안에다가 매크로를 넣어서 이런 것들을 할 수 있도록 트릭을 쓸 수도 있겠다.
; 예를들어 apply,funcall같은 것을 매크로에 적용해야 한다면
(funcall #'(lambda (x y) (avg x y)) 1 3)
2
; 하지만 편하지는 않다. 그리고 이게 항상 되는 것은 아니다. 
; avg같이 단순한 매크로라 하더라도 말이다. 매크로는 &rest 매개변수를 가진다.
; 이 안에 다양한 인수를 전달할 방법이 없습니다. 그렇다 람다에서 받는다하더라도 그걸 풀어서 넣는 방법이 있을까.
; 그 매개변수 자체를 데이터로 조작해야 하는데 그건 매크로에서 할 수 있을 것이다.

; 5. Clarity of source code (소스코드의 명확성, 코드가 더러워짐)
; 매크로는 함수로 만드는 것보다 읽기가 힘들다. 그러므로 매크로를 쓴다는 것은 눈에 띌만큼 낫다는 것을 알고 있을 때다.

; 6. Clarity at runtime. (런타임에서의 명확성, 런타임에서 디버깅이 힘들다)
; 매크로는 종종(항상) 함수보다 디버깅이 힘들다.
; 매크로는 확장시 사라지므로 런타임에서는 보이는 녀석으로는 무엇을 하는 녀석인지 제대로 설명되지 못한다.
; 전혀 작동하지 않으면 trace는 매크로 호출 자체가 아니라 매크로의 확장 기능 호출을 보여줍니다.

; 7. Recursion (재귀가 문제)
; 매크로에서 재귀를 사용하는 것은 함수랑 다르다. (힘들다)
; 매크로에서 확장된 함수은 재귀적일 수 있지만 확장하는 것 자채는 쉽지 않다.
; 섹션 10.4는 매크로의 재귀를 다룰 것이다.

; 매크로 사용 결정은 위 고려사항들을 균형있게 다뤄야 한다.
; 오로지 경험만이 무엇이 우선으로 되는지 알게 된다.
; 그러나 다음 장에 나오는 매크로의 예는 매크로가 유용한 대부분의 상황을 다룬다.
; 미래에 만들어진 매크로가 아래 주어진 매크로와 유사하다면, 작성하는 것이 안전 할 것이다.
; 마지막으로 런타임시의 명확성(단점 6번)은 거의 문제가 되지 않다.
; 많은 매크로를 사용하는 코드의 디버깅은 예상만큼 어렵지 않다.
; 매크로 정의의 길이가 수백 줄인 경우 런타임에 확장을 디버그하는 것이 불쾌 할 수 있습니다.
; 그러나 유틸리티는 최소한 작고 신뢰할 수있는 계층으로 작성되는 경향이 있다. 
; 일반적으로 그 정의는 15줄 미만이다. 이런 매크로에서는 보는게 어렵진 않을 거다.

; 8.3 Applications for Macros (매크로 응용)
; 어떤 종류의 응용 프로그램에서 사용할 수 있을까?
; 가장 가까운 것(잘 맞는 것)은 syntactic transformation이다.
; 19-24 장의 모든 syntactic transformation(구문변환)으로 설명할 수 있는 전체 프로그램을 제시한다.
; 이런 것들은 실제로 모두 매크로다.
; 매크로앱은 작은 형태의 범용 매크로(while 매크로)와 큰 특정목적에 맞는 매크로 사이의 연속이다.
; 한쪽 끝에는 [유틸리티]가 있으며, 모든 Lisp에 내장 된 것과 유사하다.
; 그것들은 일반적으로 작고 일반적이며 독립적으로 작성된다.
; 그러나 특정 클래스의 프로그램에 대한 유틸리티도 작성할 수 있다. 이제 그래픽 프로그램에 사용할 매크로 모음이 있으면 그래픽을 위한 프로그래밍 언어처럼 보이기 시작할 것이다.
; 이 연속의 맨 끝에서(유틸리티의 반대편), 매크로를 사용하면 Lisp와 다른 언어(매크로로 만들어진 새로운 언어)로 전체 프로그램을 작성할 수 있습니다.
; 이러한 방식으로 사용되는 매크로는 embedded languages를 구현한다고합니다.

; 유틸리티 매크로는 bottom-up 스타일의 첫번째 자손이다.
; 심지어 프로그램이 너무 작아서 계층을 쌓기어렵다해도, 가장 낮은 계층에 Lisp자체를 추가하면 도움이 될 것이다.
; 유틸리티 nil!을 보자, 이녀석의 매개변수들을 모두 nil로 값을 넣는다. 이런 건 매크로로만 가능하다.
(defmacro nil! (x)
  `(setf ,x nil))

; nil!을 보자, 어떤 사람은 이걸 보고 이 매크로는 아무것도 하지 않는 녀석이라고 말 할 것이다. 단지 타이핑을 절약할 뿐이라는 것이라고.
; 맞다. 하지만 모든 매크로가 하는 일은 타이핑 절약이다.
; 컴파일러의 일은 기계어로 타이핑을 절약하는 일이다.
; 유틸리티의 가치를 과소평가 하면 안된다, 왜냐하면 이것들의 효과는 누적된다.
; 간단한 매크로의 몇몇 계층이 우아한 프로그램과 이해할 수 없는 프로그램의 차이를 만든다.
; 대부분 유틸리티는 이미 당신이 구현한 패턴들에서 나온다.
; 당신의 코드에서 패턴이 보이면, 유틸리티로 바꾸는 것을 고려하자.
; 패턴이 있는 일은 컴퓨터가 잘하는 거다. 왜 프로그램이 당신을 위해 해줄 수 있는데, 왜 그 프로그램에서 주어진 것들로만 개발하여 당신을 괴롭히는가.
; 어떤 프로그램을 작성할 때, 같은 일반적인 형태의 여러 다른 장소에서 [do] 루프를 사용한다고 가정하자.
(do ()
    ((not ))
     )
; 이런 패턴을 반복적으로 봤다면, 이 패턴은 종종 제대로 딱 맞는 이름이 있을 것이다.
; 여기서 그 이름은 while이라고 부른다. 이걸 새로운 유틸리티로 제공하고 싶다면, 매크로를 써야 한다.
; 왜냐하면! 우리는 조건적으로 평가를 해야 하고, 반복적으로 평가해야 한다.
; while의 구현은 page 91에 있다.
(defmacro while (test &body body)
  `(do ()
       ((not ,test))
       ,@body))
이제 이렇게 쓰면 된다.
(while 
  )
; 코드가 더 짧아지고 더 명확하게 선언된다.
; 인수를 변환하는 기능은 인터페이스 작성에서 매크로가 유용하게 되는 이유다.
; 적절한 매크로를 사용하면 길고 복잡한 표현이 필요한 경우! 짧고 간단한 표현으로 바꿀 수 있다.

; 그래픽 인터페이스를 예로들면 비록 그래픽 인터페이스에서 앤드유저를 위한 매크로 작성의 필요성은 감소하지만
; 프로그래머는 그 어느 때 보다 많이 사용한다.
; 가장 일반적인 예는 defun이다. 이녀석 때문에 함수를 바인딩하는 것이 표면적으로 Pascal/C 같은 언어와 유사하게 만들 수 있게 된 것이다.
; Chapter 2에서 보여줬지만 아래 두 표현식은 같은 효과를 낸다.
(defun foo (x) (* x 2))

(setf (symbol-function 'foo)
      #'(lambda (x) (* x 2)))

defun매크로는 후자의 표현식으로 바꿔주는 것으로 구현된다(대략적으로)
(defmacro our-defun (name params &body body)
  `(progn
     (setf (symbol-function ',name)
           #'(lambda ,params (block ,name ,@body)))
     'name))

; while,nil!같은 매크로는 general-purpose 유틸리티로 볼 수 있다. 이것들은 모든 리습 프로그램에서 사용할 수 있다.
; 하지만! 특정 도메인은 그들만의 유틸리티를 가질 수 있다.
; base lisp(리스프언어)만이 우리가 확장해야할 계층이라고 생각하면 안된다.
; CAD 프로그램을 예로들자, 가장 좋은 결과는 때때로 두개의 계층으로 작성하는 것이다.
; 1. a language(좀 더 마일드하게 표현하면 툴킷), CAD program만을 위한 언어를 만드는 것이다.
; 2. 그리고 그 계층 위에 특정 앱을 구현하는 것이다.

; lisp는 다른 언어가 당연스럽게 여기는 구분을 흐릿하게 만든다.
; 다른 언어에서는, 컴파일타임/런타임, 프로그램/데이터, 언어/프로그램 같은 것들 사이에 개념적 차이가 있다.
; Lisp에서는 오직 대화로 만들어지는 규칙(conversational conventions)에선 존재한다.(너희들이 정하는 거다? 뭐 이런 뜻이려나)
; 뚜렷한 구분선이 없다. 예를들어보자, 언어와 프로그램의 구분선이 리습에 확실하게 있는지...
; 당신은 문제가 있는 곳바다 선을 그릴 수 있다. 따라서 기본 코드 레이어(기본 코드 계층에서 만든 매크로 포함)를 툴킷이라 할지, 언어라 할지 그건 용어의 차이에 불과하다.
; 이걸 언어라고 생각할 때의 장점은,  Lisp를 사용하는 것 같이, 유틸리티를 사용하여 언어를 확장할 수 있다는 것이다.

; 대화식 2D 그리기 프로그램을 만든다 가정하자. 
; 간단히 하기 위해, 프로그램에 처리되는 유일한 객체는 원점와 벡터로 표시되는 선분(line segments)이라 가정하자.
; 이런 프로그램이 해야 할 일은 하나의 개체 그룹을 미끄러지듯이 움직이게 하는 것이다.(slide)
; 이것에 그림 8.1의 move-objs가 하는 일이다. 효율적으로, 우리는 각 연산이 완료되엇을 때 스크린의 전부를 redraw하고 싶지 않다.
; 우리는 변경된 부분만 redraw하고 싶다. 하여, bounds함수를 두번(이동전,후) 호출하여 (min x, min y, max x, max y 사각형)객체 그룹의 경계를 리턴한다.
; "move-objs"의 작동 부분은 "bounds"에 대한 두 호출 사이에 끼워져 이동 전후에 경계 사각형을 찾은 다음 영향을받는 전체 영역을 다시 그린다.
; "scale-objs"는 객체의 그룹의 사이즈를 바꾼다. 역시 비슷한 패턴을 가진다. 이건 뭐 회전, 반전, 뭐 이런거에 전부 적용될 듯하다.
; 여기서 그림 8.1의 구현을 보자.

; Fig 8.1 Original move and scale.
(defun move-objs (objs dx dy)
  (multiple-value-bind (x0 y0 x1 y1) (bounds objs)
    (dolist (o objs)
      (incf (obj-x o) dx)
      (incf (obj-y o) dy))
    (multiple-value-bind (xa ya xb yb) (bounds objs)
      (redraw (min x0 xa) (min y0 ya)
              (max x1 xb) (max y1 yb)))))

(defun scale-objs (objs factor)
  (multiple-value-bind (x0 y0 x1 y1) (bounds objs)
    (dolist (o objs)
      (setf (obj-dx o) (* (obj-dx o) factor)
            (obj-dy o) (* (obj-dy o) factor)))
    (multiple-value-bind (xa ya xb yb) (bounds objs)
      (redraw (min x0 xa) (min y0 ya)
              (max x1 xb) (max y1 yb)))))


; 매크로를 이용하면 우리는 이 패턴의 코드를 추상화할 수 있다.
; 그림 8.2 with-redraw는 이 패턴의 스켈레톤을 제공한다.
; 이렇게 하니 함수들이 4줄로 바뀌게 된다. 각각!
; Fig 8.2 Move and scale filleted
(defmacro with-redraw ((var objs) &body body)
  (let ((gob (gensym))
        (x0 (gensym)) (y0 (gensym))
        (x1 (gensym)) (y1 (gensym)))
    `(let ((,gob ,objs))
       (multiple-value-bind (,x0 ,y0 ,x1 ,y1) (bounds ,gob)
         (dolist (,var ,gob) ,@body)
         (multiple-value-bind (xa ya xb yb) (bounds ,gob)
           (redraw (min ,x0 xa) (min ,y0 ya)
                   (max ,x1 xb) (max ,y1 yb)))))))

(defun move-objs (objs dx dy)
  (with-redraw (o objs)
    (incf (obj-x o) (dx)
    (incf (obj-y o) (dy)))

(defun scale-objs (objs factor)
  (with-redraw (o objs)
    (setf (obj-dx o) (* (obj-dx o) factor)
          (obj-dy o) (* (obj-dy o) factor))))

; 우리는 이 with-redraw를 대화식 그리기 프로그램을 위한 언어 안에 있는 구성물로 볼 수 있다.(언어의 파트가 된 것?)
; 우리가 더 많은 매크로를 개발함에 따라, 그것들은 실제로 이름뿐만 아니라 프로그래밍 언어와 유사하게 될 것이며, 
; 우리의 응용 프로그램 자체는 특정 요구에 맞게 정의 된 언어로 작성된 프로그램에서 기대할 수있는 우아함을 보이기 시작할 것.

; 매크로의 다른 주요용도는 embedded language를 만드는 것이다.
; 리습은 프로그래밍 언어를 작성하기 아주 좋은 언어이다.
; 왜냐하면 리습 프로그램은 리스트로 표현될 수 있으며, 리습은 그렇게 표현된(리스트로 표현된?!) 빌트인 파서(read)와 컴파일러(compile)가 있다.
; 변환을 수행하는 코드를 컴파일하여 내장 언어 컴파일러를 암시적으로 가질 수 있다. (25 페이지).
; embedded language는 리스프 위에 작성되지 않은 언어이다. 하여 신텍스는 리스프와 새로운 언어에 특정한 구조들의 혼합이다.
; 임베디드 언어를 구현하는 나이브한 방법은 리습에서 인터프리터를 작성하는 것이다.
; 가능한 경우, 더 나은 방법은 변환(transformation)을 통해 언어를 구현하는 것이다.
; 인터프리터가 해당 표현식을 평가하기 위해 실행했을 리습 코드로 변환하라.
; 이 개념에서 매크로가 들어오는 것이다.
; 매크로의 역할은 한 표현식의 형태를 다른 형태로 변환하는 것이다. 하여 임배디드언어(내장언어)를 만들 때 자연적으로 선택하게 되는 기술이다.

; 일반적으로 변환으로 임베디드 언어를 더 많이 구현할 수록 좋다.
; 첫번째, 일을 적게한다. 
; 예를 들어, 새로운 언어에 산술이 있다면 숫자 수량을 표현하고 조작하는 모든 복잡성에 직면 할 필요는 없습니다.
; 만약 리습의 산술능력이 당신의 목적에 충분한 만큼 괜찮다면, 산술 표현식을 동등한 Lisp 표현식으로 변환하고 나머지는 Lisp에 남겨 둘 수 있습니다.

; 변환을 사용하면 일반적으로 내장 언어도 더 빨라진다.
; 인터프리터는 속도와 관련하여 고유한 단점이 있다.
; 예를 들어 루프 내에서 코드가 발생하면 인터프리터는 각 반복에 대해 작업을 수행해야하는 경우가 종종 있습니다. 컴파일 된 코드에서는 한 번만 수행 할 수 있는데...
; 자체 인터프리터가 있는 임베디드 언어는 느려진다. 인터프리터 자체가 컴파일된다해도 그렇다.
; 그러나 새로운 언어의 표현식이 리습으로 변환되면, 결과코드는 리습컴파일러에 의해 컴파일 될 수 있다.
; 이런게 구현된 언어는 런타임에 인터프리테이션의 오버헤드를 겪지 않아도 된다.
; 언어에 맞는 진정한 컴파일러 작성은 매크로가 최상의 성능을 제공한다.
; 실제로, 새로운 언어를 변형시키는 매크로는 이것을 위한 컴파일러라고 볼 수 있다.
; 대부분의 작업을 수행하기 위해 기존 lisp 컴파일러에 의존하는 것이다.
; 여기서 임베디드 언어에 대한 예제는 다루지 않을 것이며 19-25에서 다룰 것임

; 챕터 19는 인터프리팅과 임베디드 언어 변환과의 차이를 다룰 것이다.
; 그리고 두 방식에 대한 구현을 각각 제시한다.

; 어떤 커먼리습을 책은 매크로의 스코프가 아주 제한적이라고 한다.
; 그리고 CLTL1에 정의된 연산자의 사실을 증거로 인용하는데, 10% 미만의 매크로만이 구현되어 있다고 한다.
; 이 말은 집이 벽돌로 되어 있다고 가구도 벽돌로 되어있어야 한다는 논리다.
; 커먼리습 프로그램 안에 매크로 비율은 전적으로 해야 할 일이 달려 있습니다.
; 일부 프로그램에는 매크로를 아예 가지지 않는다.
; 일부 프로그램은 모두 매크로 일 수 있다.