LG전자 SEED2016 발표
개미수열 문제를 풀어보면서 다양한 프로그래밍 개념들을 적용시켜봅니다. (Java/JavaScript/Go/C/Scala/Haskell 코드가 조금씩 등장)
- regex
- list processing
- iterator/generator
- coroutine/continuation
- lazy list
2. 개미수열
1
11
21
1211
111221
312211
13112221
1113213211
?? n번째 줄 출력하기
개미수열은 앞 줄을 읽어 다음 줄
을 만들어내는 수열
두 번째 줄 “11”을 “2개의 1(영어
식 표현)”이라고 읽어서 “21”이 됨
?? 로 표시한 9번째 줄은 8번째
줄을 읽어서 구할 수 있음
“3개의 1, 1개의 3, 1개의 2, …”
“31131211131221”
문제: 개미수열의 n(0 기준)번째
줄을 출력하는 함수를 작성하라
3. 열 명 중 아홉 명은…
String ant(int n) {
String s = "1";
for (int i = 0; i < n; i++) {
char c = s.charAt(0);
int count = 1;
String result = "";
for (int i = 1; i < s.length(); i++) {
if (c == s.charAt(i)) count++;
else {
result += count;
result += c;
c = s.charAt(i);
count = 1;
}
}
result += count;
result += c;
s = result;
}
return s;
}
대부분 중첩 for로 작성
s는 “1”로 시작하고,
n만큼 반복 적용
s를 읽으면서 이미 읽은 글자 c와
비교하고 같으면 count++
다르면 다음 줄 결과 result에
count와 c를 append
4. 아주 일부는
String ant(int n) {
String s = "1";
for (int i = 0; i < n; i++) {
s = next(s);
}
return s;
}
String next(String s) {
char c = s.charAt(0);
int count = 1;
String result = "";
for (int i = 1; i < s.length(); i++) {
if (c == s.charAt(i)) count++;
else {
result += count;
result += c;
c = s.charAt(i);
count = 1;
}
}
result += count;
result += c;
return result;
}
next() 함수를 만들고 반복 적용하
는 식으로 작성하는 경우도 있음.
한 덩어리로 작성하는 것보다는
낫다.
6. JavaScript/Regex
• 규칙을 Regular Expression으로 표현할 수 있다면 매우 다행
– 실제로 그렇지 못한 경우가 많음
– 게다가, 정확한 Regex를 작성하는 것은 매우 어려운 일
function next(s) {
return s.replace(/(.)1*/g, g => g.length + g[0])
}
“1112…”
3 1
g
length g[0]
7. Java/Regex
• Java에도 JavaScript의 replace같은 함수가 있다면…
– 직접 만들면 됨
String next(String s) {
return replaceAll(s, "(.)1*", g -> format("%d%c", g.length(), g.charAt(0)));
}
String replaceAll(String s, String regex, UnaryOperator<String> f) {
StringBuffer sb = new StringBuffer();
Matcher m = Pattern.compile(regex).matcher(s);
while (m.find()) {
String g = m.group();
m.appendReplacement(sb, f.apply(g));
}
m.appendTail(sb);
return sb.toString();
}
기본 틀은 compile/matcher/find/group
추가로, appendReplacement와
appendTail을 이용하면 JavaScript의
replace처럼 동작하는 도움함수를 작성
할 수 있음
Java8의 UnaryOperator를 이용
8. App-Specific vs. General
• 처음 ant(n)는 한 덩어리
• next(s)만 분리되어도 좀 나음
• next(s)도 Regex로 한단계 더 분리되었음
– replaceAll(s,regex,f)은 일반적인 함수
ant(n)
next(s)
replaceAll(s, regex, f)
문제를 잘게 나누다보면 결국 일반적인 작은 문
제들이 되고,
이런 작은 문제들은 빈번하게 등장하게 된다.
replaceAll(s,regex,f)는 어디나 써먹을 수 있음
9. next() 함수를 더 나누기
• 열명 중 아홉이 짜는 next()
String next(String s) {
char c = s.charAt(0);
int count = 1;
String result = "";
for (int i = 1; i < s.length(); i++) {
if (c == s.charAt(i)) count++;
else {
result += count;
result += c;
c = s.charAt(i);
count = 1;
}
}
result += count;
result += c;
return result;
}
count
compare
loop
format/append
10. next는 리스트 프로세싱
• List<Integer> next(List<Integer> ns)
– 리스트를 통째로 다루기
13112221
1 3 11 222 1
11 13 21 32 11
1113213211
32
List<List<A>> group(List<A> as)
List<B> map(Function<A,B> f,
List<A> as)
List<A> concat(List<List<A>> ass)
다른 접근을 살펴보기 위해 String 대
신 List<Integer>로 바꿔보자!
리스트를 처리하는
group/map/concat 도움함수는
signature만 보더라도 매우 일반화된
함수라는 것을 알 수 있음
11. next()는 리스트 프로세싱
List<Integer> next(List<Integer> ns) {
return concat(map(g => listOf(g.size(), g.get(0)), group(ns));
}
ant(n)
next(s)
group (list)
map(f, list)
concat(list-of-list)
String에서 replaceAll같은 일반화된 도움함수를 사용
한 것처럼 List에 대해 일반화된 도움함수를 얻을 수 있
고, 이를 이용하면 next()는 아주 간단히 해결됨
for/==/++/+= 등의 primitive는 감춰지기 때문에 실수할
여지가 줄어듬
13. 개미수열 n == 100
1
11
21
1211
111221
312211
13112221
1113213211
..
..
..
...
?? 100 번째 줄 출력하기
14. 개미수열
OutOfMemoryError
• 한 줄마다 길이가 30%씩 증가
– 100번째 줄의 길이는??
– 대충… 5천억 = 5e11 = 500G
• 그럼 어떻게?
https://en.wikipedia.org/wiki/Look-
and-say_ sequence
이미 이 수열의 각 줄이 30%씩 증가
한다는 것을 증명한 수학자가 있음
100번째 줄은 String/List 에 담을 수
도 없다.
출력하기 위해 꼭 String/List에 담아
둘 필요는 없음
15. Iterator
• 무한 수열을 나타낼 수 있음
– boolean hasNext() { return true; }
Iterator<Integer> ant(int n) {
Iterator<Integer> s = asList(1).iterator();
for (int i=0; i<n; i++)
s = new Next(s);
return s;
}
class Next implements Iterator<Integer> { ... }
16. Iterator
• 100번째 줄 위로는 필요한 만
큼만 계산
– lazy evaluation
• 메모리 문제는 없지만 5천억개가
출력될 때까지 지켜봐야 함
1
Next
Next
Next
Next
Next while (s.hasNext())
System.out.print(s.next());
Next는 또다른 Iterator를 포함하
는 Wrapper Iterator
18. class Next implements Iterator<Integer> {
public Integer next() {
if (state == State.INIT) {
state = State.HAS_NEXT;
next = inner.next();
}
if (state == State.HAS_NEXT) {
state = State.LAST;
elem = next;
count = 1;
while (inner.hasNext()) {
int next = inner.next();
if (next == elem) {
count++;
} else {
state = State.COUNT;
this.next = next;
break;
}
}
return count;
} else if (state == State.LAST) {
state = State.INIT;
return elem;
} else {
state = State.HAS_NEXT;
return elem;
}
}
Next 검토
• 그런데, 다시 처음 그 문제
가…
– 한 덩어리 Next.next()
• 그건 그렇고, 왜 for문보다
더 복잡하지?
– loop를 while(hasNext())로
넘겼음
– 상태변수를 따로 두어야 함
– 연속해서 값을 생성하기 어
려움
19. 리스트 프로세싱
• 한 덩어리 문제는 해결됨
• 복잡해진 진짜 원인은 loop를 빼앗긴 탓
– Iterator는 Loop 컨트롤을 외부로 빼앗겼음
– 상태 유지가 더 힘들어졌음
Iterator<Integer> next(Iterator<Integer> s) {
return new Concat(new Map(g -> …, (new Group(s)));
}
Concat/Map/Group은 Decorator
Map/Group을 합쳐서 RunLength 이터레이터를 만들 수도 있음
Concat/Map을 합쳐서 ConcatMap 이터레이터를 만들어도 됨
20. JavaScript/Generator
• 상태를 가지는
Iterator를 쉽게 만들
수 있는 도구
function *next(line) {
let prev = line.next().value
let count = 1
for (let c of line) {
if (prev === c)
count++
else {
yield count; yield prev
prev = c
count = 1
}
}
yield count; yield prev
}
for를 가지고
있다!
loop 내에서 여
러 값을 출력할
수도
21. Java/Generator
• Generator는
– yield에서 control을 놓고
– next()호출하면 resume
• 일종의 Coroutine
– Java에서는 Thread로 Generator Coroutine을 구현할 수 있
음
Generator<Integer> ints = Generator.of((g) -> {
int i = 0;
while (true)
g.yield(i++);
});
for (int i = 0; i < 100; i++) {
System.out.println(ints.next());
}
Thread로 구현하면 Thread를 종료시켜
줘야 하는 문제가…
일단 Generator interface는 쉽게 구현
할 수 있음.
JavaScript의 Generator처럼 사용 가능
22. • 코루틴은 서브루틴보다 더 일반적인 개념.
• 서브루틴은 call/return 뿐이지만, 코루틴은 suspend/resume이 가능.
• 코루틴이 suspend하지 않으면 그것이 서브루틴
• 이런의미로 앞의 Generator는 일종의 코루틴 (코루틴은 다른 코루틴을 suspend하면서 다른
코루틴을 resume할 수있는데, Generator는 suspend하면 호출한 쪽으로 되돌아감)
• Iterator/infinite list를 만드는데 사용할 수 있다고 나와 있음
23. Go/goroutine
• Go 언어는 고루틴/채널을 기본 제공
• go f()
– f()함수를 새로운 고루틴에서 실행
• c := make(chan int)
– 고루틴 간의 통신은 채널을 이용
– c 0 : c로 값을 전달
– c : c에서 값을 읽음
24. Go/goroutine
1
Next
Next
Next
Next
Next
고루틴
채널
ch := make(chan int)
go func() {
ch <- 1
close(ch)
}()
for i := 0; i < n; i++ {
ch1 := make(chan int)
go next(ch, ch1)
ch = ch1
}
func next(in, out chan int) {
... generator와 거의 같음
}
채널 연산은 yield/resume 역할을 함
Go에서 고루틴을 이용하여 작성하면 n == 100을 쉽
게 출력할 수 있음
25. Java/goroutine
• Thread/BlockingQueue를 이용하여 go/send/recv/close 만들기
interface Goroutine {
void run()
}
static void go(Goroutine go) {
new Thread(() -> go.run()).start();
}
class Chan<A> {
SynchronousQueue<Option<A>> queue = new …
void send(A a) { queue.put(option(a)); }
Option<A> recv() { return queue.take(); }
void close() { queue.put(option()); }
}
Chan<Integer> ch = new .
go(() -> {
ch.send(1);
ch.close();
})
28. 개미수열
StackOverflowError
• Iterator.next -> next -> next …
– Generator는 producer 코루틴을 위한 것
– Next는 transducer 코루틴(출력 뿐 아니라 입력도 필요하다)
• Thread를 이용한 Coroutine의 경우에는 OutOfMemoryError
– OutOfMemoryError: unable to create new native thread
• Go는 괜찮은데..
– Goroutine은 Green thread
• 그럼 어떻게?
– Go의 Green thread를 흉내내거나
– Stack을 사용하지 않는 Coroutine을 만들거나
29. 개미수열 복잡도
50번째 줄을 출력하는 과정 살펴보기
맨 아랫줄에 빨간색 칸은 그 위치의 값을 계산하려
고 suspen되었음을 보여주고
그 앞줄/그 앞줄… 도 suspend
그러다 값이 계산되면 차례로
resume/resume/resume될 것
어느 한 순간에는 하나의 Coroutine만 Active!
30. Coroutine의 본질
• Cooperative multitasking (non-preemptive)
– yield/resume
– 각각을 쓰레드로 보더라도 동시에 실행되지는 않음
• producer transducer consumer
start
yield
데이터 요청
resume
yield
데이터 요청
resume
yield
데이터 생성
resume
yield
데이터 요청
resume
yield
데이터 생성
yield
데이터 생성
resume
startstart
Go에서는 채널에
데이터 요청/전달
나머진 거의 같음
• 첫줄(1을 출력)은 Producer,
• 앞줄을 읽어 다음 줄을 생성하는
Next()는 Transducer,
• n번째 줄을 출력하는 건
Consumer임
코루틴이 yield하는 이유가 두가지
(요청/생성)
31. resume/yield
• 우리가 가진 건 서브루틴 뿐
– call/return
• return할 내용
– yield하는 이유(값 요청? 전달?)
– 다시 resume할 위치
• call할 때 전달할 내용
– resume할 위치
– 요청 값
• 코루틴들을 organize할 dispatcher 함수 필요
– 각 코루틴들의 상태를 기억(스택 변수는 쓸모없음)
• 심지어 C로도 가능하다
32. C/Coroutine
typedef struct state {
char prev; // 이전에 읽은 값
char count; // 현재까지 누적 카운트
char next; // 다음으로 읽은 값
char ptr; // resume할 위치
} state;
int init(state *s) {
switch (s->ptr) {
case 0:
s->ptr = 1;
return 1;
default:
return 0;
}
}
반환값 약속
• -1: 값 요청
• 0: 스트림 종료
• 1/2/3: 값 전달
func init(i, o) {
o <- 1
close(o)
}
go init(ch)
function *init() {
yield 1
return
}
resume 위치를 반환하
는 방법도 있음
33. C/Coroutine
typedef struct state {
char prev; // 이전에 읽은 값
char count; // 현재까지 누적 카운트
char next; // 다음으로 읽은 값
char ptr; // resume할 위치
} state;
int init(state *s) {
switch (s->ptr) {
case 0:
s->ptr = 1;
return 1;
default:
return 0;
}
}
int next(state *s) {
switch (s->ptr) {
case 0:
s->ptr = 1;
return -1;
case 1:
s->prev = s->next;
s->count = 1;
s->ptr = 2;
return -1;
case 2:
if (s->prev == s->next) {
s->count++;
return -1;
} else if (s->next == 0) {
s->ptr = 3;
return s->count;
반환값 약속
• -1: 값 요청
• 0: 스트림 종료
• 1/2/3: 값 전달
값을 읽기 위해
yield
ptr 조작 X
loop!!
resume
resume
다음 값을 읽음
yield
34. C/Coroutine
int n = 1000000;
state* lines = (state*)calloc(n + 1, sizeof(state));
int cur = n + 1;
while (1) {
int result = (cur == 0) ? init(&lines[0]) : next(&lines[cur]);
switch (result) {
case -1: // read
cur--;
break;
default: // close or write 1/2/3
if (cur < n) {
cur++;
lines[cur].next = result;
} else {
printf("%d", result);
}
}
값을 읽으려면 선행 코루틴
실행해야
다음 코루틴으로 값을 전달하고
resume
35. 개미수열 복잡도
• 공간 복잡도
– O(n)
• 시간 복잡도
– n번째 줄 m번째 글자까지 출력
– O(n + m log m)
37. Coroutine vs. Continuation
• Coroutine
– Thread 이용
• 실제로 pause/resume
– resume pointer
• Continuation을 반환하는 것으로 이해할 수 있음
• Continuation
– ptr 반환후 resume할 때 jump 하는 대신
– 다음 실행할 continuation을 closure로 반환
– resume은 continuation을 호출하는 것
38. JavaScript/CPS
• Continuation Passing Style
– setTimeout(continuation, 1000)
• 1초뒤 실행할 내용을 continuation에 담아 전달
function init() {
return write(1, undefined)
}
function write(value, cont) {
return { type: 'write', cont }
}
1을 전달하고 다음 실행할
내용은 없다
39. JavaScript/CPS
function init() {
return write(1, undefined)
}
function next() {
return read(c => loop(c, 1))
function loop(prev, count) {
return read(c => {
if (typeof c === 'undefined') return write(count, () => write(prev, undefined))
else if (prev === c) return loop(prev, count + 1)
else return write(count, () => write(prev, () => loop(c, 1)))
})
}
}
첫 글자 읽고 loop 진입
loop에서
글자 읽어서
종료? count/prev 출력 후 종료
같음? count증가 후 loop 반복
다름? count/prev 출력 후 새로 읽은 글자로 반복
40. JS/CPS 검토
• write2 같은 추상화 가능
• Callback Hell
– 흐름을 추적하기 어려움
– Promise같은 CPS 추상화 필요
function write2(a, b, cont) {
return write(a, () => write(b, cont))
}
41. JS/Promise
• Callback에 대한 추상화
– 직접 Callback 인자를 받고, Callback을 호출하는 대신
– Callback을 처리할 Promise 객체를 반환
– Promise가 Callback을 처리
– Chaining이 가능한 then 메쏘드
step1(arg1, (res1) => {
step2(arg2, (res2) => {
step3(arg3, (res3) => {
...
})
})
})
step1(arg1)
.then((res1) => ... step2(arg2))
.then((res2) => ... step3(arg3))
.then((res3) => ...)
42. Read/Write 추상화
class Read {
constructor(cont) { this.cont = cont }
then(f) {
return new Read(x => this.cont(x).then(f)
}
}
class Write {
constructor(value, cont) { this.value = value; this.cont = cont }
then(f) {
return new Write(this.value, this.cont.then(f))
}
}
function read() { return new Read(undefined) }
function write(value) { return new Write(value, undefined) }
43. Read/Write 추상화
function init() {
return write(1)
}
function next() {
return read()
.then(c => loop(c, 1))
function loop(prev, count) {
return read()
.then(c => {
if (typeof c === 'undefined') return write2(count,prev)
else if (prev === c) return loop(prev, count + 1)
else return write2(count, prev)
.then(() => loop(c, 1))
})
}
}
44. CPS 추상화 검토
• 추상화를 깔고 새로운 추상화
• OOP Design Patterns의 Interpreter패턴
– Read/Write 는 cont를 가지며 Composite
function forever(program) {
return program.then(() => program)
}
const echo = read().then(write)
const prog = forever(echo)
run(prog)
47. Scala/Haskell
• Scala는 Stream[A] 라는 Lazy list를 지원
• Haskell은 기본 리스트가 Lazy (사실 전부 lazy)
def ant = Stream.iterate(Stream(1))(next)
def next(s: Stream[Int]) = group(s) flatMap {g => Stream(g.size, g.head)}
def group[A](as: Stream[A]): Stream[Seq[A]] = ...
ant(1000000)(1000000) // 1M번째 줄 1M번째 글자
ant = iterate(group >=> sequence[length, head]) [1]
48. Java/JS
• Java와 JS는 동적 언어
• 쉽게 Lazy list를 만들 수 있다.
• Lazy list의 핵심은 Linked list의 Tail을 필요할 때 생성하기
class List<A> {
...
public List(A head, Supplier<List<A>> tail) { ...}
public A head() { return head; }
public List<A> tail() { return tail.get(); }
}
List<Integer> intsFrom(int n) {
return new Node(n, () => intFrom(n+1))
}
49. Java/JS
• Java와 JS는 동적 언어
• 쉽게 Lazy list를 만들 수 있다.
• Lazy list의 핵심은 Linked list의 Tail을 필요할 때 생성하기
class List<A> {
...
public List(A head, Supplier<List<A>> tail) { ...}
public A head() { return head; }
public List<A> tail() { return tail.get(); }
}
List<Integer> intsFrom(int n) {
return new Node(n, () => intFrom(n+1))
}
50. List vs. Stream
• List/Stream은 같은 추상화의 동작만 다
른 형태
List<Integer> next(List<Integer> ns) {
return concat(map(g => listOf(g.size(), g.get(0)), group(ns));
}
Stream<Integer> next(Stream<Integer> ns) {
return concat(map(g => streamOf(g.size(), g.head()), group(ns));
}