이 글은 조제 발림이 엘릭서 언어 공식 블로그에 올린 Elixir Design Goals라는 글의 전문을 번역한 글입니다.

엘릭서 언어 디자인 목표

August 08, 2013 · by José Valim . in Internals

작년 한 해 동안 엘릭서를 소개하기 위해 여러 컨퍼런스에서 발표를 했습니다. 보통은 얼랭 VM을 소개하는 것부터 시작해서 엘릭서의 목표에 대해서 이야기하고, 마지막에는 라이브 시연 시간을 마련해서 리모트 노드간 정보 교환이나 핫 코드 스와핑 같은 멋진 기능을 보여주었습니다.

이 글은 그런 발표를 요약한 글로, 엘릭서 언어 목표인 호환성, 생산성 및 확장성을 집중적으로 다룹니다.

호환성

엘릭서는 얼랭 VM 및 기존 생태계와 호환되도록 설계되었습니다. 얼랭은 보통 다음과 같은 세 부분으로 나눌 수 있습니다.

  • 얼랭이라 불리는 함수형 프로그래밍 언어
  • OTP라 불리는 디자인 원칙의 집합체
  • EVM 또는 BEAM이라 불리는 얼랭 가상 머신

엘릭서는 얼랭과 동일한 가상 머신에서 작동하며 OTP와 호환됩니다. 그뿐 아니라 얼랭 생태계에서 사용 가능한 모든 도구와 라이브러리를 엘릭서에서도 사용할 수 있습니다. 얼랭에서 엘릭서를 호출하는 것과 그 반대 경우 모두에 전환 비용이 없기 때문입니다.

우리는 얼랭 VM이 엘릭서의 가장 큰 자산이라고 이야기하곤 합니다.

모든 엘릭서 코드는 가벼운 프로세스(액터) 내에서 실행되는데, 각 프로세스는 자신만의 상태를 가지고 있고 서로 메시지를 주고 받습니다. 얼랭 VM은 그런 프로세스들을 여러 코어에 분배해주어서 코드를 정말 쉽게 병렬적으로 실행할 수 있도로 해줍니다.

실제로 엘릭서 소스 코드를 비롯해 어떤 엘릭서 코드를 컴파일해도 기본적으로 머신의 모든 코어를 사용하는 것을 볼 수 있습니다. Parallella같은 기술이 더 저렴해지고 접근성이 증가하는 현 상황에서 얼랭 VM을 통해서 얻을 수 있는 능력을 간과하기는 어렵습니다.

마지막으로 얼랭 VM은 영원히 동작하고 스스로를 복구하며 스케일하는 시스템을 만들기 위해 디자인되었습니다. 얼랭의 개발자 중 한 명인 조 암스트롱이 OTP와 얼랭 VM의 디자인적 결정사항에 대해서 최근에 훌륭한 발표를 한 적이 있습니다.

우리가 위에 적은 내용은 특별히 새로운 것은 아닙니다. CouchDB, Riak, RabbitMQ, Chef11 등의 오픈소스 프로젝트와 Ericsson, Heroku, Basho, Klarna, Wooga 등의 기업은 얼랭 VM의 장점을 이미 활용하고 있습니다. 그 중에는 꽤 오래 전부터 이를 활용해온 곳도 있습니다.

생산성

이제 메타적인 주제로 넘어가 봅시다. 우리는 이제 언어 디자인을 언어 디자인에 대한 패턴이라고 생각해야만 합니다. 동일한 종류의 도구를 더 만들기 위한 도구 말입니다. […] 언어 디자인은 어떤 하나의 무엇인가여서는 안 됩니다. 언어 디자인은 패턴, 즉 성장을 위한 패턴이어야만 합니다. 패턴을 키워내기 위한 패턴이자, 프로그래머들이 자신의 실제 업무와 주 목표를 위해 사용하는 패턴을 정의할 수 있는 패턴이어야 합니다.

  • 가이 스틸, 1998 ACM OOPSLA 컨퍼런스 “Growing a Language”라는 기조연설에서

생산성은 일반적으로 측정하기 어려운 목표입니다. 데스크탑 어플리케이션을 만들 때 생산적인 언어는 수학적 계산을 할 때에는 생산적이지 않을 수 있습니다. 생산성은 사용자가 언어를 사용하려고 하는 분야, 생태계 내에서 사용 가능한 도구, 그리고 해당 도구를 얼마나 쉽게 만들고 확장할 수 있는 지에 직접적으로 영향을 받습니다.

이런 이유로 우리는 언어의 핵심부를 작게 만들기로 결정했습니다. 예를 들어 일부 언어에서 if, case, try 등은 해당 언어의 파서에서 자신만의 규칙을 가진 키워드들이지만, 엘릭서에서는 모두 그냥 매크로일 뿐입니다. 덕분에 우리는 엘릭서의 대부분을 엘릭서를 사용해서 구현할 수 있었습니다. 또한 매크로는 개발자들이 우리가 언어를 만들기 위해서 사용했던 것과 동일한 도구를 사용해서 자신들이 작업하는 특정 분야에 맞추어 언어를 확장할 수 있도록 해줍니다.

여러 언어에 키워드로 구현되어 있는 unless를 엘릭서에서 구현하는 예시입니다.

defmacro unless(expr, opts) do
  quote do
    if(!unquote(expr), unquote(opts))
  end
end

unless true do
  IO.puts "this will never be seen"
end

코드 표현을 인자로 받는 매크로 덕분에 우리는 컴파일 타임에 unlessif로 간단히 변환할 수 있습니다.

매크로는 엘릭서의 메타프로그래밍, 즉 코드를 생성하는 코드를 작성할 수 있는 기능의 기본적인 구성요소이기도 합니다. 메타프로그래밍은 개발자가 손쉽게 보일러플레이트 코드를 제거하고 강력한 도구를 만들 수 있게 해줍니다. 발표에서 테스트 프레임워크의 표현력을 위해서 매크로를 사용한다는 예시를 자주 사용했었습니다. 한 번 그 예시를 살펴봅시다.

ExUnit.start

defmodule MathTest do
  use ExUnit.Case, async: true

  test "adding two numbers" do
    assert 1 + 2 == 4
  end
end

처음 눈에 띄는 것은 async: true 옵션입니다. 테스트에 사이드 이펙트가 없으면 async: true 옵션을 넣어서 병렬로 테스트를 실행할 수 있습니다.

다음으로 테스트 케이스를 정의하고 assert 매크로를 사용해서 어설션을 만듭니다. 그냥 assert를 호출하면 별로 도움이 되지 않는 에러 리포트가 나올 것이기 때문에 여러 언어에서 권장되는 방법은 아닐 것입니다. 그런 언어에서는 해당 어설션을 수행할 때 assertEqual이나 assert_equal 같은 함수나 메서드를 사용하는 것을 권장할 것입니다.

하지만 엘릭서의 assert는 매크로이기 때문에, 어설션의 대상이 되는 코드를 살펴보고 해당 코드가 비교를 실행한다는 것을 추론할 수 있습니다. 그렇게 분석한 후 해당 코드를 변환하여, 테스트가 실행되면 구체적인 에러 리포트를 제공하도록 만듭니다.

1) test adding two numbers (MathTest)
   ** (ExUnit.ExpectationError)
                expected: 3
     to be equal to (==): 4
   at test.exs:7

이 간단한 예시에서 개발자가 매크로를 사용해서 간결하지만 강력한 API를 제공할 수 있는 방법을 볼 수 있습니다. 매크로는 컴파일 환경 전체에 접근할 수 있기 때문에 임포트된 함수, 매크로, 정의된 변수 등을 확인하고 사용할 수 있습니다.

위 예시는 매크로를 사용해서 엘릭서에서 할 수 있는 것들의 극히 일부분일 뿐입니다. 예를 들어 우리는 매크로를 통해서 표현력이 높지만 고도로 최적화된 라우팅 알고리즘을 제공하고, 이를 사용해서 웹 어플리케이션의 라우트를 VM에서 고도로 최적화할 수 있는 패턴의 집합체로 컴파일하고 있습니다.

매크로 시스템은 또한 문법에도 큰 영향을 미쳤습니다. 마지막 주제로 넘어가기 전에 문법에 대해서 간략히 이야기하도록 하겠습니다.

문법

문법은 엘릭서에 대해서 이야기할 때 보통 가장 먼저 나오는 주제 중 하나이긴 하지만, 우리의 목표는 단순히 다른 문법을 제공하는 것이 아닙니다. 우리는 매크로 시스템을 제공하고 싶었고, 그러기 위해서는 엘릭서의 자체적인 데이터 구조를 사용해서 엘릭서 문법을 명시적으로 표현할 수 있어야만 했습니다. 이런 목표를 염두에 두고 우리가 설계한 최초의 엘릭서 버전은 다음과 같이 생겼었습니다.

defmodule(Hello, do: (
  def(calculate(a, b, c), do: (
    =(temp, *(a, b))
    +(temp, c)
  ))
))

위 코드에서는 변수를 제외한 모든 것을 함수 또는 매크로 호출로 표현했습니다. 첫 버전에서부터 do: 같은 키워드 인자가 존재했다는 점에 주목해주세요. 우리는 여기에다가 새로운 문법을 천천히 추가해서, 자주 사용하는 패턴을 보다 세련되게 만드는 동시에 그 기반이 되는 데이터적 표현의 동일성을 유지했습니다. 연산자에도 곧 인픽스 표기법을 추가했습니다.

defmodule(Hello, do: (
  def(calculate(a, b, c), do: (
    temp = a * b
    temp + c
  ))
))

괄호를 선택사항으로 만드는 것이 다음 단계였습니다.

defmodule Hello, do: (
  def calculate(a, b, c), do: (
    temp = a * b
    temp + c
  )
)

그리고 마지막으로 자주 사용되는 do: (...) 구성요소를 더 간편하게 쓸 수 있도록 do/end를 추가했습니다.

defmodule Hello do
  def calculate(a, b, c) do
    temp = a * b
    temp + c
  end
end

제게 루비 경력이 있는 만큼 추가된 구성요소 중 일부를 루비에서 빌려오는 것은 자연스러운 수순이었습니다. 하지만 그렇게 추가된 부분은 부산물이었지 언어의 목표는 아니었습니다.

언어 구문 중 여럿은 또한 그에 대응되는 얼랭 구성요소에서 따왔습니다. 제어 흐름 매크로, 연산자, 컨테이너 등이 그 예시입니다. 일부 엘릭서 코드가 어떻게 얼랭에 대응하는지 보세요.

# A tuple
tuple = { 1, 2, 3 }

# Adding two lists
[1, 2, 3] ++ [4, 5, 6]

# Case
case expr do
  { x, y } -> x + y
  other when is_integer(other) -> other
end
% A tuple
Tuple = { 1, 2, 3 }.

% Adding two lists
[1, 2, 3] ++ [4, 5, 6].

% Case
case Expr of
  { X, Y } -> X + Y;
  Other when is_integer(Other) -> Other
end.

확장성

엘릭서는 작은 핵심부에 기반을 두고 만들어진 언어이기 때문에, 개발자들이 개발 대상으로 삼은 특정 분야에 따라 언어 구성요소의 대부분을 필요한 대로 교체하고 확장할 수 있습니다. 하지만 엘릭서가 본질적으로 뛰어난 특정 분야가 있는데, 이는 병렬 분산 어플리케이션을 만드는 분야입니다. OTP와 얼랭 VM 덕분입니다.

엘릭서는 다음과 같은 특징을 가진 표준 라이브러리를 제공하여 이 분야를 더욱 보완합니다.

  • 유니코드 문자열 및 유니코드 연산
  • 강력한 유닛 테스트 프레임워크
  • 새롭게 구현한 셋과 딕셔너리, 그리고 레인지 같은 추가적인 데이터 구조
  • (컴파일 타임에 한정된 얼랭의 레코드와 대비되는) 폴리모픽 레코드
  • 스트릭트 및 레이지한 열거 API
  • 경로나 파일시스템 등 스크립팅용 편의성 함수
  • 엘릭서 코드를 컴파일하고 테스트할 수 있는 프로젝트 관리 툴

그리고 그 외에도 많이 있습니다.

위에 언급된 기능 대부분에는 자체적인 확장 메커니즘이 있습니다. 예를 들어 Enum 모듈을 봅시다. Enum 모듈은 내장된 레인지, 리스트, 셋 등의 데이터 타입을 열거할 수 있도록 해줍니다.

list = [1, 2, 3]
Enum.map list, fn(x) -> x * 2 end
#=> [2, 4, 6]

range = 1..3
Enum.map range, fn(x) -> x * 2 end
#=> [2, 4, 6]

set = HashSet.new [1, 2, 3]
Enum.map set, fn(x) -> x * 2 end
#=> [2, 4, 6]

그 뿐 아니라 모든 개발자는 어떤 데이터 타입에건 Enumerable 프로토콜을 구현하기만 하면 Enum 모듈을 익스텐드해서 해당 데이터 타입에 사용할 수 있습니다. 개발자가 셋, 리스트, 딕셔너리용 개별적 API를 외우는 대신 Enum API만 알면 열거를 할 수 있게 해주는 매우 간편한 방식입니다.

언어에서 제공하는 프로토콜은 이 외에도 더 많이 있습니다. 데이터 구조를 보기 좋게 출력해주는 Inspect 프로토콜도 있고, 키를 사용해서 키-밸류 데이터에 접근할 수 있는 Access 프로토콜도 있습니다. 엘릭서는 확장이 가능하기 때문에 개발자가 언어와 싸우는 대신 협력해서 개발할 수 있도록 해줍니다.

요약

이 글의 목적은 호환성, 생산성, 확장성이라는 엘릭서 언어 목표를 요약하는 것이었습니다. 우리는 얼랭 VM과 호환성을 유지함으로써 개발자들에게 병행, 분산, 장해 허용 시스템을 만들 수 있는 또다른 툴셋을 제공하고 있습니다.

또한 엘릭서가 얼랭 VM에 무엇을 제공하는지가 이 글에서 명확히 전달되었기를 바랍니다. 구체적으로는 매크로를 통한 메타프로그래밍, 확장성을 위한 폴리모픽 구성요소, 다양한 타입에 대한 확장성 있고 일관적인 표준 라이브러리가 제공됩니다. 여기에는 스트릭트 및 레이지 열거, 유니코드 처리, 테스트 프레임워크 등도 포함됩니다.

엘릭서를 한 번 사용해 보세요! 공식 엘릭서 시작 가이드로 시작하거나, 아니면 사이드바에서 타 학습자료를 확인해볼 수 있습니다.