네이밍 규칙

네이밍 규칙 (Naming conventions)

코드를 읽을 때 이름만 봐도 "이 함수는 실패하면 예외를 터뜨리는구나", "이 함수는 boolean을 돌려주는구나"를 알 수 있다면 얼마나 좋을까요? Elixir는 그런 **이름 짓기 규칙(convention)**을 언어 수준에서 잘 정리해 두었어요. 대문자·소문자 표기부터 구두점까지, Elixir의 네이밍 규칙을 한자리에 모은 문서가 바로 이 페이지예요.

출처: Naming conventions

본문

이 문서는 Elixir의 네이밍 규칙을 대소문자(casing)부터 구두점(punctuation)까지 정리한 참고 자료예요. 네이밍 규칙은 정의상 Elixir 문법의 부분집합이에요. 규칙은 언어와 커뮤니티를 위한 베스트 프랙티스를 따르고 세우는 것을 목표로 하죠. 규칙을 넘어선 Elixir 문법 전체를 완전한 참조로 보고 싶다면 Syntax reference를 살펴보세요.

대소문자 (Casing)

Elixir 개발자는 변수, 함수명, 모듈 속성 등을 정의할 때 **반드시 snake_case**를 써야 해요:

some_map = %{this_is_a_key: "and a value"}
is_map(some_map)

단, 주로 모듈 이름으로 쓰이는 alias는 예외예요. 대문자로 시작해서 CamelCase로 써야 하죠. OptionParser처럼요. alias에서는 약어(acronym)에도 대문자를 유지해요. ExUnit.CaptureIOMix.SCM처럼요.

atom은 :snake_case로 쓰든 :CamelCase로 쓰든 상관없지만, Elixir 전반에서는 snake case 버전을 쓰는 게 관례예요.

일반적으로 파일명은 그 안에 정의된 모듈의 snake_case 관례를 따라요. 예를 들어 MyAppmy_app.ex 파일 안에 정의하는 게 자연스러워요. 다만 이건 어디까지나 관례예요. 결국 어떤 파일명을 쓰든 컴파일된 코드에는 아무 영향을 주지 않으니까요.

밑줄 (_foo)

Elixir는 여러 상황에서 밑줄을 활용해요.

예를 들어 쓰지 않을 값_나 밑줄로 시작하는 변수에 할당해야 해요:

iex> {:ok, _contents} = File.read("README.md")

함수명도 밑줄로 시작할 수 있어요. 그런 함수는 기본적으로 import되지 않아요:

iex> defmodule Example do
...>   def _wont_be_imported do
...>     :oops
...>   end
...> end

iex> import Example
iex> _wont_be_imported()
** (CompileError) iex:1: undefined function _wont_be_imported/0

이런 성질 덕분에 Elixir는 밑줄로 시작하는 함수로 모듈에 컴파일 타임 메타데이터를 붙이는 일을 해요. 그런 함수는 대부분 __foo__ 형태예요. 예를 들어 Elixir의 모든 모듈에는 __info__/1 함수가 있어요:

iex> String.__info__(:functions)
[at: 2, capitalize: 1, chunk: 2, ...]

Elixir에는 이중 밑줄 형태를 따르는 다섯 가지 특수 형태(special form)도 있어요. __CALLER__/0, __DIR__/0, __ENV__/0, __MODULE__/0은 현재 환경에 대한 컴파일 타임 정보를 가져오고, __STACKTRACE__/0은 현재 예외의 스택트레이스를 가져와요.

뒤의 느낌표 (foo!)

뒤에 붙는 느낌표는 실패 시 예외를 던지는 함수나 매크로를 뜻해요. 대부분 :ok/:error 튜플(또는 nil)을 반환하는 함수의 "raise 버전"으로 존재하죠.

대표적인 예가 File.read/1File.read!/1이에요. File.read/1은 성공·실패 튜플을 돌려주고, File.read!/1은 평범한 값을 돌려주거나 실패하면 예외를 던져요:

iex> File.read("file.txt")
{:ok, "file contents"}
iex> File.read("no_such_file.txt")
{:error, :enoent}

iex> File.read!("file.txt")
"file contents"
iex> File.read!("no_such_file.txt")
** (File.Error) could not read file no_such_file.txt: no such file or directory

패턴 매칭으로 여러 결과를 각각 처리하고 싶을 땐 ! 없는 버전이 더 좋아요:

case File.read(file) do
  {:ok, body} -> # `body`로 무언가를 한다
  {:error, reason} -> # `reason`이 일으킨 오류를 처리한다
end

하지만 결과가 항상 성공이라고 기대되는 상황(예: 파일이 항상 존재할 거라고 기대하는 상황)이라면, 느낌표 버전이 더 편리해요. 실패해도 (실패한 패턴 매칭보다) 더 도움이 되는 오류 메시지를 던져 주니까요.

실패 케이스를 생각할 때, 우리는 대개 그 연산과 관련된 의미적(semantic) 오류를 떠올려요. 파일을 여는 데 실패한다거나 Map에서 키를 가져오는 데 실패한다거나 하는 경우죠. 반면 잘못된 인자 타입 같은 오류는 함수에 느낌표가 있든 없든 항상 예외를 던져야 해요. 그런 경우 예외는 대개 ArgumentError나 자세한 FunctionClauseError예요:

iex(1)> File.read(123)
** (FunctionClauseError) no function clause matching in IO.chardata_to_string/1

 The following arguments were given to IO.chardata_to_string/1:

 # 1
 123

 Attempted function clauses (showing 2 out of 2):

 def chardata_to_string(string) when is_binary(string)
 def chardata_to_string(list) when is_list(list)

짝을 이루는 함수의 예가 더 있어요: Base.decode16/2Base.decode16!/2, File.cwd/0File.cwd!/0. 어떤 상황에서는 느낌표 없는 버전 없이 느낌표 함수만 존재하기도 해요. 그것도 역시 오류 가능성을 암시하죠. Protocol.assert_protocol!/1이나 PartitionSupervisor.resize!/2처럼요. 미래에 non-raise 버전을 추가할 가능성이 있다고 예상된다면 이런 설계가 유용해요.

뒤의 물음표 (foo?)

boolean을 반환하는 함수는 이름 끝에 물음표를 붙여요.

예시: Keyword.keyword?/1, Mix.debug?/0, String.contains?/2

단, boolean을 반환하면서 가드(guard)에서도 유효한 함수는 다음 절에서 설명하는 다른 규칙을 따라요.

is_ 접두사 (is_foo)

가드 절(guard clause)에서 허용되는 타입 검사와 기타 boolean 검사는 is_ 접두사를 써요.

예시: Integer.is_even/1, is_list/1

이 함수들과 매크로는 Erlang의 is_ 접두사 관례를 따라요. 물음표 접미사 대신 말이죠. 그 이유는 정확히 가드 절에서 허용된다는 사실을 나타내기 위해서예요. 가드에서 유효하지 않은 타입 검사는 이 관례를 따르지 않아요. Keyword.keyword?/1처럼요.

물음표 접미사와 is_ 접두사를 함께 쓰면 안 돼요.

특별한 이름들

Elixir에서 특정한 의미를 갖는 이름들이 있는데, 그 경우들을 아래에서 자세히 볼게요.

length와 size

함수 이름에 size가 보이면, 그 연산은 상수 시간(O(1) 시간)으로 동작한다는 뜻이에요. 크기가 데이터 구조와 함께 저장되어 있으니까요.

예시: map_size/1, tuple_size/1

함수 이름에 length가 보이면, 그 연산은 선형 시간(O(n) 시간)으로 동작해요. 데이터 구조 전체를 순회해야 하니까요.

예시: length/1, String.length/1

다시 말해, 이름에 "size"가 들어간 함수는 데이터 구조가 아무리 작거나 커도 같은 시간이 걸려요. 반대로 "length"가 들어간 함수는 데이터 구조가 커질수록 시간도 더 걸려요.

get, fetch, fetch!

키-값 데이터 구조에서 get, fetch, fetch! 함수를 만나면 다음 동작을 기대할 수 있어요:

  • get — 키가 없으면 기본값(기본값은 그 자체로 nil)을 반환하고, 있으면 요청한 값을 반환해요
  • fetch — 키가 없으면 :error를, 있으면 {:ok, value}를 반환해요
  • fetch! — 키가 없으면 예외를 던지고, 있으면 요청한 값을 반환해요

예시: Map.get/2, Map.fetch/2, Map.fetch!/2, Keyword.get/2, Keyword.fetch/2, Keyword.fetch!/2

compare

compare/2 함수는 첫 번째 항이 두 번째보다 작으면 :lt, 서로 같다고 비교되면 :eq, 첫 번째가 더 크면 :gt를 반환해야 해요.

예시: DateTime.compare/2

이 규칙은 Enum.sort/2의 기대와 맞물려 있어서 특히 중요해요.

더 알아보기