Tak jak wspomniałem ostatnio, Elixir napisany jest w Elixr :) Dziwiło mnie to strasznie i zarazem zainteresowało. Zacząłem zamiast poznawać nowe to sprawy z Elixir, czytać kod źródłowy szukając potwierdzenia w tym co przeczytałem. No i je znalazłem :) ale o tym zaraz.

Elixir opiera się na makrach, ale nie tych z Excel czy z C. Nie, każde wyrażenie które używamy a które normalnie nazywalibyśmy keyword tutaj jest makrem. Bardzo ciekawe podejście. Takie makro dzięki kompilatorowi następnie zamieniane jest na odpowiedni set wyrażeń/wywołań (trochę czarnej magii.. Ale tylko trochę).

Tak naprawdę makra te pozwalają tworzyć coś w deseń własnego języka domenowego (Domain Specific Language – DSL). Nie tylko są wykorzystywane przez sam Elixir, ale także umożliwiają nam, NAM upiększać język wprowadzając to nowe wyrażenia/skróty. Mamy skomplikowany kod który cały czas powtarzamy? Co za problem stworzyć dla niego makro, które nam wstawi nasz kod w odpowiednim momencie. Nic :) takie makro tworzy się za pomocą wyrażenia defmacro nazwa do/end.  Jednak ich tworzeniem i instalowaniem zajmie się kiedyś indziej. Teraz ogólnie chciałem sobie samemu udowodnić, że tak jest jak piszą :) i jak to jest zrobione :)

Makra w Elixir

A żeby nie pisać że tak jest a udowodnić, że rzeczywiście Elixr jest napisany w Elixir ;) wystarczy otworzyć plik kernel.ex w którym bez problemu znajdziemy kod tworzący wyrażenie if:

defmacro if(condition, clauses) do
	build_if(condition, clauses)
end

Wyrażenie unless (przeciwieństwo if):

defmacro unless(condition, clauses) do
	build_unless(condition, clauses)
end

Czy też znany nam z odcinka trzeciego raise:

defmacro raise(msg) do
	# Try to figure out the type at compilation time
	# to avoid dead code and make Dialyzer happy.
	msg = case not is_binary(msg) and bootstrapped?(Macro) do
	  true  -> Macro.expand(msg, __CALLER__)
	  false -> msg
	end

	case msg do
	  msg when is_binary(msg) ->
	    quote do
	      :erlang.error RuntimeError.exception(unquote(msg))
	    end
	  {:<<>>, _, _} = msg ->
	    quote do
	      :erlang.error RuntimeError.exception(unquote(msg))
	    end
	  alias when is_atom(alias) ->
	    quote do
	      :erlang.error unquote(alias).exception([])
	    end
	  _ ->
	    generated = fn fun, var ->
	      {fun, [generated: true, line: -1], [{var, [], __MODULE__}]}
	    end

	    {fun, meta, [arg, [do: clauses]]} =
	      quote do
	        case unquote(msg) do
	          msg when unquote(generated.(:is_binary, :msg)) ->
	            :erlang.error RuntimeError.exception(msg)
	          atom when unquote(generated.(:is_atom, :atom)) ->
	            :erlang.error atom.exception([])
	          %{__struct__: struct, __exception__: true} = other when is_atom(struct) ->
	            :erlang.error other
	          other ->
	            message = "raise/1 expects an alias, string or exception as the first argument, got: #{inspect other}"
	            :erlang.error ArgumentError.exception(message)
	        end
	      end

	    clauses =
	      :lists.map(fn {:->, meta, args} ->
	        {:->, [generated: true] ++ Keyword.put(meta, :line, -1), args}
	      end, clauses)

	    {fun, meta, [arg, [do: clauses]]}
	end
end

Co mnie zaś zdziwiło && i || też są makrami!:

defmacro left && right do
	quote do
	  case unquote(left) do
	    x when x in [false, nil] ->
	      x
	    _ ->
	      unquote(right)
	  end
	end
end

defmacro left || right do
	quote do
	  case unquote(left) do
	    x when x in [false, nil] ->
	      unquote(right)
	    x ->
	      x
	  end
	end
end

Fajne jest to, że teraz widząc o jak to jest stworzone, można zrobić podobnie z nowym operatorem. W ogóle jaka prosta konstrukcja defmacro left || right do. Już wiem czemu ten język jest nazywany pięknym :)

Podsumowanie

Co za genialny pomysł, umożliwić tworzenie języka w danym języki i to jeszcze w tak prosty sposób. Nie przypuszczałem, że to będzie takie fajne :) Opcja z makrami naprawdę mnie rozwaliła. Choć z drugiej strony to prawie (prawie robi różnicę) są extension methods. Z tą różnicą iż… możecie tworzyć własne makra dla def i defp, czego już za pomocą extension methods się nie da zrobić. Możliwe, że aspekty dla .NET mogłoby tutaj trochę pomóc, jednak nie są one domyślnym rozwiązaniem – dostarczonym przez MS. Tak czy siak, nie ma co porównywać świata .NET i Elixir. Twa środowiska, dwa w swojej klasie bardzo dobre. Dwa odmienne i zarazem posiadające części wspólne. Stworzone z innymi zamysłami przez różne firmy.

W przyszłym tygodniu albo będę kontynuował makra – pisanie, albo w końcu przyjrzę się operatorowi pipeline |>.

A wy jak znajdujecie opcję pisania Elixir w Elixir i samych makr do upiększania jeszcze bardziej kodu?

5 KOMENTARZE

    • @Piotr

      Dzięki, nie znałem nazwy a to znaczy, że nigdy nie pisałem kompilatora ;) Zaś to, że ktoś ma kompilator napisany w danym języku to jeszcze nic nie oznacza. To, że istnieje możliwość kompilowania C# z poziomu C# to super – choć na to czekaliśmy dość długo (Roslyn), wcześniej to było C i C++.

      Jednak niezależnie od bootstrappingy już nie napiszesz nowej składni językowej z poziomu C# którą z miejsca będziesz mógł użyć. Tutaj raczej fascynuje mnie fakt, że Elixir to tylko baza mała, a prawie wszystkie pozostałe operatory, wyrażenia, słowa kluczowe to są makra. Każdy może to napisać. W C# masz możliwość przeciążenia operatorów, ok. Ale już stworzenia nowego nie.

  1. Co do ciekawości, podobnie było z gitem. Git zaczął być trzymany w gicie krótko po jego powstaniu :)
    Dla nie nadal Elixir to rozwinięcie ruby. Wiec można też zmockować wszystko bez żadnych dodatków. Czyli np zmienić implementację +, aby zachowywał się jak – i to podczas działani programu.

    Staram się nie być purystą językowym, ale stawiaj proszę przecinek przed a. Dziękuję.

    • @pawelek

      Z gitem tak, zresztą nie dziwie się czemu tak Linus postąpił :)

      co do elixira i przeciążania operatorów w locie. nie wiem czy da się tak to zrobić z + i -. + i – są zaimplementowane w kernel ale nie jako makra. jako normalna funkcja przyjmująca left i right.Da się to może injectować do kodu, ale to nie jest to jak overloading. Dokładniej mówiąc musimy załadować kernel bez danej operacji i następnie zaimportować nasz moduł z przeciążoną operacją:

      defmodule Tt do
          def left + right do
              Kernel.-(left, right)
          end
      end
      
      import Kernel, except: [+: 2]
      import Tt
      
      1 + 1
      

      Ale to już jest intencjonalne i wymaga ekstra kodu.

      Zaś co do własnych operatorów to:

      There we have it. In Elixir, operators are predefined in the parser. Since the parse step happens before Elixir is bootstrapped, there’s no way for us to define our own operators in our code. We would either have to modify the Elixir parser directly (and recompile Elixir) or just be satisfied with one of the fourteen unused operators:
      
      \\, < -, |, ~>>, < <~, ~>, < ~, <~>, < |>, < <<, >>>, |||, &&&, and ^^^.
      

      więc tutaj nie można z mockować wszystkiego.

      Co do tego, że Elixir to rozwinięcie Ruby. Nie zgadzam się ale ok :)

      co do przecinków, postaram się poprawić. Ale wolę mieć brakujące 2-3 przecinki niż 4 literówki :) więc coś za coś :)

Comments are closed.