Page

관련 포스팅

2023년 9월 6일 수요일

Week01 - Elixir 기초 문법





오늘 배운 부분에 대해서 정리한다. 오늘은 가장 기초적인 Elixir에 대해서 배웠다.

우선은 이 언어에서 사용하는 생소한 표현과 문법 위주로 정리를 해보려 한다.


Atom이란?


Atom은 Elixir에서 기본 데이터 타입 중 하나로, 상수적인 값이며 주로 식별자나 이름으로 사용된다. 즉, Elixir 프로그래밍에서 이름표 같은 역할을 하는 것이며, :ok, :error 같은 것들이 Atom이다. Atom은 :으로 시작하며, 대소문자를 구분한다.

Elixir에서 모듈의 이름은 Atom으로 처리되고, 이 이름으로 모듈을 찾거나 사용할 수 있다. 따라서 보다 쉽게 이해하려면 Atom은 모듈의 이름표라고 생각하면 된다.


Atom의 주요 용도

  • 식별자: 성공과 실패를 판단
  • Atom은 상태, 결과, 카테고리 등을 나타낼 때 자주 사용된다. 예를 들어 함수의 리턴값으로 성공과 실패를 나타낼 때 :ok와 :error를 사용한다.

{:ok, "성공했어!"}

  • Map의 키: 정보 정리
  • Atom은 Map 데이터 구조에서 key로 자주 사용된다. 즉, 사람 이름, 나이 같은 정보를 정리할 때 Atom을 key로 사용해서 정리한다.

%{name: "철수", age: 30}

  • 모듈 접근
  • 모듈 이름도 Atom으로 처리된다. 즉, 모듈을 부를 때, 그 이름이 내부적으로 Atom으로 처리된다는 뜻이다.
  • 예를 들어, Elixir에서 자주 사용되는 Enum 모듈이 있다고 하면, Enum 이라는 이름 자체가 하나의 Atom이라는 것이다. 조금 더 쉽게 설명하면, Enum 이라는 상자(모듈) 안에는 리스트를 다루는 도구들이 있고, 이 상자의 이름표가 Enum이라고 생각하면 된다. Elixir에서는 이 Enum 이름표를 Atom으로 보고 다룬다는 것이다. 그래서 코드 안에서 Enum 이라는 모듈을 부르면, Elixir는 내부적으로 이 Enum이라는 이름표를 Atom으로 인식하고 처리하게 된다.

Enum.map([1, 2, 3], fn x -> x * 2 end)

  • 메타데이터
  • 프로세스, 노드, 모듈 등에 대한 메타데이터를 표현할 때도 Atom이 사용된다.


Atom의 특징

  • 불변성
  • Atom은 불변(immutable)이다. 한 번 생성되면 변경할 수 없다. 따라서 다른 것과 비교할 때 빠르다.
  • 성능
  • Atom은 내부적으로 정수로 관리되므로, 문자열보다 빠르고 효율적인 비교가 가능하다.
  • 메모리
  • Atom은 가비지 컬렉션의 대상이 아니다. 따라서 동적으로 많은 수의 Atom을 생성하는 것은 메모리 누수를 일으킬 수 있다.
  • 글로벌 식별자
  • Atom은 모든 Elixir 노드에 걸쳐서 유일하다. 이는 분산 시스템에서 특히 유용하다.

Atom은 심플하면서도 표현력이 높아, Elixir 프로그래밍에서 자주 볼 수 있는 데이터 타입이다.

: (colon)과 #(hash / pound)의 구분과 용법

  • : (colon)
  • Atom 생성
  • Elixir에서 :atom과 같이 쓰이면, Atom을 나타낸다. Atom은 상수적인 값으로, 주로 식별자나 이름으로 사용된다.

                        

:ok

:error

  • Map 접근
  • Map 데이터 타입에서 키로 Atom을 사용할 때 :를 사용한다.

                        

map = %{name: "Alice", age: 30}

map[:name]  # "Alice"

  • Module 호출
  • 다른 언어의 네임스페이스와 유사하게, Elixir에서 모듈을 호출할 때 :를 사용한다.

Enum.map([1, 2, 3], fn x -> x * 2 end)

  • # (Hash/Pound)
  • 주석 처리
  • Elixir에서 #은 주석을 나타낸다. 이 문자 뒤에 오는 모든 텍스트는 주석으로 처리되어 실행되지 않는다.
  • Sigil
  • Elixir의 Sigils에서 정규 표현식이나 다른 텍스트 표현을 쓸 때 #가 사용될 수 있다. 이것은 주로 문자열 내에서 특수 문자를 쉽게 다루기 위해 사용된다.
  • Record 구분
  • Erlang과 호환성을 유지하기 위해, 때로는 Record를 다룰 때 #이 사용될 수 있다. 하지만 이는 특별한 경우이며 Elixir 프로그래밍에서 일반적이지 않다.

이렇게 :와 #은 각각 여러 가지 목적과 용법으로 사용되며, 문맥에 따라 그 의미가 달라질 수 있다.

연산자 and와 or의 특징: short circuit

Elixir에서 and 연산자는 첫 번째 피연산자가 true일 때만 두 번째 피연산자를 평가하고 그 값을 반환한다. 즉, 첫 번째 조건이 true이면 두 번째 값을 그대로 반환하는 것이다.

예를 들어서, 아래와 같이 연산을 수행하게 되면 결과 값으로 2가 나온다.

(1 < 2) and 2

2

(1 < 2)는 true이므로, and 연산자는 두 번째 피연산자인 2를 평가하고 그대로 반환한다. 그래서 결과가 2가 나오는 것이다.

이렇게 Elixir의 and 연산자는 두 번째 피연산자의 값을 반환할 수도 있는데, 이는 언어 디자인의 일부로 다른 언어에서도 종종 볼 수 있는 특징이다.

그리고 중요한 점은 and 연산자를 사용할 때, 첫 번째 피연산자는 반드시 boolean 값이어야 한다는 것이다.


2 and 1 < 2

** (BadBooleanError) expected a boolean on left-side of "and", got: 2

    iex:2: (file)

위와 같이 and 앞에 2를 넣게되면, 이것은 boolean type이 아니기 때문에 연산 자체가 성립되지 않아서 에러가 나는 것이다. 정리하자면, and 연산자를 사용할 때 첫번째 값은 반드시 true 또는 false여야 하고 그렇지 않으면 에러가 난다.


and와 &&의 공통점, 차이점


and와 &&는 비슷하게 동작하지만, 중요한 차이가 있다.

  • 피연산자 타입:
  • and: 첫 번째 피연산자가 반드시 boolean이어야 하며, 아니면 에러가 발생한다.
  • &&: 첫 번째 피연산자가 false나 nil일 때만 평가가 멈추고, boolean이 아니어도 괜찮다.
  • 반환 값:
  • and: 첫 번째 피연산자가 true일 때, 두 번째 피연산자의 값을 반환한다.
  • &&: 첫 번째 피연산자가 false나 nil이면 그 값을 반환하고, 아니면 두 번째 피연산자의 값을 반환한다.

예를 들어,

  • true and 2는 2를 반환하지만, true && 2도 2를 반환한다.
  • 1 and true는 에러를 발생시키지만, 1 && true는 true를 반환한다.

즉, &&는 좀 더 유연하게 동작하는 반면, and는 첫 번째 피연산자가 꼭 boolean이어야 하는 등 좀 더 엄격한 규칙을 따른다고 볼 수 있다.


Pattern Matching

[h|t] = [2,:ok, "hello"]

[2, :ok, "hello"]

Elixir에서 리스트를 패턴 매칭할 때 [h|t] 형태는 매우 일반적인 방식이다. 여기서 h와 t는 각각 "head"와 "tail"을 의미한다.

  • h: 리스트의 첫 번째 원소, 여기서는 2
  • t: 첫 번째 원소를 제외한 나머지 리스트, 여기서는 [:ok, "hello"]
  • |: 이 기호는 "head"와 "tail"을 구분해주는 역할을 한다.



이런 방식으로 패턴 매칭을 하면, 리스트의 첫 번째 원소와 나머지를 쉽게 분리할 수 있다. 이게 왜 유용하냐면, 재귀적으로 리스트를 다룰 때 매우 편리하기 때문이다.


간단한 패턴 매칭의 예시는 아래와 같다.

  • [a, b, c] = [1, 2, 3]
  • 결과: a에는 1, b에는 2, c에는 3
  • {x, y} = {4, 5}
  • 결과: x에는 4, y에는 5
  • [head | tail] = [6, 7, 8]
  • 결과: head에는 6, tail에는 [7, 8]
  • {:ok, result} = {:ok, "success"}
  • 결과: result에는 "success"
  • [first, second | _rest] = [9, 10, 11, 12]
  • 결과: first에는 9, second에는 10
  • [_, _, {:ok, x} | _] = [1,2,{:ok, 3},4]
  • 첫 번째 _: 첫 번째 원소인 1에 매칭되지만, _는 무시되므로 값을 저장하지 않는다.
  • 두 번째 _: 두 번째 원소인 2에 매칭되지만, _는 무시되므로 값을 저장하지 않는다.
  • {:ok, x}: 세 번째 원소인 {:ok, 3}에 매칭되고, x는 3에 매칭된다.
  • 마지막 _: 마지막 원소인 4와 나머지 리스트가 매칭되지만, _는 무시되므로 값을 저장하지 않는다.
  • 결론적으로 변수 x에는 3이 저장된다.

이 예제들에서 각 변수는 오른쪽에 있는 값들로 매칭되어 있다.. 이렇게 하면 변수를 이용해 데이터의 특정 부분만을 쉽게 뽑아낼 수 있게된다.

Guard Test란?

guard 테스트는 Elixir에서 함수의 패턴 매칭을 좀 더 세밀하게 제어할 수 있게 해주는 기능이다. guard 테스트는 when 키워드를 사용해서 함수의 헤더에 추가할 수 있다. 이 테스트는 특정 조건이 참일 때만 함수가 호출되게 하거나, 조건에 따라 다른 버전의 함수를 호출하게 할 수 있다.

예를 들어, 숫자가 양수인지 검사하는 함수를 만들고 싶다면 guard를 이렇게 사용할 수 있다:

defmodule Test do

  def check_number(n) when n > 0 do

    "양수다"

  end

  def check_number(n) when n <= 0 do

    "양수가 아니다"

  end

end

여기서 when n > 0과 when n <= 0이 바로 guard 테스트다. 이 테스트들은 n이 양수인지 아닌지를 검사하고, 그에 따라 적절한 함수 버전이 호출된다.

guard 테스트는 단순하고 빠른 연산만을 사용해야 한다. 예를 들어, 복잡한 함수를 호출하거나 변수에 값을 할당할 수 없다. 주로 타입 검사나 산술 연산, 논리 연산 등을 수행한다.

그래서 guard 테스트는 코드를 더 읽기 쉽고 관리하기 쉽게 만들어주며, 동시에 더 효율적인 패턴 매칭을 할 수 있게 해준다.

AnAtom

Elixir에서 AnAtom 같은 표현은 Atom의 한 예일 가능성이 높다. Atom은 심볼이나 식별자로 쓰이며, :ok, :error, :name 같은 형태로 표현된다. Atom은 변경 불가능하고, 메모리에서 한 번만 저장된다. 같은 이름의 Atom이 여러 번 사용되면, 모두 같은 메모리 주소를 참조한다.

AnAtom이라는 표현이 코드에서 :AnAtom으로 사용되었다면, 이것은 "AnAtom"이라는 이름을 가진 Atom일 것이다. 이런 Atom은 보통 상태를 나타내거나, 특별한 값 없이 어떤 것을 표식하기 위해 사용된다.

간단하게 말하면, :AnAtom은 "AnAtom"이라는 이름을 가진 하나의 Atom이다. 이 Atom은 다른 데이터와 비교될 수 있고, 함수의 인자나 리턴 값으로 사용될 수 있다. 예를 들어, 함수가 문제 없이 동작했을 때 :ok를 리턴하고, 오류가 있을 때 :error를 리턴하는 식으로 Atom을 사용할 수 있다.

AnAtom == :"Elixir.AnAtom" == Elixir.AnAtom 이 표현은 Elixir에서 Atom을 다양한 방법으로 표현할 수 있다는 것을 보여주는 예시이다.

  • AnAtom
  • 이 형태는 Atom을 간단하게 표현한 것이다. Elixir에서는 이렇게 쓰면 자동으로 :"Elixir.AnAtom" 형태의 전체 이름으로 변환해 준다.
  • :"Elixir.AnAtom"
  • 이 형태는 Atom의 전체 이름을 명시적으로 표현한 것이다. 기본적으로 Elixir 코드에서 만든 Atom은 모두 Elixir. 프리픽스가 붙는다.
  • Elixir.AnAtom
  • 이것도 Atom을 표현하는 또 다른 방법이다. Elixir에서 이런 형태로 쓰면 내부적으로 :"Elixir.AnAtom"과 같이 처리된다.


세 가지 표현 모두 같은 Atom을 가리키므로, 이들은 모두 같다(==)고 판단될 것이다. 이러한 다양한 표현 방법은 코드에서 Atom을 더 유연하게 다룰 수 있게 해준다.

Module name은 항상 대문자로 시작한다.

이는 Elixir의 컨벤션 중 하나다. 대문자로 시작하는 이름은 Atom으로 처리되고, 모듈 이름으로 사용된다. 예를 들어, MyModule, String, Enum 같은 모듈 이름들은 모두 대문자로 시작한다.

이렇게 하면 모듈과 다른 데이터 타입을 쉽게 구분할 수 있어서, 코드를 읽거나 디버깅할 때 편리하다. 그래서 모듈을 정의할 때는 이름을 대문자로 시작하는 것을 권장한다.

여기서 대문자로 시작하는 이름이 Atom으로 처리된다는 것은, Elixir에서 모듈 이름은 사실상 특별한 형태의 Atom이라는 뜻이다. 이 Atom은 대개 Elixir.라는 프리픽스가 붙은 형태로 내부적으로 관리된다.

예를 들어, MyModule이라는 모듈을 만들면, 이 모듈의 이름은 실제로는 :"Elixir.MyModule"이라는 Atom으로 처리된다. 즉, 모듈 이름을 사용할 때는 이것이 Atom이라는 점을 이해하면 도움이 된다. 예를 들어, 모듈 이름을 다른 변수와 비교하거나, 동적으로 모듈을 호출할 때 이러한 특성이 유용하게 쓰일 수 있다.

간단하게 말하면, 모듈 이름은 특별한 종류의 Atom이고, 이를 이해하면 Elixir 프로그래밍에서 더 다양한 작업을 할 수 있다.

apply(List, :flatten, [[1,[2],3]])

[1, 2, 3]

예를 들어, apply(List, :flatten, [[1,[2],3]]) 명령어는 List 모듈의 flatten 함수를 [1, [2], 3] 이라는 인자와 함께 호출한다. apply/3 함수는 첫 번째 인자로 모듈 이름, 두 번째 인자로 호출할 함수의 이름, 그리고 세 번째 인자로 함수에 전달할 인자 리스트를 받는다.

  • List: 호출할 모듈 이름
  • :flatten: 호출할 함수 이름
  • [[1,[2],3]]: flatten 함수에 전달할 인자
  • List.flatten/1 함수는 리스트를 평탄화한다.

즉, 중첩된 리스트를 하나의 단일 리스트로 만든다.

이 명령어를 실행하면, [1, [2], 3] 리스트가 평탄화되어 [1, 2, 3]이라는 단일 리스트가 반환된다.

간단하게 요약하면, apply(List, :flatten, [[1,[2],3]])는 [1, [2], 3]을 평탄화해서 [1, 2, 3]을 반환한다.

/ (slash)의 의미

apply/3에서 / 뒤에 오는 숫자 3은 함수의 아리티(arity)를 나타낸다. 아리티는 함수가 몇 개의 인자를 받는지를 표현하는 숫자다. apply 함수는 총 3개의 인자를 받으므로, 아리티는 3이며 apply/3으로 표현한다.

Elixir에서는 같은 이름을 가진 함수라도 아리티가 다르면 다른 함수로 취급한다. 예를 들어 foo/1과 foo/2는 이름은 같지만 아리티가 다르므로 서로 다른 함수다.

이런 표기법은 함수를 명확하게 구분하고 문서나 코드에서 특정 함수를 가리킬 때 유용하다.




Private function

defp는 Elixir에서 "private function"을 정의할 때 사용하는 키워드다. def로 정의한 함수는 모듈 외부에서도 호출할 수 있지만, defp로 정의한 함수는 해당 모듈 내에서만 사용할 수 있다.

예를 들어, 아래와 같은 모듈이 있다고 하자.

defmodule MyModule do

  def public_function do

    IO.puts "I can be called from outside this module."

    private_function

  end

  defp private_function do

    IO.puts "I can only be called within this module."

  end

end

여기서 public_function은 모듈 외부에서도 호출 가능하다. 하지만 private_function은 defp로 정의되어 있어서 모듈 내부에서만 호출할 수 있다.

간단히 말하면, defp는 해당 함수를 해당 모듈 내부에서만 사용하려는 경우에 사용한다. 이렇게 하면 함수의 접근 범위를 제한하여 코드를 더 안전하고 관리하기 쉽게 만든다.

Elixir에는 else가 없는가?

Elixir에서 else 키워드는 있고, 주로 if, unless, cond와 같은 조건문에서 사용된다. 그러나 Elixir에서는 else 없이도 패턴 매칭과 함수의 다중 정의를 통해 복잡한 로직을 간결하게 표현할 수 있기 때문에, else 없이도 많은 것을 할 수 있다.

예를 들어, 패턴 매칭을 사용한 함수 정의를 보자:

defmodule Test do

  def check_value(true), do: "It's true"

  def check_value(false), do: "It's false"

end

이 경우 check_value/1 함수는 입력값에 따라 다른 구현이 실행되므로, 별도의 else 없이도 여러 케이스를 처리할 수 있다.

또는 case 문을 사용할 수도 있다:

case value do

  true -> "It's true"

  false -> "It's false"

  _ -> "It's something else"

end

else 키워드를 사용한 예는 아래와 같다:

if value > 0 do

  "Positive"

else

  "Non-positive"

end

또는 cond에서도 사용 가능하다:

cond do

  value > 0 -> "Positive"

  value < 0 -> "Negative"

  true -> "Zero"

end

따라서 Elixir에서는 else가 있지만, else 없이도 다양한 방법으로 조건 처리를 할 수 있다.


Case문 사용 방법

case 문은 Elixir에서 여러 분기를 패턴 매칭을 통해 처리할 때 사용한다. case 문은 주어진 값에 대한 여러 패턴을 검사하고, 첫 번째로 일치하는 패턴의 코드 블록을 실행한다. 문법은 다음과 같다.

case 표현식 do

  패턴1 -> 실행할_코드1

  패턴2 -> 실행할_코드2

  ...

  _ -> 기본_실행할_코드

end

예를 들어, 정수 값에 따라 메시지를 출력하는 간단한 예제를 살펴보자.

case x do

  1 -> IO.puts "x is one"

  2 -> IO.puts "x is two"

  _ -> IO.puts "x is something else"

end

여기서 _는 와일드카드 패턴으로, 어떤 값과도 매칭된다. 그래서 위 코드에서 x가 1이나 2가 아닌 경우에는 "x is something else"가 출력된다.

case 문에서는 더 복잡한 패턴 매칭도 가능하다. 예를 들어, 리스트의 첫 번째 원소가 :ok일 때만 처리하고 싶다면 다음과 같이 할 수 있다.

case list do

  [:ok | _] -> IO.puts "First element is :ok"

  _ -> IO.puts "First element is not :ok"

end

이렇게 case 문을 사용하면 여러 분기를 패턴 매칭을 통해 깔끔하게 처리할 수 있다.


Anonymous function

익명 함수(anonymous function)는 이름 없이 정의되는 함수를 의미한다. Elixir에서는 fn 키워드를 사용해 익명 함수를 정의하고, .을 이용해 함수를 호출한다.


정의 방법

익명 함수를 정의하는 기본 구조는 아래와 같다.

변수 = fn 인자1, 인자2, ... -> 실행할 코드 end

예를 들어, 두 수를 더하는 익명 함수는 다음과 같다.

add = fn a, b -> a + b end

호출 방법

익명 함수를 호출할 때는 함수 오브젝트 뒤에 .을 붙이고 인자를 넘긴다.

result = add.(1, 2)  # 결과는 3

다중 패턴 매칭

익명 함수도 다중 패턴 매칭을 지원한다. fn과 end 사이에 여러 개의 ->를 사용해 여러 분기를 만들 수 있다.

f = fn

  :ok -> "Everything is OK"

  :error -> "Something went wrong"

  _ -> "Unknown"

end

IO.puts(f.(:ok))  # "Everything is OK" 출력

변수 캡쳐

익명 함수는 그 함수가 정의된 지역의 변수를 캡쳐할 수 있다.

x = 10

multiply = fn y -> x * y end

IO.puts(multiply.(2))  # 20 출력

이렇게 익명 함수는 특정 작업을 수행하는 코드 블록을 변수에 저장해 다른 함수로 전달하거나 나중에 실행할 수 있게 해준다. 익명 함수는 고차 함수(higher-order function)를 사용할 때 유용하다.

return value

Elixir에서 모든 표현식은 값을 반환한다. 즉, 'return value가 없다'라는 개념이 사실상 존재하지 않는다. 예를 들어, if 문도 값을 반환하고, case 문도 값을 반환한다.

그러나 일반적으로 '값을 반환한다'라고 표현하는 경우와 '값을 반환하지 않는다'라고 표현하는 경우가 있는데, 이는 대개 '사이드 이펙트(side effect)'의 유무와 관련이 있다.


값이 '중요한' 경우

함수나 표현식의 주 목적이 어떤 값을 계산해서 반환하는 것일 때, 그 반환 값이 '중요하다'고 할 수 있다. 예를 들어, 두 수를 더하는 함수나 문자열을 대문자로 변환하는 함수 등은 그 결과 값이 중요하다.

result = String.upcase("hello")  # "HELLO" 반환, 중요한 값


값이 '중요하지 않은' 경우 (사이드 이펙트 중심)

반면에, IO 작업을 수행하거나 외부 상태를 변경하는 등의 '사이드 이펙트'가 주 목적인 함수나 표현식은 그 반환 값이 크게 중요하지 않을 수 있다. 이런 경우에는 보통 '값을 반환하지 않는다'고 설명하기도 한다.

IO.puts("Hello, World!")  # "Hello, World!"를 출력하고 :ok를 반환, 반환 값은 중요하지 않음

이 예에서 IO.puts/1 함수는 화면에 문자열을 출력하는 것이 주 목적이고, 반환 값인 :ok는 그다지 중요하지 않다.

따라서 Elixir에서는 모든 표현식이나 함수가 값을 반환하지만, 그 값의 '중요성'은 함수나 표현식의 주된 목적에 따라 다르다. 이 차이를 이해하면 함수의 역할과 사용법을 더 잘 파악할 수 있다.

익명함수(Anonymous function) 의 간소화

Elixir에서 & 기호는 주로 익명 함수(anonymous function)를 더 간단하게 표현할 때 사용한다. 다음과 같은 상황에서 &를 붙여서 사용한다.


익명 함수 간소화

일반적으로 익명 함수를 정의할 때 fn과 end를 사용한다. 하지만 &를 사용하면 코드를 더 간단하게 만들 수 있다.

# 일반적인 익명 함수

add = fn a, b -> a + b end

# &를 사용한 간단한 표현

add = &(&1 + &2)

여기서 &1, &2 등은 함수에 전달되는 첫 번째, 두 번째 인자를 의미한다.


함수를 인자로 전달

& 기호는 기존에 정의된 함수를 다른 함수의 인자로 전달할 때도 사용한다. 이렇게 하려면 함수 이름 앞에 &를 붙이고, / 뒤에 함수의 아리티(인자 개수)를 명시한다.

# Enum.map 함수에 :math.sqrt 함수를 인자로 전달

Enum.map([1, 4, 9], &(:math.sqrt/1))


블록 구문

&는 블록 내부에서도 사용될 수 있다. 이는 함수를 인자로 받는 함수에 코드 블록을 넘길 때 유용하다.

# 키워드 리스트에서 :a 키의 값을 가져오는 예

find_value = &(&1[:a])

find_value.(a: 1, b: 2)  # 결과는 1

이렇게 & 기호를 사용하면 익명 함수를 간결하게 표현하거나, 함수를 다른 함수에 인자로 전달하는 등의 작업을 쉽게 할 수 있다.

Closure 현상

iex(1)> x=5

5

f = fn y -> x + y end

#Function<42.125776118/1 in :erl_eval.expr/6>

f.(10)

15

x=1

1

f.(10)

15

위의 예제에서 x의 값을 5에서 1로 후에 변경을 했지만, 그 결과값은 15가 나온다. 이 현상은 클로저(closure)라고 부르는 특성 때문에 발생한다. 클로저는 함수 객체와 그 함수가 생성될 때의 환경을 함께 캡슐화한다. 즉, fn y -> x + y end 함수가 정의될 때의 x 값(5)가 함수 객체 내부에 '캡쳐'되어 저장된다.

x=5

5

f = fn y -> x + y end  # 여기에서 x의 값 5가 '캡쳐'됨

이후에 x의 값이 변경되더라도, 함수 f는 처음에 캡쳐한 x의 값(5)을 계속 사용한다.

f.(10)

15  # 5 + 10

x=1  # x 값을 변경

1

f.(10)

15  # 여전히 5 + 10, 새로운 x 값인 1은 무시됨

따라서 f.(10)을 호출하면 계산 결과는 여전히 15가 된다. 이는 Elixir의 클로저가 렉시컬 스코핑(lexical scoping)을 따르기 때문이다. 함수는 자신이 정의된 시점의 환경을 '기억'한다.


Lexical scoping이란?

lexical scoping이라는 용어는 변수의 범위(scope)가 코드의 텍스트 구조에 의해 결정된다는 것을 의미한다. 다시 말해, 변수는 그 변수가 선언된 블록 또는 함수 안에서만 접근할 수 있다. 이 개념은 함수가 어디에서 호출되는지가 아니라 어디에서 정의되었는지에 따라 변수의 범위가 결정된다.

예를 들어, Elixir에서 클로저와 lexical scoping은 다음과 같이 작동한다.

outer_x = 5

inner_function = fn ->

  inner_x = 3

  outer_x + inner_x  # 여기에서 outer_x는 클로저에 의해 '캡쳐'됨

end

# outer_x와 inner_x는 각자 다른 범위(scope)에 있다.

# inner_function은 outer_x를 그 범위 안에서 사용할 수 있다.

이 예에서 inner_function은 outer_x 변수에 접근할 수 있다. 이는 inner_function이 정의될 때 그 주변 환경(lexical scope)의 변수를 '기억'하기 때문이다.

클로저와 lexical scoping은 변수가 어떻게 접근되고 저장되는지를 제어하는 중요한 메커니즘이며, 여러 프로그래밍 언어에서 일반적으로 사용된다.

Elixir에서 Character

?a를 실행하면 'a' 문자의 ASCII 코드 값이 나온다. ASCII 코드에서 'a'는 97로 표현되므로 ?a의 결과는 97이 나온다.

Elixir에서 ?는 문자의 ASCII 값을 얻기 위한 단축 문법이다. 따라서 ?a는 'a' 문자의 ASCII 값을 반환하게 됩니다.

이 기능은 문자와 그에 해당하는 ASCII 값 사이에서 쉽게 변환할 수 있도록 도와준다.

[97, 98, 99]를 넣으면 ~c"abc"가 나오는 이유는 Elixir에서 리스트는 문자열을 표현하는 하나의 방법이기 때문이다. 실제로 문자열은 ASCII 코드의 리스트로 간주된다. 각각의 정수는 해당 문자의 ASCII 코드를 나타낸다.

예를 들어, ASCII 코드에서 'a'는 97, 'b'는 98, 'c'는 99이다. 따라서 [97, 98, 99]는 실질적으로 "abc"와 같은 문자열을 나타낸다.

~c는 Elixir에서 문자 리스트를 표현하는 다른 방법이다. ~c 다음에 따옴표로 둘러싼 문자들이 오면, 이 문자들은 ASCII 코드로 변환되어 리스트가 된다. 따라서 ~c"abc"는 [97, 98, 99]와 동일한 의미를 가진다.

여기서 주의를 해야할 부분이 있다. 숫자 97, 99, 99를 일반 숫자 리스트로 표현하고 싶다면, 아무런 문법적 표기 없이 그냥 리스트로 나열하면 된다. 예를 들어,

number_list = [97, 98, 99]

이렇게 하면 number_list 변수는 숫자 97, 98, 99를 원소로 가지는 일반적인 리스트가 된다. 이 리스트는 문자열이나 문자 리스트와는 별개로 취급된다. Elixir에서는 컨텍스트에 따라 리스트가 다르게 해석될 수 있다. 하지만 명시적으로 숫자 리스트로 다루려면 별도의 표기는 필요하지 않다.

그렇다면 여기서 number_list = ~c"abc" 가 되는 것 아닌가? 라는 의문이 들 것이다. 하지만,  number_list = ~c"abc"와 [97, 98, 99]는 다르다. ~c는 문자 리스트(char list)를 만드는 sigil이다.


  • ~c"abc"는 'a', 'b', 'c' 문자의 ASCII 값으로 구성된 Elixir의 char list를 만든다. 즉, [97, 98, 99]가 되지만, 이는 Elixir에서 특별한 char list 타입으로 취급된다.
  • [97, 98, 99]는 그냥 일반적인 정수 리스트다.


두 경우 모두 리스트 내부의 값은 [97, 98, 99]로 같지만, 데이터 타입이나 취급 방식에서 차이가 있다.

만약 숫자 97, 98, 99를 담은 일반 리스트를 만들고 싶다면, 별도의 표기나 sigil 없이 그냥 [97, 98, 99]라고 표현하면 된다.


beam file이 뭔가?

BEAM 파일은 Erlang 가상 머신에서 실행되는 컴파일된 바이트코드 파일이다. Elixir는 Erlang의 가상 머신 위에서 동작하므로, Elixir 코드도 결국 BEAM 파일로 컴파일된다. 이러한 BEAM 파일은 .beam 확장자를 가지고 있고, 컴파일러가 소스 코드를 중간 언어로 변환한 뒤 저장한다.

Erlang 또는 Elixir에서 프로젝트를 컴파일하면, 각 모듈에 대한 BEAM 파일이 생성되어 해당 언어의 가상 머신에서 실행될 수 있다. 이런 방식으로 BEAM 파일은 Erlang과 Elixir가 뛰어난 병렬 처리, 분산 처리, 오류 복구 등의 기능을 구현할 수 있게 도와준다.

댓글 없음:

댓글 쓰기

관련 포스팅