Rust의 반복문

Java나 C++ 같은 언어에서는 조건 반복문 과 for-each 반복문 에 같은 for 키워드를 사용한다. 하지만, Rust 는 조건 반복문에는 while 키워드 을 for-each 반복문에는 for 키워드를 사용한다. Rust는 여기에 하나의 반복문을 더 제공한다. loop 반복문이다. 이는 while true 라고 쓰는 것과 같이 같은 코드를 무한히 실행한다. 실제로 무한 반복이 필요한 경우에도 사용되고, 반복문의 조건을 하나의 표현식으로 서술하기 힘들어 break 문으로 뺄 때에도 사용된다. 하지만 loop 가 while true 와 완전히 같은 코드는 아니다. loop 는 while 문과 다르게 그 자체로 값을 가진다. break 문 뒤에 값을 적으면 이 값이 반복문 전체의 값이 된다. 그렇다면 다른 반복문은 값을 가지지 않는데 loop 문만 값을 가지는 이유는 무엇일까? 일반적으로 반복문이 끝나는 데는 두 가지 조건이 있다. 주어진 조건이 끝나는 것과 break 문을 만나는 것이다. 평범한 반복문에서도 같은 문법을 써서 break 문을 만났을 때의 값은 정하게 할 수는 있지만, 조건이 끝나 반복문이 종료되는 경우 값을 지정할 수 없다. 그래서 일반적으로 반복문은 값을 가지지 않는다. 하지만 loop 문은 종료 조건이 없기 때문에 끝나기 위해서는 항상 break 문을 만나야 한다. 이런 특징 덕분에 loop 는 다른 반복문과 다르게 구문 자체가 값을 가질 수 있다. Rust에서는 for-each 반복문을 for-in 반복문이라고 부른다. 실제로 for value in values 라고 코드를 작성하기 때문이다. 그렇다면 여기서 values 에 들어갈 수 있는 값은 어떤 값일까? 내가 만든 타입을 for-in 반복문에 사용하고 싶으면 어떻게 해야 할까? 값을 순회할 수 있는 타입은 모두 for-in 반복문에 사용할 수 있다. Rust에서는 Iterator 가 순회할 수 있는 타입을 의미한다. 다시 말해 for-in 반복문은 Iter...

조건 반복문과 for-each 반복문

프로그램에서 반복문은 크게 두 가지로 나눌 수 있다. 하나는 특정 조건을 만족할 때까지 같은 코드를 반복하는 것이다. 포트란으로부터 시작해서 C를 거쳐 대부분의 프로그래밍 언어들은 기본적으로 이런 형태의 반복문을 제공한다. 예를 들면 C의 while 문이나 for 문이 그렇다. 이것들은 주어진 조건이 성립하는 동안 정해진 코드를 실행한다. 혹은 특정 범위의 모든 값에 대해 같은 코드를 실행하기도 한다. 이런 반복문은 C++에서는 range-based for loop , C#과 Java는 foreach 문 , JavaScript는 for ... of 문으로 부르는 등 다양한 이름으로 불린다. 이런 종류의 반복문을 부르는 이름은 언어마다 다르지만, 편의를 위해 이 글에서는 이것을 for-each 문으로, 임의의 조건을 걸 수 있는 반복문을 조건 반복문으로 부르도록 하겠다. for-each 반복문은 사실 조건 반복문을 특정한 조건에서만 사용할 수 있도록 특수화시킨 것으로 볼 수도 있다. 실제로 C++의 range-based for loop나 Rust의 for loop 는 조건 반복문의 syntactic sugar로 설명하기도 한다. C++이나 Java 등의 언어는 원래 for-each 반복문이 없었다. 그래서 조건 반복문을 이용해야 했기 때문에 off-by-one-error 같은 실수가 종종 발생했다. 이에 대한 해결책이 없었던 것은 아니다. 예를 들어 Lisp 같은 언어는 이미 for each 반복문을 가지고 있었다. 하지만 언어 스펙을 변경하는 것은 큰일이기 때문에 많은 언어들이 새 문법을 추가하지 않고 해결하고자 했다. C++의 이터레이터를 이용하는 algorithm 라이브러리 가 이런 경우다. 하지만 이런 해결책은 함수를 인자로 받는 higher-order function 을 쓰기 때문에 for 문을 쓸때보다 사용하기 불편하거나 코드를 읽기 어려워지는 경우가 있는 것은 사실이었다. 그래서 Java, C++, JavaScript ...

[Rust] 함수의 lifetime parameter는 언제 써야 하고 언제 생략할 수 있나요?

누군가가 나에게 러스트 가 다른 언어와 다른 가장 큰 차이가 뭐냐고 묻는다면, 라이프타임이라고 대답할 것이다. 그래서 러스트를 처음 배우는 사람들이 러스트의 적응하는데 가장 어려운 부분도 이것이라고 생각한다. 특히 함수의 경우 라이프타임을 표기하는 파라미터는 생략이 가능해서 이 생략 규칙에 익숙하지 않은 사람은 함수가 의미하는 것을 이해하지 못하기도 한다. 일단 함수를 정의할 때는 크게 신경 쓰지 않아도 된다. 러스트는 분석 도구가 매우 잘 만들어져 있다. 지금까지 나온 어떤 언어보다 잘 돼 있다고 말하고 싶을 정도다. 어떤 라이프타임을 생략할 수 있는지 확실치 않은 경우는 모든 레퍼런스에 파라미터를 적어주고 clippy 를 사용하면 어떤 파라미터가 필요 없는지 친절하게 알려준다. 이에 따라 코드를 다듬으면 된다. 문제는 다른 사람이 작성한 함수를 읽을 때이다. 라이프타임에 따라 함수의 의미가 달라진다. 때문에 라이프타임을 정확히 파악하지 못하면, 코드를 잘못 이해하는 경우가 생긴다. 생략된 라이프타임 파라미터가 어떤 값이 되는가는 생략된 라이프타임이 함수의 인자에 사용됐는가 결괏값에 사용됐는가에 따라 다르다. 우선 입력에 사용된 라이프타임이 생략된 경우는 간단하다. 전부 다른 파라미터로 생각하면 된다. 다시 말해서, 입력에 사용된 라이프타임은 다른 라이프 타임과 관계없다면 모두 생략할 수 있다. std::cmp::Ord::cmp(&self, other: &Self) -> Ordering 가 2개의 레퍼런스 타입을 받지만, 라이프타임이 명시되지 않은 이유가 이 때문이다. 결괏값에 사용된 라이프타임을 생략하는 것은 이보다 약간 더 복잡하다. 우선 출력에 사용된 라이프타임은 특정한 두 경우만 생략될 수 있다. 첫 번째 경우는 함수의 인자에 단 하나의 라이프타임만 사용된 경우이다. 함수의 인자에 라이프타임 파라미터가 하나만 사용된 경우, 결괏값에 생략된 라이프타임 파라미터는 전부 인자의 라이프타임으로 간주한다. 예를 들어 std...

일정 예상은 왜 실패할까?

모든 일에서 데드라인을 지키는 것은 중요하다. 마감을 지키는 것은 일의 기본이다. 특히 팀으로 작업하는 일에서는 더더욱 그렇다. 소프트웨어 개발의 많은 일은 파이프라인 형식으로 진행되기 때문에, 내가 약속한 일정을 못 맞추면, 내 일만 뒤로 밀리는 것이 아니라 협업 중인 다른 사람의 일정에까지 영향을 주는 경우가 많다. 하지만 일정을 맞추는 것은 어렵다. 그게 뭐가 어렵냐고 생각할 수 있는데 정말 어렵다. 언제나 이번만큼은 예외라고 생각하지만, 언제나 일정은 틀어진다. 심지어 예상보다 오래 걸릴 것이라는 경험을 바탕으로 계산보다 일정을 길게 잡아도 여전히 마감을 못 지킨다. 이런 현상을 호프스태터의 법칙 이라고 부른다. 그렇다면 호프스테터의 법칙은 왜 생길까? 일정 예측이 틀리는 이유를 알아보기 전에 우선 짚고 넘어갈 것이 있다. 어떤 프로젝트는 일정을 맞출 수 없다. 애초에 부족한 시간이 주어진 경우가 그렇다. 아쉽게도 많은 프로젝트에서 일정은 기술적 요인만으로 결정되지 않고, 다른 사업적 이유로 결정된다. 이런 경우는 답이 없다. 하지만 이런 것은 "프로젝트가 * 같았다"라고 말하지 예측이 실패했다고는 하지 않는다. 이 글에서 말하는 일정 예상이 틀렸다고 말하는 것은 기술적 요인을 충분히 고려하여 일정을 잡았어도 예상한 경우보다 많이 걸리는 경우를 말한다. 애초에 이루어질 수 없는 스케줄을 주고 일정 예측에 실패했다고 말하는 것은 그냥 양심 없는 짓이다. 일정이 틀린 이유를 물어보면 다양한 답이 나온다. 과거에 작업했던 코드에서 버그가 있어 수정하느라 늦었다고 하는 경우도 있고, 스펙이 완전치 않아 스펙을 보완하느라 늦어졌다고 하기도 한다. 레거시 코드를 다루는 경우 기존 코드를 완전히 이해하지 못한 채로 시작을 해 늦어졌다고 하기도 한다. 어떤 경우는 구현해야 할 기능에 대한 지식이 부족하여 일정을 잘못 예상했다고 대답하는 경우도 있다. 이 모든 것들은 일정 예측이 틀리는 타당한 이유이긴 하지만 근본적인 이유는 아니다. 만...

[TypeScript] Promise.all에는 인자 개수 제한이 없다

트위터에서 이상한 글을 봤다. TypeScript 가 선언한 Promise.all 이 iterable 을 받지 않고 인자 개수 제한이 있다는 것이다. 근데 그럴 리가. TypeScript쯤 되는 프로젝트에서 무언가가 스펙과 다른 것으로 보인다면, 코드를 잘못 이해했을 확률이 높다. 코드를 다시 확인해보자. 당연히 iterable 을 인자로 받는 Promise.all 선언 도 있다. 다만, 이 선언이 es2015.promise.d.ts 가 아닌 es2015.iterable.d.ts 에 있을 뿐이다. 타입스크립트 컴파일러 옵션 중 --lib 으로 사용할 라이브러리를 지정할 수 있다. 대부분 es2015 , es2018 같은 식으로 버전을 지정하여 포함하는 것이 일반적이라 잘 알려지지 않았지만, es2015.symbol 로 Symbol 에 대한 선언만 포함하거나, es2015.promise 로 Promise 에 대한 선언만 포함하는 것이 가능하다. 각 라이브러리는 서로 간에 의존성이 없도록 작성돼 있으며, 특히 iterable 에 관련된 선언은 전부 es2015.iterable 에서 선언한다. 예를 들어 Map 이나 Set 은 es2015.collection 에 선언돼 있지만, iterable 을 반환하는 entries , values , keys 등의 선언은 전부 es2015.iterable 에 선언돼 있다. 마찬가지로 iterable 을 인자로 받는 Promise.all 함수도 es2015.iterable에 선언돼있다. iterable 을 받는 Promise.all 함수 선언은 es2015.iterable 에 선언돼 있는데, es2015.promise 에 10개가 더 선언돼있다. 이들은 무엇일까? 사실 이 함수들은 배열을 인자로 받는 함수가 아니라 tuple 을 인자로 받는 함수 선언이다. JavaScript 에는 임의 개수의 원소를 가지는 배열은 있지만, 고정된 개수의 원소를 가지는 tuple은 없다. 그래서 tuple이 필요할 때 배열을 사...

[Rust] 타입 변환하기

Rust에서 타입 변환은 특별한 것이 아니다. 그저 단순히 하나의 값을 소유권을 받아 다른 타입의 값을 반환하는 함수다. 따라서 자신이 원하는 이름으로 아무 함수나 만들면 된다. 하지만 가독성을 위해 as_ , to_ , into_ 를 prefix로 사용 하는 method를 만들거나, from_ 가 prefix로 붙는 생성자를 만들어 converting constructor처럼 만들어 사용한다. From 타입 변환을 보다 일반적으로 구현하고 싶으면 From 트레잇을 구현하면 된다. 예를 들어 A 라는 타입을 B 라는 타입으로 변환하고 싶을 때는, B 타입에 From<A> 를 구현하는 것이다. From 트레잇은 from 이라는 associated function 을 제공하기 때문에 A 타입의 변수 a 를 B 타입으로 변환시킬 경우 From::from(a) 이라는 식으로 사용한다. 만약, 컴파일러가 타입 추론을 못 해주는 경우 변환할 타입을 명시적으로 적어 B::from(a) 이라는 식으로 사용하면 된다. Into From 트레잇은 변환될 타입의 associated function을 제공한다. 덕분에 변환될 타입을 명시할 수 있다. 이는 From 트레잇의 장점이지만, 구체적인 타입을 많이 적을수록 코드의 범용성을 떨어뜨린다. 범용성을 떨어뜨리지 않기 위해 From::from(a) 와 같은 식으로 사용할 수도 있지만, from 은 associated function이기 때문에 a.from() 같은 식으로 호출할 수는 없다. 이 경우 Into 트레잇이 유용하게 사용된다. From 트레잇이 변환될 타입의 associated function을 제공하는 반면, Into 는 into 라는 method를 제공한다. 따라서 A 타입에서 B 로 가는 Into 트레잇을 구현하고 있으면, A 타입인 변수 a 가 있을 때, a.into() 같은 식으로 변환 함수를 호출할 수 있다. 물론, A::into(a) 나 Into::into(a) ...

Garbage collection과 memory leak

Garbage collection (a.k.a. GC)은 프로그램이 더 이상 접근할 수 없는 메모리를 자동으로 해제시켜 주는 기술을 의미한다. John McCarthy 가 Lisp에 처음 구현한 이후 많은 언어가 사용하여 현대 프로그래머 중에 모르는 사람이 없다고 해도 좋을 정도로 널리 알려진 개념이다. 근데 이 GC에 대해서 사람들이 자주 착각하는 것이 있다. GC를 사용하는 이유가 memory leak 을 잡아주기 위해서라고 생각하는 것이다. 만약 이렇게 생각했다면 GC에 대해 큰 착각을 하는 것이다. GC는 memory leak을 막지 못한다. 사실 튜링 완전 한 언어에서 memory leak을 막아주는 방법은 세상에 존재할 수 없다. 이것을 설명하기 위해서는 우선 memory leak이 무엇인지 알아야 한다. Wikipedia 는 memory leak을 다음과 같이 설명한다. a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released. 쉽게 설명해 memory leak이란, 사용하지 않을 메모리를 해제하지 않는 현상이다. 결국 memory leak을 잡기 위해서는 사용하지 않는 메모리를 찾아내는 것이 먼저다. 그리고 사용하지 않는 메모리를 완벽하게 찾아내는 건 불가능하다. 자세한 설명은 아래 코드를 통해서 하도록 하겠다. 위 코드에서 x 는 언제 해제해야 할까? 보통은 use(x) 가 끝난 뒤 해제해야 한다고 생각할 것이다. 하지만 some_function 이 종료되지 않는 함수라면 어떨까? some_function 이 내부에서 무한 루프를 돌고 절대 종료되지 않는 함수라면 use(x) 는 호출될 일이 없다. 따라서 x 는 앞으로 접근할 일이 없는 메모리고, some_function 이 실행되고 있는...

이 블로그의 인기 게시물

USB 2.0 케이블의 내부 구조

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Web] SpeechSynthesis - TTS API

[Python] cache 데코레이터로 최적화하기