TIL: Functional Decomposition with Elixir

sam·2023년 10월 26일

Elixir

목록 보기
2/7
post-thumbnail

코딩테스트

이번 문제는 Dart 점수계산기 라는 문제다
매우 간단하다.

  • 과녁밖으로 가면 0점 (과녁의 반지름은 10unit)
  • 가장 바깥쪽의 테두리 안에 들어가면 1점
  • 중간 테두리 안에 들어가면 5점
  • 센터 테두리 안에 들어가면 10점

나의 솔루션

부끄럽지만 나의 한방솔루션은 아래와 같다.

defmodule Darts do
  @type position :: {number, number}
  @doc """
  Calculate the score of a single dart hitting a target
  """
  @spec score(position) :: integer
  def score({x, y}) do
    xx = if is_integer(x), do: x/1, else: x
    yy = if is_integer(y), do: y/1, else: y
    r = Float.pow( Float.pow(xx, 2) + Float.pow(yy,2), 1/2)
    cond do 
      r > 10 -> 0
      r > 5 -> 1
      r > 1 -> 5
      true -> 10
    end
  end
end

반지름 구하는 공식은 다 알테고,
테스트케이스를 돌려보았다.
정수형(Integer)로 {x,y} 포지션값을 주는 경우가 있어, 정수형인 경우 모두 Float 자료형으로 변환하고자 했으나, 뭔지 몰라 copilot에게 물어봤다.

Float.to_float/1 그럴듯한 함수를 주었으나, 그런거 없다. 🤡

없는데? 하니까 또 죄송해하며 그럴듯한 함수를 줬다. Float.float/1 -> 없다. 👺


진리의 스택오버플로에서 뭔 이상한 야로를 줬다. /1 하면 무조건 Float 자료형은 반환한다고... 매우 찝찝하지만, 돌아는 가니까 그냥 이렇게 제출하고, 과연 고수들의 답변은 어떤가 비교해봤다.

고수의 답변

고수를 고르는 기준: 수염이 있다 +10p(흰수염 +5) , 실제 사진이다 +10, 중년아재다 +10
-- 뇌피셜

defmodule Darts do
  @type position :: {number, number}

  @doc """
  Calculate the score of a single dart hitting a target
  """
  @spec score(position) :: integer
  def score(position), 
    do: 
      position
      |> zone()
      |> points()

  defp zone(position={_,_}),
    do: 
      position
      |> radius()
      |> circle()

  defp radius({x,y}), do: :math.sqrt(x ** 2 + y ** 2)

  defp circle(radius) when radius <= 1, do: :inner
  defp circle(radius) when radius <= 5, do: :middle
  defp circle(radius) when radius <= 10, do: :outer
  defp circle(_), do: :outside

  defp points(:inner), do: 10
  defp points(:middle), do: 5
  defp points(:outer), do: 1
  defp points(:outside), do: 0

end

보자마자 번쩍 했다. 나의 무지함과 새로운 세상의 안목을 넓히는 충격적인 경험.

하나씩 뜯어보자

def 는 퍼블릭이고, defp는 프라이빗 이다. 즉 defp 는 함수밖에서 호출할 수 없다.

score/1

사용자가 호출하는 함수는 score/1 함수이므로, 해당 함수를 보면 아래와 같이 데이터를 의미있는 어떤 형태로 조각해 나가는 것을 볼 수 있다.

  1. 입력 인자는 {x,y} 인데, x,y 의 값이 Ingeger 자료형이나, Float 자료형이 들어올 수 있다.
  2. 포지션으로 받은 다음 어떤 zone 에 해당하는 지로 변환하고 zone/1,
  3. 어떤 zone을 평가하는 함수 point/1에 넘겨주어 최종적으로 점수를 계산하여
  4. 사용자에게 반환한다.

zone/1

내부함수 zone/1 또한 직교좌표계의 x,y 값을 반지름 으로 변환하고, 반지름을 주어진 zone에 매칭하는 로직을 담당하는 circle/1 함수를 사용한다.

points/1, circle/1

가장 낮은단계의 연산이 이뤄지는 이 함수들은, 함수형 프로그래밍에서 많이 보듯이, guard를 통하여 함수를 여러개 정의하여서 맞는 함수를 자동으로 찾아가게 구현하였다.
물론 defp circle(_), do: :outside 처럼 관심없는 경우에 모두 기본값 :outside 를 반환하게 설정하였다.

  defp circle(radius) when radius <= 1, do: :inner
  defp circle(radius) when radius <= 5, do: :middle
  defp circle(radius) when radius <= 10, do: :outer
  defp circle(_), do: :outside

  defp points(:inner), do: 10
  defp points(:middle), do: 5
  defp points(:outer), do: 1
  defp points(:outside), do: 0

컴퓨터 언어를 모르는 일반인도, 그냥 자연어(영어)처럼 읽을 수 있는 아름다운 다중 절 함수로 비즈니스 규칙을 구현하는 것이 얼마나 쉬운지 보여주는 예이다.

핵심아이디어 - functional Decomposition

함수의 합성으로 문제를 풀어간다. 이를 통해, 영역이 추가된다던가, 영역의 포인트가 변경된다고 하더라도, 기본 함수 (score/1)는 변형이 없고, 필요한 영역과 포인트 함수만 추가하거나, 변경함으로 쉽고 안전하게 유지보수가 가능해진다.

이는 기능적 분해가 도메인 개념을 표현하고 유지 관리 가능한 코드를 작성하는 좋은 예라고 생각한다.


이 코드를 공유해 주신 선생님

profile
다이조부

0개의 댓글