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에서는 false
와 nil
은 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에 대해 독자적인 의미를 부여할수도 있습니다. 뿐만 아니라 큐와같이 좀 더 탄탄한 구조체를 만들거나 Enumerable
과 Blank
같은 관련있는 모든 프로토콜 구현하기위해 구조체를 사용할 수 있습니다.
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_any
를 true
로 설정하면 됩니다.
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.