52
SW공학연구소 한주영 개미수열 프로그래밍 look-and-say sequence

Seed2016 - 개미수열 한주영 (annotated)

Embed Size (px)

Citation preview

Page 1: Seed2016 - 개미수열 한주영 (annotated)

SW공학연구소한주영

개미수열 프로그래밍look-and-say sequence

Page 2: Seed2016 - 개미수열 한주영 (annotated)

개미수열

1

11

21

1211

111221

312211

13112221

1113213211

?? n번째 줄 출력하기

개미수열은 앞 줄을 읽어 다음 줄을 만들어내는 수열

두 번째 줄 “11”을 “2개의 1(영어식 표현)”이라고 읽어서 “21”이 됨

?? 로 표시한 9번째 줄은 8번째줄을 읽어서 구할 수 있음

“3개의 1, 1개의 3, 1개의 2, …”

“31131211131221”

문제: 개미수열의 n(0 기준)번째줄을 출력하는 함수를 작성하라

Page 3: Seed2016 - 개미수열 한주영 (annotated)

열 명 중 아홉 명은…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

Page 4: Seed2016 - 개미수열 한주영 (annotated)

아주 일부는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() 함수를 만들고 반복 적용하는 식으로 작성하는 경우도 있음.

한 덩어리로 작성하는 것보다는낫다.

Page 5: Seed2016 - 개미수열 한주영 (annotated)

오늘 우리는

• 다양한 방법으로 개미수열을 풀어봅니다

Page 6: Seed2016 - 개미수열 한주영 (annotated)

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]

Page 7: Seed2016 - 개미수열 한주영 (annotated)

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를 이용

Page 8: Seed2016 - 개미수열 한주영 (annotated)

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)는 어디나 써먹을 수 있음

Page 9: Seed2016 - 개미수열 한주영 (annotated)

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

Page 10: Seed2016 - 개미수열 한주영 (annotated)

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만 보더라도 매우 일반화된함수라는 것을 알 수 있음

Page 11: Seed2016 - 개미수열 한주영 (annotated)

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는 감춰지기 때문에 실수할여지가 줄어듬

Page 12: Seed2016 - 개미수열 한주영 (annotated)

Recap

• ant/next

– for/count/==/+=/…

• next/regex

– replace(regex, f)

• next/list-processing

– concat/map/group

Page 13: Seed2016 - 개미수열 한주영 (annotated)

개미수열 n == 100

111211211111221312211131122211113213211.........?? 100 번째 줄 출력하기

Page 14: Seed2016 - 개미수열 한주영 (annotated)

개미수열

OutOfMemoryError

• 한 줄마다 길이가 30%씩 증가

– 100번째 줄의 길이는??

– 대충… 5천억 = 5e11 = 500G

• 그럼 어떻게?

https://en.wikipedia.org/wiki/Look-

and-say_ sequence

이미 이 수열의 각 줄이 30%씩 증가한다는 것을 증명한 수학자가 있음

100번째 줄은 String/List 에 담을 수도 없다.

출력하기 위해 꼭 String/List에 담아둘 필요는 없음

Page 15: Seed2016 - 개미수열 한주영 (annotated)

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> { ... }

Page 16: Seed2016 - 개미수열 한주영 (annotated)

Iterator

• 100번째 줄 위로는 필요한 만큼만 계산

– lazy evaluation

• 메모리 문제는 없지만 5천억개가출력될 때까지 지켜봐야 함

1

Next

Next

Next

Next

Next while (s.hasNext())System.out.print(s.next());

Next는 또다른 Iterator를 포함하는 Wrapper Iterator

Page 17: Seed2016 - 개미수열 한주영 (annotated)

개미수열 n == 100

11131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211132221121311121312211213211312111322211213211321322113311213212322211231131122211311123113223112111311222112132113311213211221121332211211131221131211132221231122212213211321322112311311222113311213212322211211131221131211132221232112111312111213322112131112131221121321131211132221121321132132212321121113121112…

Page 18: Seed2016 - 개미수열 한주영 (annotated)

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())로

넘겼음

– 상태변수를 따로 두어야 함

– 연속해서 값을 생성하기 어려움

Page 19: Seed2016 - 개미수열 한주영 (annotated)

리스트 프로세싱

• 한 덩어리 문제는 해결됨

• 복잡해진 진짜 원인은 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 이터레이터를 만들어도 됨

Page 20: Seed2016 - 개미수열 한주영 (annotated)

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 내에서 여러 값을 출력할수도

Page 21: Seed2016 - 개미수열 한주영 (annotated)

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처럼 사용 가능

Page 22: Seed2016 - 개미수열 한주영 (annotated)

• 코루틴은 서브루틴보다 더 일반적인 개념.

• 서브루틴은 call/return 뿐이지만, 코루틴은 suspend/resume이 가능.

• 코루틴이 suspend하지 않으면 그것이 서브루틴• 이런의미로 앞의 Generator는 일종의 코루틴 (코루틴은 다른 코루틴을 suspend하면서 다른

코루틴을 resume할 수있는데, Generator는 suspend하면 호출한 쪽으로 되돌아감)

• Iterator/infinite list를 만드는데 사용할 수 있다고 나와 있음

Page 23: Seed2016 - 개미수열 한주영 (annotated)

Go/goroutine

• Go 언어는 고루틴/채널을 기본 제공

• go f()

– f()함수를 새로운 고루틴에서 실행

• c := make(chan int)

– 고루틴 간의 통신은 채널을 이용

– c 0 : c로 값을 전달

– c : c에서 값을 읽음

Page 24: Seed2016 - 개미수열 한주영 (annotated)

Go/goroutine

1

Next

Next

Next

Next

Next

고루틴

채널

ch := make(chan int)

go func() {ch <- 1close(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을 쉽게 출력할 수 있음

Page 25: Seed2016 - 개미수열 한주영 (annotated)

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();

})

Page 26: Seed2016 - 개미수열 한주영 (annotated)

Recap

• Iterator

– next()

• Generator

– next()/yield

• Coroutine

– with Thread/goroutine

n == 100에서 어떻게 Iterator로 변형Iterator 구현이 지저분함 JavaScript의 Generator Java로 …

Go의 Goroutine Java로 …

Page 27: Seed2016 - 개미수열 한주영 (annotated)

개미수열 n == 10000

111211211111221312211131122211113213211............................ ............?? 10000 번째 줄 출력하기

Page 28: Seed2016 - 개미수열 한주영 (annotated)

개미수열

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을 만들거나

Page 29: Seed2016 - 개미수열 한주영 (annotated)

개미수열 복잡도

50번째 줄을 출력하는 과정 살펴보기

맨 아랫줄에 빨간색 칸은 그 위치의 값을 계산하려고 suspen되었음을 보여주고

그 앞줄/그 앞줄… 도 suspend

그러다 값이 계산되면 차례로resume/resume/resume될 것

어느 한 순간에는 하나의 Coroutine만 Active!

Page 30: Seed2016 - 개미수열 한주영 (annotated)

Coroutine의 본질

• Cooperative multitasking (non-preemptive)

– yield/resume

– 각각을 쓰레드로 보더라도 동시에 실행되지는 않음

• producer transducer consumerstart

yield데이터 요청

resume

yield데이터 요청

resume

yield데이터 생성

resume

yield데이터 요청

resume

yield데이터 생성

yield데이터 생성

resume

startstart

Go에서는 채널에데이터 요청/전달나머진 거의 같음

• 첫줄(1을 출력)은 Producer,

• 앞줄을 읽어 다음 줄을 생성하는Next()는 Transducer,

• n번째 줄을 출력하는 건Consumer임

코루틴이 yield하는 이유가 두가지(요청/생성)

Page 31: Seed2016 - 개미수열 한주영 (annotated)

resume/yield

• 우리가 가진 건 서브루틴 뿐

– call/return

• return할 내용

– yield하는 이유(값 요청? 전달?)

– 다시 resume할 위치

• call할 때 전달할 내용

– resume할 위치

– 요청 값

• 코루틴들을 organize할 dispatcher 함수 필요– 각 코루틴들의 상태를 기억(스택 변수는 쓸모없음)

• 심지어 C로도 가능하다

Page 32: Seed2016 - 개미수열 한주영 (annotated)

C/Coroutinetypedef 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 <- 1close(o)

}

go init(ch)

function *init() {yield 1return

}

resume 위치를 반환하는 방법도 있음

Page 33: Seed2016 - 개미수열 한주영 (annotated)

C/Coroutinetypedef 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 조작 Xloop!!

resume

resume

다음 값을 읽음yield

Page 34: Seed2016 - 개미수열 한주영 (annotated)

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: // readcur--;break;

default: // close or write 1/2/3if (cur < n) {

cur++;lines[cur].next = result;

} else {printf("%d", result);

}}

값을 읽으려면 선행 코루틴실행해야

다음 코루틴으로 값을 전달하고resume

Page 35: Seed2016 - 개미수열 한주영 (annotated)

개미수열 복잡도

• 공간 복잡도

– O(n)

• 시간 복잡도

– n번째 줄 m번째 글자까지 출력

– O(n + m log m)

Page 36: Seed2016 - 개미수열 한주영 (annotated)

C/Coroutine 검토

• No abstraction

– 중복 코드를 제거할 수도 없다

– 함수를 분리할 수도 없다

Page 37: Seed2016 - 개미수열 한주영 (annotated)

Coroutine vs. Continuation

• Coroutine– Thread 이용

• 실제로 pause/resume

– resume pointer• Continuation을 반환하는 것으로 이해할 수 있음

• Continuation– ptr 반환후 resume할 때 jump 하는 대신

– 다음 실행할 continuation을 closure로 반환

– resume은 continuation을 호출하는 것

Page 38: Seed2016 - 개미수열 한주영 (annotated)

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을 전달하고 다음 실행할내용은 없다

Page 39: Seed2016 - 개미수열 한주영 (annotated)

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 출력 후 새로 읽은 글자로 반복

Page 40: Seed2016 - 개미수열 한주영 (annotated)

JS/CPS 검토

• write2 같은 추상화 가능

• Callback Hell

– 흐름을 추적하기 어려움

– Promise같은 CPS 추상화 필요

function write2(a, b, cont) {return write(a, () => write(b, cont))

}

Page 41: Seed2016 - 개미수열 한주영 (annotated)

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) => ...)

Page 42: Seed2016 - 개미수열 한주영 (annotated)

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) }

Page 43: Seed2016 - 개미수열 한주영 (annotated)

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))})

}}

Page 44: Seed2016 - 개미수열 한주영 (annotated)

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)

Page 45: Seed2016 - 개미수열 한주영 (annotated)

Recap

• hand-written Coroutine

• JS/Continuation passing style

• CPS 추상화

Page 46: Seed2016 - 개미수열 한주영 (annotated)

무한 수열

• 더 일반적인 방법

– Stream/Lazy list

Page 47: Seed2016 - 개미수열 한주영 (annotated)

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]

Page 48: Seed2016 - 개미수열 한주영 (annotated)

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))

}

Page 49: Seed2016 - 개미수열 한주영 (annotated)

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))

}

Page 50: Seed2016 - 개미수열 한주영 (annotated)

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));

}

Page 51: Seed2016 - 개미수열 한주영 (annotated)

Recap

• for-loop, regex

• list processing

– stream(lazy list)도 마찬가지

• iterator/generator/coroutine

– Thread로 흉내내기

– Coroutine의 본질 – resume ptr

• Continuation passing

– CPS 추상화 : Interperter 패턴

Page 52: Seed2016 - 개미수열 한주영 (annotated)

감사합니다.

• http://github.com/jooyunghan/look-and-say