본문 바로가기
개발/Elixir(엘릭서)

프론트엔드 개발자가 본 Elixir의 Arity와 Clauses에 대하여

by 핸디(Handy) 2025. 5. 26.

들어가며

해당 글은 엘릭서를 처음 배우는 프론트엔드 개발자의 시선으로 엘릭서를 비교 분석한 글이다. 이번 글에서는 그 중에 "Arity"와 "Clauses"에 대해 살펴보자.

최근 들어 엘릭서에 대한 학습을 미뤄두다가 exercism에서 엘릭서 모든 문제를 풀어보는 것을 목표로 열심히 풀어재끼고 있다.

 

Elixir on Exercism

Get fluent in Elixir by solving 162 exercises. And then level up with mentoring from our world-class team.

exercism.org

갈 길이 멀다..

문제를 풀어보면서 전통적인 프론트엔드 개발자 입장에서 엘릭서라는 언어를 어떻게 살펴보고 다른지 적어보면 좋을듯하여 이렇게 기획하여 작성하고자 한다.

프론트엔드 개발자(여기서는 타입스크립트, 자바스크립트를 주로 사용하는 개발자를 의미한다)로서 함수 오버로딩, 기본 매개변수, 화살표 함수등을 이용해 함수를 만들곤 한다.

하지만 이런 점이 Elixir의 함수 개념을 어색하게 만들곤 한다. 따라서 이 글에서는 아리티(arity)와 클라우저(Clauses)에 대해 알아보도록 하자.

아리티(Arity): 함수 식별의 열쇠

우선 "아리티"란 함수가 인자의 개수를 의미한다. 엘릭서에서는 함수의 이름과 아리티(인자 개수)의 조합으로 함수를 "고유하게 식별"한다. 예를 들어 엘릭서에서 add/2라고 적으면 이름이 'add'이고 인자를 2개를 받는 함수를 가리킨다.

add/1라는 함수는 같은 'add'이름이지만 인자 1개 짜리로, add/2와는 완전히 다른 별개의 함수이다. 이러한 엘릭서만의 함수 식별 방식 덕분에 엘릭서에서는 같은 이름을 가진 함수라도 인자 개수가 다르면 공존할 수 있다.

# Elixir 예제
defmodule Math do
  def add(a, b) do
    a + b
  end

  def add(a) do
    a + 10
  end
end

IO.puts Math.add(1, 2)  # 출력: 3
IO.puts Math.add(5)     # 출력: 15

하지만 타입스트립트의 경우, 함수는 보퉁 이름만으로 식별되고 인자 개수는 엄격한 식별자의 일부가 아니다. 자바스크립트 기준으로 함수를 호출할 때 인자를 덜 주거나 더 주어도 undefined로 처리하거나 무시될 뿐, 동일한 이름의 두 함수를 인자수로 구분하여 판단하지 않는다.

function add(a: number, b?: number): number {
  if (b !== undefined) {
    return a + b;
  } else {
    return a + 10;
  }
}

특히나 타입스크립트의 경우 인자 수가 많아지면 함수의 시그니처가 복잡해지기 때문에, "객체를 통한 전달"을 권장하기도 한다.

type AddParams = {
  a: number;
  b?: number;
};

function add({ a, b }: AddParams): number {
  return (b ?? 10) + a;
}

console.log(add({ a: 1 }));         // 11
console.log(add({ a: 1, b: 2 }));   // 3

이렇게 엘릭서의 Math.add와 달리 타입스크립트는 하나의 add 함수만 정의되어 있고, 함수 내부에서 매개변수를 처리하여 다른 결과를 반환하는 구조를 사용한다.

함수 클라우징과 패턴 매칭으로 여러 구현 정의하기

이제 함수 클라우징에 대해 알아보자. 이는 하나의 함수 이름에 대해 여러 개의 구현을 패턴매칭으로 정의하는 것을 의미한다. 앞서 아리티가 다른 함수들은 완전히 별개의 함수로 간주되었다고 알아보았다.

아리티가 다른 함수들은 별개의 함수이지만, 아리티가 동일한 함수들은 엘릭서에서 패턴 매칭을 기반한 하나의 함수의 여러 클라우즈로 취급된다. 즉, 같은 아리티를 가진 함수를 여러 개 작성하면, 호출 시 전달되는 인자에 가장 알맞게 매칭되는 클라우즈의 함수 코드가 실행되는 것이다.

이처럼 함수 클라우징을 활용하면 조건에 따라 함수 내부에서 if나 case로 분기하지 않고, 아예 함수를 여러 버전으로 작성하여 각각의 경우를 처리할 수 있다. 엘릭서 런타임은 함수를 호출할 때 코드의 상단부터 하단으로 함수의 매개변수 패턴을 시도하며, 처음으로 인자와 맞아떨어지는 정의를 찾아 실행한다.

만약 어떤 정의와도 매칭되지 않으면 FunctionClauseError 예외가 발생하며, 이를 위해서 기본 클라우즈와 와일드 카드 패턴등으로 함수를 방어하기도 한다.

또한 여기에 한걸을 더 나아가 함수 정의에서 패턴 매칭을 이용하여 가드(guards)라는 조건식을 추가하여 패턴만으로는 구분하기 어려운 세부 조건까지 명시할 수 있다. 가드는 when 키워드를 사용하여 추가적인 참/거짓 여부를 판단하는 작은 함수라고 보면 된다.

백문이불여일견이라, 코드로 보면 더욱 직관적으로 이해할수 있으니 한번 코드로 보자.

일단 타입스크립트는 다음처럼 if/else 분기를 사용한다.

// TypeScript: 숫자의 부호를 판별하는 함수
function sign(n: number): string {
  if (n === 0) {
    return "zero";
  } else if (n > 0) {
    return "positive";
  } else {
    return "negative";
  }
}

console.log(sign(0));   // "zero"
console.log(sign(42));  // "positive"
console.log(sign(-7));  // "negative"

이 코드를 엘릭서로 다시 작성해보자.

# Elixir: 패턴 매칭과 가드로 숫자의 부호 분류
defmodule SignChecker do
  def sign(0), do: "zero"                      # 값이 0인 패턴
  def sign(n) when n > 0, do: "positive"       # 0보다 큰 경우 (가드)
  def sign(n) when n < 0, do: "negative"       # 0보다 작은 경우 (가드)
end

IO.puts SignChecker.sign(0)    # "zero"
IO.puts SignChecker.sign(42)   # "positive"
IO.puts SignChecker.sign(-7)   # "negative"

위 코드는 SignChecker.sign/1 함수를 3 가지 클라우즈로 정의했다. 각각의 패턴은 0일 때와, 0보다 클 때, 0보다 작을 때이다. 그리고 엘릭서에서 이 함수를 호출할 때 전달된 값에 대해 이 패턴들을 상단부터 차례로 대입해 보며, 조건을 만족하는 클라우즈를 찾아 해당 함수를 실행한다.

이처럼 타입스크립트에서는 함수 본문 내부에서 if/else를 분기하지만, 엘릭서에는 아예 함수 정의를 3개로 나누어 코드를 작성해 버린다. 이 방식은 조건별로 함수 자체를 선언적으로 정의하며, 코드의 의도가 드러나고 가독성이 좋다고 한다. (일반적으로)

하지만 타입스크립트처럼 함수 내부에 분기를 타는 코드를 주로 작성해 보던 내게 이 부분은 "근본 개념 자체를 뒤흔드는 일이었다"

로직을 위해서 함수를 만들다니.. 그것도 조건식에 따라서 만들다니.. 말도 안 되는 일이고 코드를 작성하는 내내 이 개념이 익숙하지 않아서 많이 고생했다. 실은 지금도 어색하다.

조건문보다 패턴 매칭을 선호하는 이유

엘릭서에는 전통적인 if/else나 switch(elixir에서는 cond)를 상대적으로 덜 사용하는 경항이 있다. 실제로 엘릭서 개발자들은 일반적으로 패턴 매칭과 가드를 포함한 함수 정의를 선호하며, 로직을 패턴으로 표현할 수 없을 때 혹은 복잡해질 경우에 if/else나 switch 같은 조건문을 고려한다. 

간단한 예시로 switch/case를 포함한 타입스크립트 코드를 살펴보자.

function handleCommand(cmd: string) {
  switch (cmd) {
    case "start":
      // ...작업 A 수행...
      break;
    case "stop":
      // ...작업 B 수행...
      break;
    default:
      // ...기본 동작...
  }
}

이 코드는 엘릭서에서 다음처럼 작성해야 혼나지 않는다.

defmodule CommandHandler do
  def handle("start"), do: # ...작업 A 수행...
  def handle("stop"),  do: # ...작업 B 수행...
  def handle(_other),  do: # ...기본 동작 (catch-all)
end

 

타입스크립트의 개발자의 엘릭서 코딩 테스트

https://exercism.org/tracks/elixir/exercises/bird-count/iterations

 

Exercism

Learn, practice and get world-class mentoring in over 50 languages. 100% free.

exercism.org

해당 코테 문제는 재귀를 이용해서 문제를 풀어야 한다. 그리고 이 문제를 본 타입스크립트의 초기 코드는 아래처럼 작성했다.

# Elixir
defmodule BirdCount do
  def today(list) do
    if list == [] do
      nil
    else
      hd(list)
    end
  end

  def increment_day_count(list) do
    if list == [] do
      [1]
    else
      [head | tail] = list
      [head + 1 | tail]
    end
  end

  def has_day_without_birds?(list) do
    Enum.any?(list, fn x -> x == 0 end)
  end

  def total(list) do
    Enum.sum(list)
  end

  def busy_days(list) do
    Enum.count(list, fn x -> x >= 5 end)
  end
end

조금조금씩 엘릭서의 문법을 위해서 [head | tail] = list 식의 문법을 작성했다 (하지만 이는 자바스크립트의 구조분해할당이랑 비슷해서 익숙할 뿐이다.)

그래서 처음에 문제를 풀 때는 함수 내부에서 if else 분기를 타거나 Enum 모듈을 활용해서 total를 계산한다거나 카운팅 하는 방식으로 문제를 해결하곤 했다. 하지만 이 코드가 엘릭서의 의도와 개념을 활용하면 다음처럼 변경되어야 한다.

# Elixir
defmodule BirdCount do
  def today([]), do: nil
  def today([head | _]), do: head

  def increment_day_count([]), do: [1]
  def increment_day_count([head | tail]), do: [head + 1 | tail]

  def has_day_without_birds?([]), do: false
  def has_day_without_birds?([0 | _]), do: true
  def has_day_without_birds?([_ | tail]), do: has_day_without_birds?(tail)

  def total([]), do: 0
  def total([head | tail]), do: head + total(tail)

  def busy_days([]), do: 0
  def busy_days([head | tail]) when head > 4, do: 1 + busy_days(tail)
  def busy_days([_ | tail]), do: busy_days(tail)
end

 

마무리

엘릭서는 배우면 배울수록 나의 언어에 대한 틀을 깨 주고 있다. 그 첫 번째는 바로 아래와 같다.

조건에 따라 함수를 다르게 정의한다

타입스크립트는 조건식 내부에서 분기하는데 자연스러웠는데, 이젠 함수 자체를 분리하는 게 자연스러운 세계관으로 오게 되었다.

또한 if/else 같은 분기문이 아닌 재귀와 패턴 매칭으로 로직을 표현할 수 있다는 점이 참 매력적인 듯했다. RxJS나 함수형 프로그래밍을 배워보고 싶은 나에겐 좋은 교보재가 될 것 같아서 생각보다 재밌다.

끝.

반응형

댓글