이진 데이터를 다루는 것은 어떤 언어에서나 중요한 개념이에요. Elixir는 이진 데이터를 작성하고, 매칭하고, 구성할 수 있는 우아한 문법을 제공해요.
Elixir에서 이진 데이터는 비트스트링 타입이라고 불러요. 이진 데이터 타입 은(일반적인 이진 데이터와 혼동하면 안 돼요) 비트스트링의 한 가지 특별한 형태인데, 이건 나중 연습 문제에서 다뤄요.
비트스트링 리터럴은 <<>>라는 비트스트링 특수 형식으로 정의해요. 비트스트링 리터럴을 정의할 때는 여러 세그먼트로 나눠서 정의하는데, 각 세그먼트는 값과 타입을 가지고, 둘은 :: 연산자로 구분해요. 타입은 값을 인코딩하는 데 몇 비트를 사용할지 지정해요. 타입은 완전히 생략할 수 있는데, 그러면 기본값으로 8비트 정수가 돼요.
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
타입을 ::1로 지정하는 건 ::size(1)을 쓰는 것의 축약형이에요. 비트 크기가 변수에서 온다면 더 긴 문법을 사용해야 해요.
이진 정수 리터럴을 쓸 때는 리터럴 앞에 0b를 붙여서 2진법 표기로 바로 쓸 수 있어요. 참고로 이런 값은 테스트 결과에 출력되거나 iex에서 사용할 때 어차피 10진수로 표시돼요.
<<0b1011::4>> == <<11::4>>
# => true
세그먼트의 값이 그 세그먼트 타입의 용량을 넘으면, 값은 왼쪽에서부터 잘려 나가요.
<<0b1011::3>> == <<0b0011::3>>
# => true
특수 형식을 사용하면 기존 비트스트링의 앞에도, 뒤에도 붙일 수 있어요. 크기를 모르는 비트스트링이라면 ::bitstring 타입을 사용해야 해요.
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
특수 형식을 사용하면 변수에 저장된 비트스트링들을 이어 붙일 수 있어요. 크기를 모르는 두 비트스트링을 이어 붙일 때는 ::bitstring 타입을 사용해야 해요.
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
패턴 매칭을 사용하면 특수 형식에서 값을 꺼낼 수도 있어요. 값을 가져오려는 각 조각마다 비트 수를 알아야 하는데, 예외가 하나 있어요. ::bitstring 타입은 크기를 모르는 비트스트링에 패턴 매칭할 때 쓸 수 있지만, 이건 마지막 조각에만 사용할 수 있어요.
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
비트스트링은 (테스트 러너나 iex에서) 만들어질 때 사용한 형식과 다른 형식으로 출력될 수 있어요. 비트스트링을 배울 때 자주 혼란을 일으키는 부분이에요.
기본적으로 비트스트링은 8비트(1바이트) 단위 조각으로 표시돼요. 다른 크기의 조각으로 만들었더라도 마찬가지예요.
<<2011::11>>
# => <<251, 3::size(3)>>
출력 가능한 UTF-8 인코딩 문자열을 나타내는 비트스트링을 만들면, 문자열로 표시돼요.
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
숫자로 색칠하기(또는 색으로 칠하기)는 색칠을 하기 위한 흑백 그림이에요. 그림의 각 영역에는 서로 다른 숫자가 적혀 있고, 그 숫자는 미리 정해진 색상 팔레트의 특정 색에 대응해요. 목표는 각 영역을 알맞은 색으로 채워서, 마지막에는 아름다운 색채 그림을 완성하는 거예요. 아이든 어른이든 누구나 즐길 수 있는 편안한 활동이에요.
이제 Elixir로 숫자로 색칠하기 앱을 만들어야 해요. 앱에서 직접 정의한 데이터 형식으로 그림을 가져오고 내보낼 수 있으면 좋겠어요. 그래서 그림 데이터를 저장할 때는 바이너리 파일을 사용하기로 했어요.
이 연습 문제는 이진수에 익숙하고, 이진수를 십진수로, 십진수를 이진수로 바꾸는 원리를 이해하고 있다고 가정해요.
아래와 같은 스마일 그림이 있다고 상상해 봐요. 그림의 배경은 흰색이고, 스마일에는 검은색 테두리와 노란색 채우기 색이 있어요.
이 그림은 색을 3개 사용해요. 이 색들에 다음과 같이 인덱스를 부여한다고 해봐요:
0 (이진수: 0b00),1 (이진수: 0b01),2 (이진수: 0b10).이제 이 색 인덱스로 각 픽셀의 색을 표현할 수 있어요.
| 스마일 | 색 인덱스를 표시한 스마일 |
|---|---|
![]() |
![]() |
PaintByNumber.palette_bit_size/1 함수를 구현해 보세요. 이 함수는 팔레트의 색 개수를 인자로 받아서, 그만큼의 색 인덱스를 이진수로 표현하는 데 필요한 비트 수를 반환해야 해요. 색 인덱스는 항상 0부터 시작하고, 연속해서 증가하는 정수예요.
예를 들어, 13가지 색을 표현하려면 4비트가 필요해요. 4비트로는 색 인덱스를 최대 16개(2^4)까지 저장할 수 있어요. 3비트로는 부족한데, 3비트로는 색 인덱스를 최대 8개(2^3)까지만 저장할 수 있기 때문이에요.
PaintByNumber.palette_bit_size(13)
# => 4
참고: Elixir 표준 라이브러리에는 log2 함수가 없어요. 나중에 Elixir에서 Erlang 라이브러리를 사용하는 방법을 배우게 되는데, 거기에서 이 함수를 찾을 수 있어요. 지금은 대신 재귀와 거듭제곱 연산자 **로 이 문제를 풀어 보세요.
PaintByNumber.empty_picture/0 함수를 구현해 보세요. 빈 비트스트링을 반환해야 해요.
미리 정의된 테스트 그림은 앱의 기본 기능을 직접 테스트할 때 사용돼요. 테스트 그림은 서로 다른 색 4개로 이루어진 픽셀 4개로 구성돼요.
PaintByNumber.test_picture/0 함수를 구현해 보세요. 4개의 세그먼트로 이루어진 비트스트링을 반환해야 해요. 각 세그먼트의 비트 크기는 2여야 하고, 세그먼트의 값은 0, 1, 2, 3이어야 해요.
PaintByNumber.prepend_pixel/3 함수를 구현해 보세요. 이 함수는 세 개의 인자를 받아요. 픽셀을 앞에 추가할 그림이 담긴 비트스트링, 팔레트의 색 개수, 새 픽셀의 색 인덱스예요. 앞에 새 픽셀이 추가된 그림을 담은 비트스트링을 반환해야 해요.
picture = <<2::4, 0::4>>
color_count = 13
pixel_color_index = 11
PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index)
# => <<178, 0::size(4)>>
# (which is equal to <<11::4, 2::4, 0::4>>)
PaintByNumber.get_first_pixel/2 함수를 구현해 보세요. 이 함수는 두 개의 인자를 받아요. 읽어들일 그림이 담긴 비트스트링과 팔레트의 색 개수예요. 주어진 그림의 첫 번째 픽셀의 색 인덱스를 반환해야 해요. 빈 그림이 주어지면 nil을 반환해야 해요.
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
PaintByNumber.drop_first_pixel/2 함수를 구현해 보세요. 이 함수는 두 개의 인자를 받아요. 픽셀을 제거할 그림이 담긴 비트스트링과 팔레트의 색 개수예요. 첫 번째 픽셀이 없는 그림을 반환해야 해요. 빈 그림이 주어지면 빈 그림을 반환해야 해요.
picture = <<2::3, 5::3, 5::3, 0::3>>
color_count = 6
PaintByNumber.drop_first_pixel(picture, color_count)
# => <<180, 0::size(1)>>
# (which is equal to <<5::3, 5::3, 0::3>>)
PaintByNumber.concat_pictures/2 함수를 구현해 보세요. 두 개의 인자, 즉 두 개의 비트스트링을 받아요. 두 번째 인자 앞에 첫 번째 인자를 붙인 결과를 담은 비트스트링을 반환해야 해요.
picture1 = <<52::6, 51::6>>
picture2 = <<0::6, 34::6, 12::6>>
PaintByNumber.concat_pictures(picture1, picture2)
# => <<211, 48, 34, 12::size(6)>>
# (which is equal to <<52::6, 51::6, 0::6, 34::6, 12::6>>)
Exercism에 가입하고 Elixir 트랙을 개념 58개연습 문제 168개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.