트랙
/
Elixir
Elixir
/
연습 문제
/
춤추는 점들
춤추는 점들

춤추는 점들

학습 연습 문제

소개

Use

use 매크로를 사용하면 다른 모듈이 제공하는 기능으로 우리 모듈을 빠르게 확장할 수 있어요. 모듈을 use하면 그 모듈이 우리 모듈에 코드를 주입할 수 있어요. 예를 들어 함수를 정의하거나, 다른 모듈을 import 또는 alias하거나, 모듈 속성을 설정할 수 있죠.

Exercism에서 Elixir 연습 문제의 테스트 파일을 본 적이 있다면, 대부분 use ExUnit.Case로 시작한다는 걸 눈치챘을 거예요. 이 한 줄의 코드가 테스트 모듈에서 test와 assert 매크로를 사용할 수 있게 해줘요.

defmodule LasagnaTest do
  use ExUnit.Case

  test "expected minutes in oven" do
    assert Lasagna.expected_minutes_in_oven() === 40
  end
end

__using__/1 매크로

모듈을 use할 때 정확히 무슨 일이 일어나는지는 그 모듈의 __using__/1 매크로가 결정해요. 이 매크로는 옵션이 담긴 키워드 리스트를 인자 하나로 받고, 인용된 표현식을 반환해요. 이 인용된 표현식에 담긴 코드는 use를 호출할 때 우리 모듈에 삽입돼요.

defmodule ExUnit.Case do
  defmacro __using__(opts) do
    # some real-life ExUnit code omitted here
    quote do
      import ExUnit.Assertions
      import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
    end
  end
end

옵션은 use를 호출할 때 두 번째 인자로 넘겨줄 수 있어요. 예를 들어 use ExUnit.Case, async: true처럼요. 명시적으로 넘겨주지 않으면 빈 리스트가 기본값이에요.

Behaviours

behaviour를 이용하면 _behaviour 모듈_에 인터페이스(함수와 매크로의 집합)를 정의해 두고, 나중에 여러 _콜백 모듈_에서 구현할 수 있어요. 인터페이스를 공유하기 때문에 그 콜백 모듈들은 서로 바꿔 쓸 수 있죠.

Note

참고로 "behaviours"는 영국식 철자예요.

behaviour 정의하기

behaviour를 정의하려면 새 모듈을 만들고, 원하는 인터페이스에 속하는 함수들의 목록을 지정해야 해요. 각 함수는 @callback 모듈 속성으로 정의해요. 문법은 함수 타입스펙(@spec)과 똑같아요. 함수 이름, 인자 타입의 목록, 그리고 가능한 모든 반환 타입을 지정해야 해요.

defmodule Countable do
  @callback count(collection :: any) :: pos_integer
end

behaviour 구현하기

기존 behaviour를 우리 모듈에 추가하려면(콜백 모듈을 만들려면) @behaviour 모듈 속성을 사용해요. 값은 추가하려는 behaviour 모듈의 이름이에요.

그다음에는 그 behaviour 모듈이 요구하는 모든 함수(콜백)를 정의해야 해요. Elixir에 내장된 Access나 GenServer behaviour처럼 다른 사람이 만든 behaviour를 구현한다면, behaviour의 모든 콜백 목록은 hexdocs.pm 문서에서 찾을 수 있어요.

콜백 모듈이 자기 behaviour에 속한 함수만 구현해야 하는 건 아니에요. 하나의 모듈이 여러 behaviour를 구현할 수도 있어요.

어떤 함수가 어떤 behaviour에서 온 것인지 표시하려면 각 함수 앞에 @impl 모듈 속성을 사용해요. 값은 이 콜백을 정의한 behaviour 모듈의 이름이에요.

defmodule BookCollection do
  @behaviour Countable

  defstruct [:list, :owner]

  @impl Countable
  def count(collection) do
    Enum.count(collection.list)
  end

  def mark_as_read(collection, book) do
    # other function unrelated to the Countable behaviour
  end
end

기본 콜백 구현

behaviour를 정의할 때 콜백의 기본 구현을 제공할 수 있어요. 이 구현은 __using__/1 매크로의 인용된 표현식 안에 정의해요. behaviour 모듈을 사용하는 쪽에서 기본 구현을 재정의할 수 있게 하려면, 함수 구현 뒤에 defoverridable/1 매크로를 호출해요. 이 매크로는 함수 이름을 키로, 함수의 인자 개수를 값으로 하는 키워드 리스트를 받아요.

defmodule Countable do
  @callback count(collection :: any) :: pos_integer

  defmacro __using__(_) do
    quote do
      @behaviour Countable
      def count(collection), do: Enum.count(collection)
      defoverridable count: 1
    end
  end
end

__using__/1 안에 함수를 정의하는 건 기본 콜백 구현을 정의할 때를 빼고는 권장하지 않아요. 하지만 언제든 다른 모듈에 함수를 정의하고 __using__/1 매크로에서 import할 수 있어요.

지침

예술가를 꿈꾸는 친구가 프로젝트 아이디어를 들고 찾아왔어요. 친구의 시각적 창의성과 기술적 전문성을 결합해 봐요. 이제 생성 예술을 시작할 때예요!

제약은 창의성을 돕고 프로젝트 마감도 앞당겨 주기 때문에, 두 사람은 걸작을 단 하나의 도형, 원으로만 제한하기로 했어요. 그런데 원은 아주 많을 거예요. 게다가 움직이기까지 해요! 이름은... 춤추는 점들이에요.

친구는 점들을 위한 새롭고 정교한 움직임을 계속 만들어 내고 싶어 할 테니, 나중에 새로운 애니메이션을 쉽게 정의할 수 있는 구조를 만드는 것부터 시작해요.

1. 애니메이션 behaviour 정의하기

각 애니메이션 모듈은 init/1과 handle_frame/3 두 개의 콜백을 구현해야 해요. 이 둘을 Animation 모듈에 정의해요.

init/1 콜백을 정의해요. 이 콜백은 opts 타입의 인자 하나를 받고, {:ok, opts} 튜플이나 {:error, error} 튜플을 반환해야 해요. 이 콜백의 구현체는 주어진 옵션이 이 특정 애니메이션 유형에 유효한지 확인해요.

handle_frame/3 콜백을 정의해요. 이 콜백은 점, 프레임 번호, 옵션 세 개의 인자를 받아요. 항상 점을 반환해야 해요. 이 콜백의 구현체는 현재 프레임 번호와 애니메이션의 옵션에 따라 점의 속성을 수정해요.

2. init/1 콜백의 기본 구현 제공하기

Animation behaviour는 use DancingDots.Animation을 호출해서 다른 모듈에 쉽게 포함할 수 있어야 해요.

그렇게 하려면 Animation 모듈에 __using__ 매크로를 구현해서, 다른 모듈의 behaviour로 Animation 모듈을 설정하게 해요. 또한 init/1 콜백의 기본 구현도 제공해야 해요. init/1의 기본 구현은 주어진 옵션을 그대로 반환해야 해요.

defmodule MyCustomAnimation do
  use DancingDots.Animation
end

MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}

3. Flicker 애니메이션 구현하기

Animation behaviour를 사용해서 깜빡이는 애니메이션을 구현해요.

이 애니메이션은 옵션을 받지 않기 때문에 기본 init/1 콜백을 사용해야 해요.

단일 프레임을 처리하는 handle_frame/3 콜백을 구현해요. 프레임 번호가 4의 배수라면, 함수는 원래 불투명도의 절반을 가진 점을 반환해야 해요. 다른 프레임에서는 점을 그대로 반환해야 해요.

프레임은 1부터 세요. handle_frame/3에 전달되는 점은 항상 원래 상태의 점이고, 이전 프레임의 상태가 아니에요.

dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}

DancingDots.Flicker.handle_frame(dot, 1, [])
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}

DancingDots.Flicker.handle_frame(dot, 4, [])
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}

4. Zoom 애니메이션 구현하기

Animation behaviour를 사용해서 확대/축소 애니메이션을 구현해요.

이 애니메이션은 옵션을 하나 받아요. 바로 속도예요. 속도는 어떤 숫자든 될 수 있어요. 속도가 음수면, 점은 확대되는 대신 축소돼요.

init/1 콜백을 구현해요. 전달된 옵션이 :velocity 키를 가진 키워드 리스트인지 검증해야 해요. 속도 값은 숫자여야 해요. 숫자가 아니면, 오류 "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"를 반환해요.

handle_frame/3 콜백을 구현해요. 현재 프레임 번호에서 1을 뺀 값에 속도를 곱한 만큼 점의 반지름을 늘려서 반환해야 해요.

프레임은 1부터 세요. handle_frame/3에 전달되는 점은 항상 원래 상태의 점이고, 이전 프레임의 상태가 아니에요.

DancingDots.Zoom.init(velocity: nil)
# => {:error, "The :velocity option is required, and its value must be a number. Got: nil"}

dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}

DancingDots.Zoom.handle_frame(dot, 1, velocity: 10)
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}

DancingDots.Zoom.handle_frame(dot, 2, velocity: 10)
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Elixir Exercism

춤추는 점들 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Elixir 트랙을 개념 58개연습 문제 168개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.