16 프로토콜 - Protocols

프로토콜은 다형성을 위한 메커니즘입니다. 프로토콜이 구현되어 있는 동안에는 어떠한 데이터 타입도 디스패칭할 수 있습니다. 예제를 살펴봅시다.

Protocols are a mechanism to achieve polymorphism in Elixir. Dispatching on a protocol is available to any data type as long as it implements the protocol. Let's see an example.

Elixir에서는 falsenil은 false로 처리됩니다. 그렇지 않는것들은 모두 true입니다. 어떠한 데이터 타입대해서도 값이 비어있는지 확인하기 위해 blank?라는 프로토콜이 필요한 어플케이션이 있다고 해봅시다. 예를들어, 비어있는 리스트 또는 비어있는 바이너리는 비어있다고 할 수 있습니다.

In Elixir, only false and nil are treated as false. Everything else evaluates to true. Depending on the application, it may be important to specify a blank? protocol that returns a boolean for other data types that should be considered blank. For instance, an empty list or an empty binary could be considered blanks.

이 프로토콜은 아래와 같이 정의 할 수 있습니다:

We could define this protocol as follows:

defprotocol Blank do
  @doc "Returns true if data is considered blank/empty"
  def blank?(data)
end

하나의 인자를 받는 blank?라는 함수를 프로토콜로 정의했습니다. 이제 Elixir의 다른 데이터타입에 대해서도 아래와 같이 프로토콜을 구현할 수 있습니다:

The protocol expects a function called blank? that receives one argument to be implemented. We can implement this protocol for different Elixir data types as follows:

# Integers are never blank
defimpl Blank, for: Integer do
  def blank?(_), do: false
end

# Just empty list is blank
defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_),  do: false
end

# Just empty map is blank
defimpl Blank, for: Map do
  # Keep in mind we could not pattern match on %{} because
  # it matches on all maps. We can however check if the size
  # is zero (and size is a fast operation).
  def blank?(map), do: map_size(map) == 0
end

# Just the atoms false and nil are blank
defimpl Blank, for: Atom do
  def blank?(false), do: true
  def blank?(nil),   do: true
  def blank?(_),     do: false
end

그리고 모든 네이티브 데이터 타입에도 쓸 수 있습니다. 사용할 수 있는 타입들은:

And we would do so for all native data types. The types available are:

  • Atom
  • BitString
  • Float
  • Function
  • Integer
  • List
  • Map
  • PID
  • Port
  • Reference
  • Tuple

이제 구현된 프로토콜이 있으니 호출할 수 있습니다:

Now with the protocol defined and implementations in hand, we can invoke it:

iex> Blank.blank?(0)
false
iex> Blank.blank?([])
true
iex> Blank.blank?([1, 2, 3])
false

구현되지 않는 데이터 타입의 프로토콜은 에러를 던집니다:

Passing a data type that does not implement the protocol raises an error:

iex> Blank.blank?("hello")
** (Protocol.UndefinedError) protocol Blank not implemented for "hello"

16.1 프로토콜과 구조체 - Protocols and structs

프로토콜과 구조체가 함께할때 Elixir의 확장성의 파워를 가질수 있습니다.

The power of Elixir's extensibility comes when protocols and structs are used together.

이전 장에서 구조체는 맵의 프로토콜 구현을 가지지 않는 맵이라고 배웠습니다. 이전 장에서의 User를 다시 정의해봅시다:

In the previous chapter, we have learned that although structs are maps, they do not share protocol implementations with maps. Let's define a User struct as in the previous chapter:

iex> defmodule User do
...>   defstruct name: "john", age: 27
...> end
{:module, User,
 <<70, 79, 82, ...>>, {:__struct__, 0}}

그리고 나서 확인해보면:

And then check:

iex> Blank.blank?(%{})
true
iex> Blank.blank?(%User{})
** (Protocol.UndefinedError) protocol Blank not implemented for %User{age: 27, name: "john"}

맵의 프로토콜 구현을 가지는것 대신에 구조체는 자기자신만의 프로토콜 구현을 가질 수 있습니다:

Instead of sharing protocol implementation with maps, structs require their own protocol implementation:

defimpl Blank, for: User do
  def blank?(_), do: false
end

필요하다면 비어있는 User에 대해 독자적인 의미를 부여할수도 있습니다. 뿐만 아니라 큐와같이 좀 더 탄탄한 구조체를 만들거나 EnumerableBlank같은 관련있는 모든 프로토콜 구현하기위해 구조체를 사용할 수 있습니다.

If desired, you could come up with your own semantics for a user being blank. Not only that, you could use structs to build more robust data types, like queues, and implement all relevant protocols, such as Enumerable and possibly Blank, for this data type.

그러나 대부분의 경우, 개발자는 모든 구조체에 구현을 명시하는 지루함보다 기본 구현을 제공하길 원할것입니다. 그럴때는 Any 데이터 타입으로 falling back 하는게 도움이 됩니다.

In many cases though, developers may want to provide a default implementation for structs, as explicitly implementing the protocol for all structs can be tedious. That's when falling back to Any comes in handy.

16.2 Falling back to Any

모든 타입에 대해 기본 구현이 제공되면 편할것입니다. 이를 위해서는 프로토콜 정의에 @fallback_to_anytrue로 설정하면 됩니다.

It may be convenient to provide a default implementation for all types. This can be achieved by setting @fallback_to_any to true in the protocol definition:

defprotocol Blank do
  @fallback_to_any true
  def blank?(data)
end

이와같이 구현할수 있습니다:

Which can now be implemented as:

defimpl Blank, for: Any do
  def blank?(_), do: false
end

이제 구현하지 않은 (구조체를 포함하여) 모든 데이터 타입의 Blank프로토콜은 non-blank로 간주됩니다.

Now all data types (including structs) that we have not implemented the Blank protocol for will be considered non-blank.

16.3 내장된 프로토콜 - Built-in protocols

Elixir에는 일부 내장된 프로토콜이 있습니다. 이전 장에서 Enumerable프로토콜을 구현한 어떤 데이터 구조라도 다룰수 있는 Enum모듈에 대해 살펴봤습니다:

Elixir ships with some built-in protocols. In previous chapters, we have discussed the Enum module which provides many functions that work with any data structure that implements the Enumerable protocol:

iex> Enum.map [1, 2, 3], fn(x) -> x * 2 end
[2,4,6]
iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
6

또다른 유용한 예로 문자 데이터 구조를 어떻게 문자열로 변환하는지 명시하는 String.Chars프로토콜이 있습니다. to_string함수에서 사용하고 있습니다:

Another useful example is the String.Chars protocol, which specifies how to convert a data structure with characters to a string. It's exposed via the to_string function:

iex> to_string :hello
"hello"

문자열 interpolation도 to_string함수를 호출합니다:

Notice that string interpolation in Elixir calls the to_string function:

iex> "age: #{25}"
"age: 25"

위의 예는 숫자형이 String.Chars프로토콜을 구현하기때문에 동작합니다. 예를들어,튜플은 에러가 납니다:

The snippet above only works because numbers implement the String.Chars protocol. Passing a tuple, for example, will lead to an error:

iex> tuple = {1, 2, 3}
{1, 2, 3}
iex> "tuple: #{tuple}"
** (Protocol.UndefinedError) protocol String.Chars not implemented for {1, 2, 3}

더 복잡한 데이터 구조를 "출력"할 필요가 있는 경우 단순히 Inspect프로토콜의 inspect함수를 사용하면 됩니다:

When there is a need to "print" a more complex data structure, one can simply use the inspect function, based on the Inspect protocol:

iex> "tuple: #{inspect tuple}"
"tuple: {1, 2, 3}"

Inspect프로토콜은 어떠한 데이터 구조라도 읽을 수 있는 텍스트 표현으로 변환시켜줍니다. IEx에서 결과를 표시하는 데에도 사용하고 있습니다:

The Inspect protocol is the protocol used to transform any data structure into a readable textual representation. This is what tools like IEx use to print results:

iex> {1, 2, 3}
{1,2,3}
iex> %User{}
%User{name: "john", age: 27}

inspect된 값이 #로 시작되는것들은 유효하지 않는 Elixir문법입니다. 이 말은 inspect된 프로토콜이 다시 돌이킬수 없는 정보라는 뜻입니다.

Keep in mind that, by convention, whenever the inspected value starts with #, it is representing a data structure in non-valid Elixir syntax. This means the inspect protocol is not reversible as information may be lost along the way:

iex> inspect &(&1+2)
"#Function<6.71889879/1 in :erl_eval.expr/5>"

Elixir에는 다른 프로토콜도 있지만 가장 일반적인것들에 대해 살펴보았습니다. 다음 장에서는 에러 핸들링과 예외에 대해 배울것입니다.

There are other protocols in Elixir but this covers the most common ones. In the next chapter we will learn a bit more about error handling and exceptions in Elixir.