#elixirlang 다형성 도구인 protocol를 사용한 pheonix param
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
프로토콜이라 추가한 구조체에 대해 쉽게 확장할 수 있다.