Upload
jooyung-han
View
862
Download
3
Embed Size (px)
Citation preview
SW공학연구소한주영
개미수열 프로그래밍look-and-say sequence
개미수열
1
11
21
1211
111221
312211
13112221
1113213211
?? n번째 줄 출력하기
개미수열은 앞 줄을 읽어 다음 줄을 만들어내는 수열
두 번째 줄 “11”을 “2개의 1(영어식 표현)”이라고 읽어서 “21”이 됨
?? 로 표시한 9번째 줄은 8번째줄을 읽어서 구할 수 있음
“3개의 1, 1개의 3, 1개의 2, …”
“31131211131221”
문제: 개미수열의 n(0 기준)번째줄을 출력하는 함수를 작성하라
열 명 중 아홉 명은…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
아주 일부는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() 함수를 만들고 반복 적용하는 식으로 작성하는 경우도 있음.
한 덩어리로 작성하는 것보다는낫다.
오늘 우리는
• 다양한 방법으로 개미수열을 풀어봅니다
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]
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를 이용
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)는 어디나 써먹을 수 있음
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
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만 보더라도 매우 일반화된함수라는 것을 알 수 있음
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는 감춰지기 때문에 실수할여지가 줄어듬
Recap
• ant/next
– for/count/==/+=/…
• next/regex
– replace(regex, f)
• next/list-processing
– concat/map/group
개미수열 n == 100
111211211111221312211131122211113213211.........?? 100 번째 줄 출력하기
개미수열
OutOfMemoryError
• 한 줄마다 길이가 30%씩 증가
– 100번째 줄의 길이는??
– 대충… 5천억 = 5e11 = 500G
• 그럼 어떻게?
https://en.wikipedia.org/wiki/Look-
and-say_ sequence
이미 이 수열의 각 줄이 30%씩 증가한다는 것을 증명한 수학자가 있음
100번째 줄은 String/List 에 담을 수도 없다.
출력하기 위해 꼭 String/List에 담아둘 필요는 없음
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> { ... }
Iterator
• 100번째 줄 위로는 필요한 만큼만 계산
– lazy evaluation
• 메모리 문제는 없지만 5천억개가출력될 때까지 지켜봐야 함
1
Next
Next
Next
Next
Next while (s.hasNext())System.out.print(s.next());
Next는 또다른 Iterator를 포함하는 Wrapper Iterator
개미수열 n == 100
11131221131211132221232112111312111213111213211231132132211211131221232112111312211213111213122112132113213221123113112221133112132123222112111312211312112213211231132132211211131221131211132221121311121312211213211312111322211213211321322113311213212322211231131122211311123113223112111311222112132113311213211221121332211211131221131211132221231122212213211321322112311311222113311213212322211211131221131211132221232112111312111213322112131112131221121321131211132221121321132132212321121113121112…
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())로
넘겼음
– 상태변수를 따로 두어야 함
– 연속해서 값을 생성하기 어려움
리스트 프로세싱
• 한 덩어리 문제는 해결됨
• 복잡해진 진짜 원인은 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 이터레이터를 만들어도 됨
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 내에서 여러 값을 출력할수도
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처럼 사용 가능
• 코루틴은 서브루틴보다 더 일반적인 개념.
• 서브루틴은 call/return 뿐이지만, 코루틴은 suspend/resume이 가능.
• 코루틴이 suspend하지 않으면 그것이 서브루틴• 이런의미로 앞의 Generator는 일종의 코루틴 (코루틴은 다른 코루틴을 suspend하면서 다른
코루틴을 resume할 수있는데, Generator는 suspend하면 호출한 쪽으로 되돌아감)
• Iterator/infinite list를 만드는데 사용할 수 있다고 나와 있음
Go/goroutine
• Go 언어는 고루틴/채널을 기본 제공
• go f()
– f()함수를 새로운 고루틴에서 실행
• c := make(chan int)
– 고루틴 간의 통신은 채널을 이용
– c 0 : c로 값을 전달
– c : c에서 값을 읽음
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을 쉽게 출력할 수 있음
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();
})
Recap
• Iterator
– next()
• Generator
– next()/yield
• Coroutine
– with Thread/goroutine
n == 100에서 어떻게 Iterator로 변형Iterator 구현이 지저분함 JavaScript의 Generator Java로 …
Go의 Goroutine Java로 …
개미수열 n == 10000
111211211111221312211131122211113213211............................ ............?? 10000 번째 줄 출력하기
개미수열
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을 만들거나
개미수열 복잡도
50번째 줄을 출력하는 과정 살펴보기
맨 아랫줄에 빨간색 칸은 그 위치의 값을 계산하려고 suspen되었음을 보여주고
그 앞줄/그 앞줄… 도 suspend
그러다 값이 계산되면 차례로resume/resume/resume될 것
어느 한 순간에는 하나의 Coroutine만 Active!
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하는 이유가 두가지(요청/생성)
resume/yield
• 우리가 가진 건 서브루틴 뿐
– call/return
• return할 내용
– yield하는 이유(값 요청? 전달?)
– 다시 resume할 위치
• call할 때 전달할 내용
– resume할 위치
– 요청 값
• 코루틴들을 organize할 dispatcher 함수 필요– 각 코루틴들의 상태를 기억(스택 변수는 쓸모없음)
• 심지어 C로도 가능하다
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 위치를 반환하는 방법도 있음
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
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
개미수열 복잡도
• 공간 복잡도
– O(n)
• 시간 복잡도
– n번째 줄 m번째 글자까지 출력
– O(n + m log m)
C/Coroutine 검토
• No abstraction
– 중복 코드를 제거할 수도 없다
– 함수를 분리할 수도 없다
Coroutine vs. Continuation
• Coroutine– Thread 이용
• 실제로 pause/resume
– resume pointer• Continuation을 반환하는 것으로 이해할 수 있음
• Continuation– ptr 반환후 resume할 때 jump 하는 대신
– 다음 실행할 continuation을 closure로 반환
– resume은 continuation을 호출하는 것
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을 전달하고 다음 실행할내용은 없다
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 출력 후 새로 읽은 글자로 반복
JS/CPS 검토
• write2 같은 추상화 가능
• Callback Hell
– 흐름을 추적하기 어려움
– Promise같은 CPS 추상화 필요
function write2(a, b, cont) {return write(a, () => write(b, cont))
}
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) => ...)
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) }
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))})
}}
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)
Recap
• hand-written Coroutine
• JS/Continuation passing style
• CPS 추상화
무한 수열
• 더 일반적인 방법
– Stream/Lazy list
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]
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))
}
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))
}
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));
}
Recap
• for-loop, regex
• list processing
– stream(lazy list)도 마찬가지
• iterator/generator/coroutine
– Thread로 흉내내기
– Coroutine의 본질 – resume ptr
• Continuation passing
– CPS 추상화 : Interperter 패턴