escape codes의 이해

escape code의 ISO 6429 에서 정의된 정확한 명칭은 control function이다. control function을 표현하는 코드 혹은 시퀀스를 흔히 escape code라고 말한다. ISO 6429 가 정의하는 컨트롤 코드는 모양에 따라 C0 코드와 C1 코드로 나누어진다. C0 코드는 ASCII Table에서 non-printing 문자에 해당하는 코드이다. 아마 Escape Codes 중 개발자들에게 가장 익숙한 코드일 것이다. 라인 피드( \n ), 캐리지 리턴( \r ), 탭( \t ), 널 문자( \0 ) 등이 여기에 해당한다. C1 코드는 0x80 에서 0x9F 사이 32개의 1바이트 값으로 표현되는 코드다. C0와 다르게 C1 코드는 ASCII 에 정의된 값이 아니다. ASCII 만 지원하는 터미널, 다시 말해 7비트 환경에서만 사용할 수 있고, 8비트 환경인 현대 터미널에서는 사용될 수 없다. 그래서 현대 터미널은 대부분 C1 코드가 필요할 때 escape sequence로 표현한다. escape sequence란 말 그대로 ESC 를 시작으로 하는 일련의 문자열을 의미한다. 그중 C1 코드와 같은 기능을 하는 시퀀스는 첫 바이트가 ESC ( 0x1B )이고 두 번째 바이트가 @ ( 0x40 )에서 _ ( 0x5F )로 이루어지는 두 바이트의 시퀀스다. 예를 들면 IND 는 8비트 환경에서는 0x84 로 표현되지만 7비트 환경에서는 ESC D ( 0x1B 0x44 )로 표현된다. 이 두 번째 바이트는 Final character of Escape sequence 라는 의미로 Fe 라고 불리며, 두 바이트로 표현된 C1 코드를 Fe sequence 라고 부르기도 한다. ISO 6429 가 정의하는 escape sequence는 Fe sequence 이외에도 더 있다. 예를 들면 ` ( 0x60 )에서 ~ ( 0x7E ) 사이의 값은 Fs 라고 불리며, ESC 다음 Fs 가 따라오는 시퀀스는 Fs sequenc

Escape Codes의 역사

이미지
개발을 위해 컴퓨터에 리눅스를 설치할 때 반드시 설치하는 프로그램이 있다. sl 이라는 프로그램이다. 이게 무슨 프로그램이냐면 커맨드 창에서 sl 을 치면 기차를 보여주는 프로그램이다. 실용성 있는 프로그램은 아니고, 터미널에서 많이 사용되는 ls 명령어의 오타를 냈을 때 화면에 기차를 보여줘 생각할 시간을 주는 프로그램이다. 이를 통해 오타를 낼 정도로 마음이 급한 상황에서 다른 실수를 하지 않고 조금은 침착해지라는 의미에서 항상 설치한다. 출처: https://github.com/mtoyoda/sl 터미널은 실행한 프로그램이 내보내는 두 출력 stdout 과 stderr 를 받아 화면에 보여주는 프로그램이다. stdout 과 stderr 은 순차적인 출력이다. 보통의 출력은 왼쪽 위에서 오른쪽 아래로 흘러간다. sl 처럼 이미 쓰인 화면에 새로운 글자를 그리기 위해서는 특별한 방법이 필요하다. 이 특별한 방법은 escape codes 라고 한다. escape codes는 터미널에 정의된 일종의 약속이다. 이 약속은 현재는 ISO 6429 에서 정의하는 표준을 따른다. 하지만 과거에는 통일된 규정이 없이 터미널 모델마다 다른 규정을 가지고 있었다. 과거의 컴퓨터를 모르면 터미널 모델마다 다르다는 것이 이해되지 않을 수도 있다. 이를 알기 위해서는 컴퓨터의 역사를 알아야 한다. 지금은 터미널이라고 하면 CLI 를 위한 애플리케이션을 의미한다. 하지만 과거 터미널은 문자 그대로 컴퓨터의 최종 단말기였다. 이 단말기는 메인프레임 컴퓨터 에 연결돼 메인프레임 컴퓨터의 입출력을 담당했다. 현대의 터미널 앱이 터미널 에뮬레이터 라고 불리는 이유가 이 터미널을 에뮬레이트 하는 것이기 때문이다. 과거 터미널은 ADM-3A , 지금도 유명한 IBM 사의 IBM 2260 과 IBM 3270 , 후에 HP에 인수되는 DEC 사의 VT 시리즈 등 다양한 회사에서 개발됐고, 이들은 각각 고유의 표준을 가지고 터미널을 조정했다. 출처: https://en.wik

[Rust] 반복자에게 할 일 더해주기 - Iterator adapters

다른 Iterator (a.k.a 반복자)를 받아 새로운 반복자를 반환하는 함수를 iterator adapter 라고 부른다. adapter라는 이름은 GoF의 디자인 패턴 중 하나인 adapter pattern 에서 온 말이라고 한다. 그런데 실제로는 adapter pattern보다는 decorator pattern 에 해당하기 때문에 이름을 신경쓰면 용도를 헷갈릴 수 있다. 그러니 이름에 대해서는 크게 신경 쓰지 않는 것이 좋다. 이름에 대한 불평은 그만하고 iterator adapter가 무엇을 하는가? iterator adapter는 반복자가 순회하면서 할 일을 더해준다. 무슨 말인지는 실제 구현체를 보면 쉽게 이해할 수 있을 것이다. map 함수는 대표적인 adapter다. 함수형 언어를 써본 사람이라면 많이 익숙할 map 함수가 반환하는 반복자는 기존의 값을 새로운 값으로 변환한 값을 순회한다. 이 외에도 표준 라이브러리에 이미 다양한 adapter가 구현돼있다. 이중 가장 많이 사용되는 것은 반복문과 같이 사용하기 좋은 adapter들이다. 조건을 만족하는 값만 돌려주는 filter , 지정 된 몇 개의 값만 반환하는 take , 반대로 몇 개의 값은 생략하고 반환하는 skip , 몇 개씩 값을 건너뛰며 순환하는 step_by 와 같은 adapter가 대표적인 예시다. 이런 adapter를 반복문과 함께 사용하면 복잡한 조건 처리를 쉽게 표현할 수 있다. 이 외에도 영원히 종료하지 않고 순환하게 만드는 cycle 이나, 순환하는 값에는 변환을 주지 않고, 사이드이펙트를 발생시키기만 하는 inspect 도 많이 사용된다. adapter를 사용할 때 주의해야 할 점이 하나 있다. adapter가 반환하는 것 역시 반복자일 뿐이다. 실제로 반복자를 사용하기 전에는 adapter가 지정한 일을 수행하지 않는다. 쉽게 말해 실제로 값이 필요할 때까지 실행을 미루는 lazy evaluation 을 한다는 것이다. 따라서 아래와 같은

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

최적화는 귀찮다. 눈에 띄는 실수를 한 게 아니면 어떻게 고쳐야 할지 감이 오지도 않고, 대부분의 최적화는 가독성을 떨어뜨리기 때문에 버그가 발생할 확률이 늘어난다. 하지만 어떤 최적화 테크닉은 코드를 크게 수정하지 않고 큰 성능 향상을 가져온다. 메모이제이션 이 그 대표적인 예제다. 계산이 무겁거나, 디스크의 값을 읽거나, 네트워크 통신처럼 근본적으로 시간이 오래 걸리는 일은 그 실행 결과를 저장했다 재사용하는 것만으로 큰 성능향상을 가지고 온다. 파이썬은 메모이제이션을 쉽게 적용할 수 있는 데코레이터 를 제공한다. functools 모듈의 lru_cache 데코레이터 가 이것이다. 이 데코레이터를 붙이면 함수의 실행 결과를 캐싱해준다. 캐시의 크기는 maxsize 로 지정할 수 있다. 저장할 실행 값이 이 개수를 넘어가는 경우 LRU 알고리즘 에 따라 가장 오래전에 사용한 결과를 지우고 새 값을 캐싱한다. lru_cache 를 사용하면 쉽게 최적화할 수 있지만 아무 함수에나 사용할 수 있는 건 아니다. 함수의 인자를 캐시키로 사용하기 때문에 함수의 실행 결과가 함수의 인자 이외에 다른 요소에 의존적인 함수에는 사용하지 못한다. 즉, 랜덤 요소가 들어가거나 시간에 따라 결괏값이 변하는 함수에는 사용하면 안 된다. 결정성이 보장되는 함수에만 사용할 수 있다는 것은 모든 캐시의 공통적인 특성이다. 여기에 더해 파이썬이 제공하는 lru_cache 는 그 구현상의 문제로 한 가지 제약이 더 있다. 이 데코레이터는 값을 저장하기 위해 인자를 키로 가지는 dictionary 를 사용한다. 따라서 모든 인자가 hashable 타입이어야 한다. 다시 말해 mutable 하지 않은 dictionary, set, list 등을 인자로 받는 함수는 이 데코레이터를 사용해 캐싱할 수 없다. 이런 타입을 인자로 받던 함수는 그 인자를 frozenset 이나 tuple 같은 immutable 타입으로 변환해야 한다. 게다가 keyword argument 를

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

이 블로그의 인기 게시물

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

RAII는 무엇인가

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

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

[Web] SpeechSynthesis - TTS API