Code

Elixir – 02: Base Type

February 1, 2016

author:

Array

Elixir – 02: Base Type

Elixir Tutorial 시리즈입니다. 거의 대부분은 튜토리얼의 한글 번역에 가깝습니다만, 생략되거나 추가로 주석을 달거나 하는 부분이 많습니다. 원문은 최하단의 링크를 참고하세요.

Elixir – 02: Basic types

기본 타입에 대해서.

Basic arithmetic

iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0

Elixir에서 /는 언제나 float를 반환합니다. 이를 원하지 않는 경우에는 div나, rem을 사용해주세요.

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

함수 호출시에 괄호는 쓰지 않아도 좋습니다.

Booleans

iex> true
true
iex> true == false
false

Elixir는 당연하지만 진리값도 지원합니다. 더불어 여러가지 확인 함수들을 제공합니다. 예를 들어, is_boolean/1처럼요.
…/1? 어디서 많이…본………Pro…..l…..o……g………
설명을 추가로 확인해 보았더니, arity를 사용해서 메소드 시그니처를 구분합니다. 루비랑은 다르네요 😉 이쯤 되니, 확실히 함수형 스타일이기는 하구나, 싶구요.

더불어서 진리값의 실체는 아래서 설명할 아톰. 편의성을 위한 shortcut이 아닌가 추측 중.

TIP: h 헬퍼를 사용하면 iex에서 도움말을 볼 수 있습니다. 예를 들면,

iex> h is_integer/1
  def is_integer(term)                           
Returns true if term is an integer; otherwise returns false.
Allowed in guard tests. Inlined by the compiler.

이런식으로 말이죠. 보다보면 예제가 있는 함수도 많네요.

Atoms

아톰은 이름 자체로 상수인 값입니다. 루비에서는 이런걸 심볼이라고 하죠. 표현식도 같습니다.

iex> :hello
:hello
iex> :hello == :world
false

Strings

  • 쌍 따옴표를 사용합니다.
  • UTF-8 기본
  • 보간 사용 가능 – 루비와 같은 방식입니다.
iex> "hellö #{:world}"
"hellö world"
  • 루비와 마찬가지로 개행을 인식합니다.
iex> "hello
...> world"
"hellonworld"
iex> "hellonworld"
"hellonworld"

IO 모듈에서는 많이 보던 애를 사용해서 화면 출력을 할 수 있습니다.

iex> IO.puts "hellonworld"
hello
world
:ok

특이한 부분은 IO.puts/1:ok를 반환한다는 것.

문자열은 내부적으로 바이트로 관리됩니다. 그래서 바이트 길이를 확인할 수도 있구요.

iex> is_binary("hellö")
true
iex> byte_size("hellö")
6

재밌는 점은 바이트 길이가 6이라는 점입니다. 이는 UTF-8에서 ö가 2바이트로 처리되기 때문입니다. 실제 문자열의 길이를 얻고 싶다면, String.length/1를 쓰면 됩니다.

iex> String.length("hellö")
5

String 모듈에는 다양한 유틸리티 함수가 있답니다 🙂

Anonymous functions

함수는 fnend를 사용해서 선언합니다.

iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> is_function(add)
true
iex> is_function(add, 2)
true
iex> is_function(add, 1)
false
iex> add.(1, 2)
3

그리고 일급 객체입니다. 예제에서는 is_function에 대해서 다른 반응을 보여주고 있는데, 이는 함수명과 arity를 포함하여 하나의 메소드 시그니쳐가 되기 때문. 잘 보면 arity가 2면 참이고, 1이면 거짓임을 알 수 있습니다.

익명함수를 호출할 때에 함수명과 (의 사이에 있는 .가 필요하다는 점을 기억하세요.

익명함수는 클로저이며, 자신이 생성된 스코프를 캡쳐합니다. 그러므로 다음과 같이 작성할 수도 있습니다.

iex> add_two = fn a -> add.(a, 2) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> add_two.(2)
4

함수 내부의 변수는 외부의 환경에 영향을 주지 않습니다.

iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42

이 부분은 약간 생소한 것이, 말인즉슨 스코프 체인이 성립하지 않는다…는 건데, 캡쳐한 이후로 주변 환경의 정보는 더이상 영향을 미치지 않는것인가…? 라는 의문이. 그래서 이런 테스트 코드를 작성해 보았음.

iex> x = 42
42
iex> (fn -> IO.puts x end).()
42
iex> x = 30
30
iex> (fn -> IO.puts x end).()
30

멀쩡하게 참조는 잘 끌어옵니다. 매번 실행시점에서 새로 환경을 참조하던가, 별도의 방법으로 변수를 관리하는 것이 아닐까, 추정…

(Linked) Lists

[]를 사용해서 리스트를 사용하며, 타입 제한은 없음.

iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3

목록 간의 덧셈, 뺄셈에는 ++/2--/2를 사용함.

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

뺄셈시의 결과값이 좀 특이…한데, 첫번째로 매칭(같은) 요소를 하나씩만 제거하는듯.

그리고 앞으로 리스트의 머리/꼬리에 대해서 이야기를 많이 한다는데, 이것도 어디서 많이 보던….건데…. ㅇ<-< 아무튼 이것들을 hd/1tl/1로 한방에 가져올 수 있다는 모양. 이렇게.

iex> list = [1,2,3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

빈 리스트를 넘겨주면 에러가 납니다.

iex> hd []
** (ArgumentError) argument error

개인적으로는 차라리 ruby의 nil같은걸 돌려주는게 처리 측면에서 유용하지 않나 싶은데, 이런 객체가 없는건지, 아니면 의도적인 것인지는 아직 불명…

가끔 리스트가 정체불명의 작은 따옴표로 된 값을 반환할 때가 있습니다.

iex> [11, 12, 13]
'vfr'
iex> [104, 101, 108, 108, 111]
'hello'

이는 Elixir가 리스트를 출력 가능한 ASCII문자로 인식해서이며, 이 때에는 문자 리스트, 다시 말해 문자열로서 출력하게 됩니다. 문자 리스트는 Erlang 코드를 보다 보면 흔하게 볼 수 있으며(…정말?), 이런 변수의 정체가 궁금한 경우에는 i/1을 통해서 정보를 확인해주세요. 이는 ruby의 p와 비슷하지만 좀 더 상세한 정보를 제공합니다.

iex> i 'hello'
Term
  'hello'
Data type
  List
Description
  ...
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List

Elixir에서는 작은 따옴표를 사용한 문자열과 큰 따옴표를 사용한 문자열이 다릅니다.

iex> 'hello' == "hello"
false

전자는 문자 리스트이며, 후자는 그냥 문자열입니다. 이 차이에 대해서는 나중에 설명해준다고 하니, 잘 읽어봐야겠습니다[…]

Tuples

Elixir에서는 튜플을 위해서 중괄호를 예약해두었습니다. 자꾸 익숙한게 나오고 있네요. ;_; 리스트와 마찬가지로 어떤 타입이든 상관없이 사용할 수 있습니다.

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

튜플의 원소들은 메모리 상에서 서로와 무척 인접한 곳에 위치하며, 이 때문에 튜플 원소들을 인덱스를 통해 접근하거나, 갯수를 세는 작업이 무척 빠르다고 합니다. 아니, 그럼 리스트는…? 이라는 생각이 드는데… C 레벨에서의 배열 -> 튜플, 링크드 리스트 -> 리스트, 라고 생각하면 그것도 그것대로 납득. 왜 이런식으로 설계를 했는가, 라고 하면 의문이 들지만 말입니다.

put_elem/3를 사용해서 튜플의 특정 인덱스에 데이터를 넣을 수 있습니다.

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

평범한 함수식 접근. 하지만, 코드를 보다보니 리스트는 어떻게 원소를 넣는지에 대한 이야기가 없었던 것을 깨달았습니다. 급 불안해지기 시작합니다.

큼큼, 아무튼 반환값은 삽입한 값이 포함된 새로운 튜플입니다. tuple에 들어있던 원래의 튜플은 변경되지 않습니다. 왜냐구요? Elixir의 객체는 IMMUTABLE이거든요!

Lists or tuples?

그래서 뭘 써야하죠?

List?

리스트는 데이터를 링크드리스트 형태로 저장합니다. 위에서 비유했던대로네요. 리스트의 각 원소들은 자기 자신의 값과 그 다음값에 대한 포인터를 가지고 있습니다. 이를 cons cell라고 부른다네요. 이 개념 자체는 Lisp에서 시작된거 같고 적당한 한글 표현을 못찾아서 우선 영어표기.

iex> list = [1|[2|[3|[]]]]
[1, 2, 3]

결국 올게 왔습니다. 이 코드가 의미하는 것은 무엇이냐. 리스트의 길이를 구하려면 선형 시간이 걸린다는 소립니다. O(n). 대신 넣는거라면 빠르겠죠.

iex> [0 | list]
[0, 1, 2, 3]

Tuple

반면, 튜플은 원소들을 한쪽에 잘 몰아놓습니다. 그러므로 길이나 각각의 인덱스에 있는 원소를 가져오는 것은 빠릅니다. 단점은 뭘까요? 위에서 보았든 불변 객체라는 점. 원소를 추가/삭제/변경을 할 때마다 매번 튜플을 복사하여 새로 만들어야합니다. 비용이 무지막지하겠죠.

So,

이러한 성능적인 특징들이 각 데이터 구조를 어느 때에 써야할지를 알려줄겁니다. 튜플의 가장 흔한 사용 예시는 반환값입니다. 예를 들어, File.read/1는 파일의 내용을 읽은 뒤에 튜플로 돌려줍니다.

iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

주어진 경로에 파일이 있다면, :ok와 함께 파일의 내용물이 돌아오며, 그렇지 않다면 :error와 자세한 에러 정보가 돌아올겁니다.

대부분의 경우, Elixir는 올바른 방법을 사용할 수 있도록 해줄겁니다. 예를 들어, elem/2라는 함수가 있으며, 이를 통해서 튜플의 특정 인덱스에 있는 값에 접근할 수 있습니다. 하지만 리스트에는 이와 동등한 함수가 제공되지 않습니다. 다시 말해서, 인덱스를 사용해야하는 구조라면 튜플을 쓰게끔 언어 레벨에서 강요하겠…다는…그런 의도로 보이네요.

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"

어떤 데이터 구조에 들어있는 원소의 개수를 세는 경우, sizelength로 2 종류가 있는데 차이점은 다음과 같습니다. size는 O(1)이구요, length는 O(n)입니다. 하나하나 이름을 들어보자면,

  • byte_size/1
  • tuple_size/1
  • length/1 -> 리스트의 길이
  • String.length/1 -> 문자열의 길이

Etc

이외에도 프로세스간 통신시에 사용하는 Port, Reference, PID라는 타입도 있습니다. 다만 이는 나중에 살펴보고, 다음으로 기본 연산자들에 대해서 알아보겠습니다.

Reference

Array