2 minute read

Polymorphism

elixir 언어의 다형성(polymorphism) 도구인 프로토콜(protocol)을 간단히 소개하고 최근에 본 pheonix 프레임워크의 param 예제를 간단히 설명한다. 먼저 간단히 프로토콜을 살펴본다. 예제는 elixir 사이트의 protocols 설명 페이지에서 가져왔다.

defprotocol Size do
  @doc "Calculates the size (and not the length!) of a data structure"
  def size(data)
end

defimpl Size, for: BitString do
  def size(string), do: byte_size(string)
end

defimpl Size, for: Tuple do
  def size(tuple), do: tuple_size(tuple)
end

Size 프로토콜을 선언하고 문자열과 튜플 데이터 타입에 대해 구현한다.

iex> Size.size("foo")
3
iex> Size.size({:ok, "hello"})
2

프로토콜 함수를 호출하면 데이터 타입에 대한 구현을 호출한다. 즉, 여러 데이터 타입에 대해 하나의 인터페이스를 사용할 수 있다.

defmodule User do
  defstruct [:name, :age]
end

defimpl Size, for: User do
  def size(_user), do: 2
end

iex> Size.size(%User{name: "elixir", age: 10})
2

사용자 정의 타입이라고도 할 수 있는 구조체(struct)도 지원한다.

다음은 phoenix 프레임워크에 있는 프로토콜을 간단히 살펴본다. Programming Phoenix 1.4 책에서 프로토콜을 사용한 좋은 예제를 봐서 가져왔다.

# lib/rumbl_web/templates/video/index.html.eex
<tbody>
  <%= for video <- @videos do %>
    <tr>
      <td>
        <%= link "Watch",
            to: Routes.watch_path(@conn, :show, video),
            class: "button" %>
      </td>
    </tr>
  <% end %>
</tbody>

템플릿으로 Watch 버튼을 추가한다.

# lib/rumbl_web/controllers/watch_controller.ex
defmodule RumblWeb.WatchController do
  def show(conn, %{"id" => id}) do
    video = Multimedia.get_video!(id)
    render(conn, "show.html", video: video)
  end
end

버튼을 클릭하면 라우터에서 RumblWeb.WatchController.show/2 함수를 호출한다. 템플릿에서 Video 구조체 전체를 인자로 넘기는데, 컨트롤러에는 id 필드만 넘어간다. 어떤 일이 일어나는 걸까?

defmodule Phoenix.Router.Helpers do
  defp expand_segments([h | t], acc),
    do:
      expand_segments(
        t,
        quote(
          do: unquote(acc) <> "/" <> URI.encode(to_param(unquote(h)), &URI.char_unreserved?/1)
        )
      )
end

url을 만들 때, to_param/1 함수를 호출한다. to_param/1 함수는 integer, string, boolean 데이터 타입이 아니면 Phoenix.Param.to_param/1 함수를 호출한다.

defprotocol Phoenix.Param do
  @fallback_to_any true

  @spec to_param(term) :: String.t()
  def to_param(term)
end

defimpl Phoenix.Param, for: Any do
  def to_param(%{id: nil}) do
    raise ArgumentError, "cannot convert struct to param, key :id contains a nil value"
  end
  def to_param(%{id: id}) when is_integer(id), do: Integer.to_string(id)
  def to_param(%{id: id}) when is_binary(id), do: id
  def to_param(%{id: id}), do: Phoenix.Param.to_param(id)
end

Phoenix.Param.to_param/1 함수는 프로토콜 함수다. @fallback_to_any true 모듈 속성을 정의해 데이터 타입에 대한 구현이 없다면 Any에 대해 구현한 함수를 호출한다. 여기서 id 필드를 사용한다. 그래서 위 예제에서 Video 구조체를 넘기면 id 필드를 컨트롤러 함수 인자로 넘기게 된다.

구조체에서 id 필드를 사용 안 할 수 있을까? 다른 형식으로 리턴하게 해서 url을 만들 수 있을까?

defimpl Phoenix.Param, for: Rumbl.Multimedia.Video do
  def to_param(%{slug: slug, id: id}) do
    "#{id}-#{slug}"
  end
end

Phoenix.Param 프로토콜이라서 쉽게 확장할 수 있다. Video 구조체에 대한 프로토콜 정의를 한다.

iex> Phoenix.Param.to_param(%Rumbl.Multimedia.Video{id: 1, slug: "hello-protocol"})
"1"

Video 구조체에 대해 구현하기 전엔 id 필드 값만 가져온다.

iex> Phoenix.Param.to_param(%Rumbl.Multimedia.Video{id: 1, slug: "hello-protocol"})
"1-hello-protocol"

Video 구조체에 대한 프로토콜 구현을 적용했다. id 필드 값을 리턴하는 게 아니라 id와 slug 필드를 조합해 문자열을 리턴한다. Phoenix.Param 프로토콜이라 추가한 구조체에 대해 쉽게 확장할 수 있다.