SICP 2장을 죽 읽다보면 map
을 이용한 리스트 조작이 나오고, 또 이를 재귀적으로 적용하여 우리가 흔히 얘기하는 이중 루프 (혹은 겹친 루프) 를 만들어 내는 예제나 연습문제가 많이 나온다.
은근하게도, 하나의 컨벤션이 제시되는데 바로 flatmap
이다.
아래는 바로 대표적인 예를 보여주는 연습문제 n-queens problem
이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| (define (n-queens n) (define (iter m) (if (zero? m) '(()) (filter (λ (queens) (let* ([current-queen (car queens)] [rest-queens (cdr queens)] [indexed-rest-queens (map cons rest-queens (range 1 (length queens)))]) (not (ormap (λ (indexed-queen) (ormap (λ (current-queen-attack-range) (= (car indexed-queen) current-queen-attack-range)) (list current-queen (+ current-queen (cdr indexed-queen)) (- current-queen (cdr indexed-queen))))) indexed-rest-queens)))) (append-map (λ (result) (map (λ (current) (cons current result)) (range 1 (add1 n)))) (iter (sub1 m)))))) (iter n))
|
plt-racket
에서의 flatmap
은 append-map
이다. 참고로, clojure
에서는 mapcat
처음 SICP 를 볼때는 아무 생각없이 flatmap
을 썼었는데, 사실 이 flatmap
이 이런 쓰임새로 활용될 때에는 머릿속에 잘 그려지지 않는다고 생각한다. 해서, flatmap
의 동작을 설명하는 코드를 짜 보았다.
먼저 flatmap
은 apply-append-map
과 같다. 위에서 append-map 으로 시작하는 코드를 다음과 같이 바꿔도 동작할 것이다.
1 2 3 4 5 6 7
| (apply append (map (λ (result) (map (λ (current) (cons current result)) (range 1 (add1 n)))) (iter (sub1 m))))
|
이는 사실 모든 원소에 함수롤 적용한 결과로부터 얻은 리스트들을 다시 하나의 리스트들로 모은다는 뜻이다. 따라서 reduce-by-append
와 같다.
1 2 3 4 5 6 7 8
| (foldr append '() (map (λ (result) (map (λ (current) (cons current result)) (range 1 (add1 n)))) (iter (sub1 m))))
|
이렇게 해놓고 나니, flatmap
이 어떻게 동작할 지, 머릿속에 분명하게 그려진다. 다시 flatmap
을 써야지.
요즘 SICP 스터디를 다시 하면서 요런코드들을 자바나 자바스크립트로 써보고 있는데, 특히 자바의 경우는 java8 이후로 람다와 스트림을 지원해서 어느정도 용이하게 작성하는게 가능해졌다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class Main { public static void main(String[] args) { System.out.println(nQueens(5)); } public static List<List<Integer>> nQueens(Integer n) { return nQueens(n, n); } public static List<List<Integer>> nQueens(Integer n, Integer m) { if (m.equals(0)) { List<List<Integer>> lst = new LinkedList<>(); lst.add(new LinkedList<>()); return lst; } else { return nQueens(n, m - 1).stream() .flatMap(result -> IntStream.range(1, n + 1) .boxed() .map(current -> { List<Integer> lst = new LinkedList<>(result); lst.add(0, current); return lst; }) ) .filter(queens -> { int currentQueen = queens.get(0); boolean isSafe = true; for (int i = 1; i < queens.size(); i++) { Integer queen = queens.get(i); if (currentQueen == queen || currentQueen + i == queen || currentQueen - i == queen) isSafe = false; } return isSafe; }) .collect(Collectors.toList()); } } }
|
요즘엔 자바를 쓰면서도 그다지 불편하지 않아서 참 다행이라는 생각이 든다.
하지만 점점 돈은 안되는 느낌이다. 슬프다..