Elixir – 20: 타입 명세와 행동
Elixir Tutorial 시리즈입니다. 거의 대부분은 튜토리얼의 한글 번역에 가깝습니다만, 생략되거나 추가로 주석을 달거나 하는 부분이 많습니다. 원문은 최하단의 링크를 참고하세요.
Elixir – 20: 타입 명세와 행동
Elixir는 동적인 타입 언어이며, 모든 타입은 실행시간에 추론됩니다. 그럼에도 불구하고 Elixir는 타입 명세를 가지고 있으며, 이는
- 커스텀 데이터 타입 선언
- 타입 함수 시그니쳐(명세) 선언
을 위해서 사용됩니다.
함수 명세
Elixir는 integer나 pid, 그 이외의 복합 타입 등 기본 타입을 몇가지 지원하고 있습니다. 예를 들어서 round/1 함수는 float는 가장 가까운 정수로 변경해주며, 매개변수로 number라는 매개변수(integer나 float)를 받고, integer를 반환합니다. 이 문서에서 볼 수 있듯, round/1의 타입 시그니쳐는 다음과 같이 표현됩니다.
round(number) :: integer
::는 왼쪽에 있는 함수가 오른쪽에 있는 타입을 반환한다는 의미입니다. 함수의 스펙은 @spec 디렉티브를 사용하여 기술되며, 함수 선언의 직전에 위치하게 됩니다. 그러므로 round/1 함수는 다음과 같이 정의될 것입니다.
@spec round(number) :: integer
def round(number), do: # implementation...
Elixir는 복합 타입도 지원합니다. 예를 들어서, 정수의 리스트는 [integer]라고 표현할 수 있습니다. 이러한 모든 내장 타입들은 타입 명세 문서에서 확인할 수 있습니다.
커스텀 타입 정의하기
Elixir가 많고 유용한 내장 타입을 제공하기도 하지만, 새로운 타입을 정의하는 것도 어렵지 않습니다. 이는 @type 디렉티브를 사용하여 모듈을 정의하면 됩니다.
일반적인 산술 연산자를 가지고 있는 LousyCalculator 모듈이 있다고 가정해보죠. 단 숫자를 반환하는 대신에 첫번째로는 연산의 결과값, 두번째 원소로 임의의 문자열을 포함하는 튜플을 반환합니다.
defmodule LousyCalculator do
@spec add(number, number) :: {number, String.t}
def add(x, y), do: {x + y, "You need a calculator to do that?!"}
@spec multiply(number, number) :: {number, String.t}
def multiply(x, y), do: {x * y, "Jeez, come on!"}
end
예제에서 볼 수 있듯, 튜플은 복합 타입이며 각 튜플은 내부에 들어있는 값으로 식별됩니다. string 대신에 String.t를 사용한 이유를 이해하려면 타입 명세 문서의 다른 부분을 확인하길 바랍니다.
이렇게 정의해도 아무런 문제는 없습니다만, {number, String.t}를 반복해서 사용할 수록 점점 더 귀찮아집니다. 이럴 때 커스텀 타입을 정의하기 위해서 @type 디렉티브를 사용할 수 있습니다.
defmodule LousyCalculator do
@typedoc """
Just a number followed by a string.
"""
@type number_with_remark :: {number, String.t}
@spec add(number, number) :: number_with_remark
def add(x, y), do: {x + y, "You need a calculator to do that?"}
@spec multiply(number, number) :: number_with_remark
def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end
@typedoc 디렉티브는 @doc와 @moduledoc 디렉티브와 유사하며, 커스턴 타입을 문서화하기 위한 도구입니다.
@type로 정의된 커스텀 타입은 외부에 노출되며, 이에 접근할 수 있습니다.
defmodule QuietCalculator do
@spec add(number, number) :: number
def add(x, y), do: make_quiet(LousyCalculator.add(x, y))
@spec make_quiet(LousyCalculator.number_with_remark) :: number
defp make_quiet({num, _remark}), do: num
end
만약 커스텀 타입을 비공개로 유지하고 싶다면, @type 대신에 @typep 디렉티브를 사용하세요.
정적 코드 분석
타입 명세는 개발자에게만 유용한 것이 아니라 추가적인 문서로서의 역할도 있습니다. Erlang의 도구에 Dialyzer라는 것이 있습니다만, 이 도구는 타입 명새를 정적 코드 분석에 활용합니다. 이것이 바로 QuietCalculator 예제에서 비공개 함수라도 명세를 작성한 이유입니다.
행동(Behaviour)
많은 모듈들은 공개 API를 공유합니다. Plug를 확인해보세요. 이것은 웹 애플리케이션에서 조합 가능한 모듈을 만들기 위해 상태를 명세로서 기술합니다. 각각의 Plug는 모듈이며 적어도 2개 이상의 공개 함수(init/1, call/2)를 구현해야합니다.
행동은 다음과 같은 방법을 제공합니다:
- 모듈에서 구현되어야 하는 함수들의 집합을 정의합니다.
- 이 집합에 있는 모든 함수들이 구현되어야 함을 보장합니다.
필요하다면 행동을 객체 지향 언어에서의 인터페이스와 비슷한 무언가라고 생각해도 좋습니다.
행동을 정의하기
구조화된 데이터를 처리하는 여러 개의 파서를 구현하고 싶은 상황을 가정해보죠. 예를 들어서 JSON 파서와 YAML 파서같은걸 말이죠. 각각의 파서들은 동일한 방식으로 동작할 것입니다. 각각 parse/1이라는 함수와 extensions/0 함수를 제공할 것입니다. parse/1 함수는 구조화된 데이터의 Elixir 표현을 반환할 것이며, extensions/0 함수는 각 데이터 타입에 따른, 사용 가능한 파일 확장자의 리스트를 반환합니다(e.g. JSON 파일의 경우 .json가 있을 겁니다).
그럼 이제 Parser 행동을 만들어 봅시다.
defmodule Parser do
@callback parse(String.t) :: any
@callback extensions() :: [String.t]
end
Parser 행동이 선언된 모듈에서는 @callback 디렉티브로 선언된 모든 함수를 구현해야만 합니다. 위에서 볼 수 있듯이 @callback은 함수의 이름 뿐만이 아니라 @spec 디렉티브에서 보았던 함수의 명세 전체를 기대합니다.
행동을 적용하기
행동을 적용하는 것은 무척 직관적입니다.
defmodule JSONParser do
@behaviour Parser
def parse(str), do: # ... parse JSON
def extensions, do: ["json"]
end
defmodule YAMLParser do
@behaviour Parser
def parse(str), do: # ... parse YAML
def extensions, do: ["yml"]
end
만약 모듈에서 주어진 행동이 요구하는 콜백들을 구현하지 않았다면, 컴파일 시간에 에러가 발생할 것입니다.
