ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Macro 101
    기술 동향 2022. 8. 8. 13:46

    Basic

    •  terms
      • Expression : 표현식, 코드
      • Represent : 코드를 표현하는 방식, AST consist of Three element tuples
      • Quote : 인용, 특정 코드 덩어리의 Representation 가져오는 
      • Unquote : 인용하는 것이 아니라 Evaluation 된 코드 결과

     

    • Elixir는 이미 데이터 구조와 함수를 사용하여 간단하고 읽기 쉬운 방식으로 일상적인 코드를 작성할 수 있는 메커니즘을 제공합니다. 매크로는 최후의 수단으로만 사용해야 합니다. 명시적(explicit)이 암시적(implicit)보다 낫다는 것을 기억하십시오. Concise Code보다 Clear Code가 좋습니다.
    • ·메타 프로그래밍의 첫번째 단계는 표현식이 어떻게 나타나는지를 이해하는 것입니다. Elixir 코드는내부적으로 추상 문법 트리(Abstract Syntax Tree, AST) 표현하는데, 이는 함수 이름과 메타데이터, 함수의 인자를 포함하는 튜플로 이루어져 있습니다.
    • AST tuple은 다음 형식에 따라 구성됩니다.
    {atom | tuple, list, list | atom}
    • 첫 번째 요소는 동일한 representation의 atom 또는 tuple입니다.
    • 두 번째 요소는 숫자 및 컨텍스트와 같은 메타데이터를 포함하는 keyword list입니다.
    • 세 번째 요소는 함수 호출 또는 atom에 대한 argument list 입니다. 이 요소가 atom이면tuple이 variable을 represent 하는 것입니다.
    • 위에 정의된 tuple 외에도 5개의 Elixir Literal Representation이 있습니다. 이 Literal은 인용(Quote)될 때 Tuple이 아닌 스스로를 반환합니다.
    :sum #=> atom
    1.0 #=> float, integer
    [1, 2] #=> list
    "string" #=> 문자열
    {key, value} #=> 두 개의 element가 있는 tuple

     

    function macro 차이

    defmodule Unless do
    
      def fun_unless(clause, do: expression) do
        if(!clause, do: expression)
      end
     
      defmacro macro_unless(clause, do: expression) do
        quote do
          if(!unquote(clause), do: unquote(expression))
        end
      end
    end
    iex(1)> Unless.fun_unless false, do: IO.puts "this should never be printed"          
    this should never be printed
    :ok
    iex(2)> Unless.fun_unless true, do: IO.puts "this should never be printed"           
    this should never be printed
    nil
    • Fun.unless/2를 호출 하였을 때 첫 번째 파라미터가 true던 false던 상관없이 모두 “this should never be printed”가 출력 됩니다.
    • 그러나 리턴 값은 :ok와 nil 로 서로 다릅니다.
    • 다른 예를 한 번 보겠습니다.
    iex(3)> Unless.fun_unless false, do: 1+3                                             
    4
    iex(4)> Unless.fun_unless true, do: 1+3 
    nil
    • 첫 번째 파라미터가 false 일 경우, 4가 리턴 되었습니다. 그러나 true일 경우, nil 이 리턴 됩니다.
    • 위의 예와 차이점이 명확하게 보입니다. do: 절에 있는 코드의 Evaluate여부 입니다.
    • 함수는 인수를 받아 Evaluate하여 함수 내부로 전달합니다. 그러나 매크로는 인용된 표현식(quoted expression)을 수신하고 이를 인용부(quote do 절)에 삽입하고 마지막으로 다른 인용된 표현식(quoted expression)을 반환합니다.
    • IO.puts 문장은 함수 구현에서 인쇄되었지만 매크로 구현에서는 인쇄되지 않았습니다. 이는 함수 호출에 대한 인수가 함수를 호출하기 전에 Evaluate되기 때문입니다. 그러나 매크로는 인수를 평가하지 않습니다. 대신 인용된 표현으로 인수를 받은 다음 다른 인용된 표현으로 변환됩니다.
    iex(5)> Unless.macro_unless false, do: IO.puts "this should never be printed"
    this should never be printed
    :ok
    iex(6)> Unless.macro_unless true, do: IO.puts "this should never be printed"
    nil
    • Macro의 정의에 의해 위의 macro_unless 에 IO.puts 는 Evaluate 되지 않고 macro 안의quote 절에 삽입되어 새로운 quoted expression으로 리턴 됩니다.
    • Quote는 특정 코드 덩어리의 Representation을 가져오는 것입니다. 그러나 때로는 가져오려는 Representation 내부에 다른 특정 코드 덩어리를 주입해야 할 수도 있습니다.
    • 예를 들어, 인용된 표현식 안에 삽입하려는 숫자가 포함된 변수 숫자가 있다고 가정합니다.
    iex> number = 13
    iex> Macro.to_string(do: 11 + number)
    
    "11 + number"
    • number 변수의 값이 삽입되지 않았고 expression에서 number가 인용되었기 때문에 우리가 원하는 것이 아닙니다. Number에 값을 삽입하려면 quote do 절 안에 unquote를 사용해야 합니다.
    iex> number = 13
    iex> Macro.to_string(quote do: 11 + unquote(number))
    
    "11 + 13"

     

    디버깅

    • 아래는 매크로로 생성된 코드를 확인하고 싶을 때, 사용할 수 있습니다.
    • 매크로로 생성된 코드를 확인하고 싶다면, 코드를 Macro.expand/2, Macro.expand_once/2로합칠 수 있습니다. 다음은 __ENV__를 사용하여 IEx와 소스를 합칩니다.
    • Expand 함수는 주어진 감싸진 코드로 매크로를 확장합니다. 첫 번째는 여러 번 확장됩니다. 하지만 뒤에 것은 한번만 확장됩니다. 예를 들어, 이 전 단락의 unless 예제를 수정해 봅시다.
    defmodule OurMacro do
    
      defmacro unless(expr, do: block) do
        quote do
          if !unquote(expr), do: unquote(block)
        end
      end
    end
     
    require OurMacro
     
    quoted =
      quote do
        OurMacro.unless(true, do: "Hi")
      end
    quoted |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts
    
    if(!true) do
      "Hi"
    end
    • 같은 코드를 Macro.expand/2로 실행하면, 흥미로운 결과가 나옵니다.
    quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts
    
    case(!true) do
      x when x in [false, nil] ->
        nil
      _ ->
        "Hi"
    end
    • Elixir에서 if는 매크로입니다. 여기에서 기저의 case 구문으로 확장되는 것을 확인할 수 있습니다.
    • 즉, 소스를 합치는 것을 “확장한다”라고 표현하는 이유는 어디에 Expand 하는지에 따라 다른quoted expression이 생성될 수 있기 때문입니다.

     

    Macro Hygiene

    • 기본적으로 Elixir 매크로는 청결하며 컨텍스트와 충돌하지 않습니다.
    defmodule Example do
    
      defmacro hygienic do
        quote do: val = -1
      end
    end
     
    iex> require Example
    nil
    iex> val = 42
    42
    iex> Example.hygienic
    -1
    iex> val
    42
    • val을 조작하고 싶은 경우에는 Hygiene하지 않은 변수를 원한다는 것을 알리기 위해서 var!/2를사용하면 됩니다.
    defmodule Example do
    
      defmacro hygienic do
        quote do: val = -1
      end
     
      defmacro unhygienic do
        quote do: var!(val) = -1
      end
    end
     
    require Example
    nil
    iex> val = 42
    42
    iex> Example.hygienic
    -1
    iex> val
    42
    iex> Example.unhygienic
    -1
    iex> val
    -1
    • 매크로에서 var!/2를 사용하는 것으로 매크로에게 val 값을 넘기지 않고, 이를 조작하였습니다. 청결하지 않은 매크로를 사용하는 것은 최소화해야 합니다. var!/2를 사용하는 것은 변수 해결시에충돌이 발생할 위험성을 증가시키게 됩니다.
    • 이미 unquote/1라는 편리한 매크로를 배웠습니다만, 이외에도 코드에 값을 주입하는바인딩이라는 방법이 있습니다.
    defmodule Example do
    
      defmacro double_puts(expr) do
        quote do
          IO.puts(unquote(expr))
          IO.puts(unquote(expr))
        end
      end
    end
     
    Example.double_puts(:os.system_time)
    1450475941851668000
    1450475941851733000
    • 시간이 다릅니다! 무슨 일이 있었던 걸까요? unquote/1를 같은 표현식에 여러 번 사용하는 것은재평가를 발생시키며 예상치 못한 결과를 가져옵니다. bind_quoted를 사용해서 예제를 변경해봅시다.
    • 변수 바인딩을 통해 매크로의 내부에 여러 변수를 포함하고, 한번만 quote 되도록 보장하는 것으로예상치 못한 재평가를 회피할 수 있습니다. 
    • 바인딩된 변수를 사용하려면 quote/2의 bind_quoted 옵션에 키워드 리스트를 넘겨주면 됩니다. 
    defmodule Example do
    
      defmacro double_puts(expr) do
        quote bind_quoted: [expr: expr] do
          IO.puts(expr)
          IO.puts(expr)
        end
      end
    end
     
    iex> require Example
    nil
    iex> Example.double_puts(:os.system_time)
    1450476083466500000
    1450476083466500000

    '기술 동향' 카테고리의 다른 글

    Debugging using Livebook  (0) 2022.07.21
    GenServer를 어떻게 종료할 것인가?  (1) 2021.08.25
    Elixir Process  (0) 2021.06.28
    Process Register  (0) 2021.04.05
    Ecto Composable Query  (0) 2020.11.16

    댓글

Elixian