Last active
July 22, 2022 09:18
-
-
Save mprymek/8379066 to your computer and use it in GitHub Desktop.
Elixir metaprogramming example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is an example of metaprogramming in the Elixir language. | |
# | |
# We will define a domain specific language (DSL) for the definition | |
# of a service which is watched by several sensors. | |
# Each sensor watches some property/functionality of the service and | |
# returns the result of the check. | |
# | |
# To determine if the service is functioning properly, we need functions | |
# to run all the sensors' code and gather the returned data. | |
# | |
# Our DSL will generate these functions for us automatically: | |
# - "sensors" returns the list of sensors for the service | |
# - "run_sensors" will run all the sensors' checking code | |
# - "run_next" will run one check and return a continuation - i.e. | |
# a function you can run again and you will get another check result | |
# and again a continuation. | |
# When all checks are run, you will start again. The continuations | |
# will be generated infinitely. | |
# | |
# The first part is a library code - not interesting for the user of the library. | |
# Second part is a user-written service definition written in our DSL. | |
# Third part is a demonstration of running of the automatically generated functions. | |
# | |
# I.e. the overall result is: the user writes only the simple DSL definition | |
# and get fully functional sensor-runner functions. | |
# | |
# ...and moreover: you can easily write code to generate service checking code | |
# from XML or any other source. You just have to slightly adapt the corresponding | |
# macro. And still you have *compiled* code - no ifs, no cases, no slow dynamical | |
# XML-parsing code and bloated datastructures in memory. Enjoy Elixir :) | |
############################################################################### | |
# DSL DEFINITION - library code | |
# | |
# This code converts our DSL to normal Elixir code, stores sensor names in a module | |
# attribute @sensors and generates all the sensor-running functions. | |
defmodule ServiceDSL do | |
# the quoted code will be inserted into all modules with "use ServiceDSL" statement | |
defmacro __using__(_) do | |
quote do | |
import ServiceDSL | |
Module.register_attribute(__MODULE__, :sensors, accumulate: true) | |
@on_definition ServiceDSL | |
@before_compile ServiceDSL | |
# run all | |
def run_sensors do | |
Enum.map(sensors,&:erlang.apply(__MODULE__,&1,[])) | |
end | |
# run one and return continuation | |
def run_next do | |
run_next sensors | |
end | |
defp run_next [s] do | |
data = apply(__MODULE__,s,[]) | |
{data,fn -> run_next sensors end} | |
end | |
defp run_next [s|rest] do | |
data = apply(__MODULE__,s,[]) | |
{data,fn -> run_next rest end} | |
end | |
end | |
end | |
# This is a DSL definition. Statement | |
# sensor "name", do: code | |
# will be converted by this macro into | |
# def sensor_name, do: code | |
defmacro sensor(name, do: code) do | |
code = Macro.escape(code, unquote: true) | |
quote bind_quoted: [name: name, code: code] do | |
name = :"sensor_#{name}" | |
def unquote(name)() do | |
unquote(code) | |
end | |
end | |
end | |
# this function must be in place before compilation | |
# it uses @sensors module attribute | |
defmacro __before_compile__(_) do | |
quote do | |
def sensors, do: @sensors | |
end | |
end | |
# whenever sensor_xxxxxx function is defined, append | |
# xxxxxx to the list in the @sensors module attribute | |
def __on_definition__(env, kind, name, args, _guards, _body) do | |
if kind == :def and sensor?(atom_to_list(name)) and length(args) == 0 do | |
mod = env.module | |
Module.put_attribute(mod, :sensors, name) | |
end | |
end | |
defp sensor?('sensor_'++_), do: true | |
defp sensor?(_), do: false | |
end | |
############################################################################### | |
# SERVICE DEFINITION - user code | |
# | |
defmodule SomeService do | |
use ServiceDSL | |
sensor "s1", do: IO.puts "Sensor 1" | |
sensor "s2", do: IO.puts "Sensor 2" | |
sensor "s3", do: IO.puts "Sensor 3" | |
end | |
############################################################################### | |
# USAGE | |
# | |
IO.puts "sensors = #{inspect(SomeService.sensors)}" | |
IO.puts "\n* Run sensors by name" | |
SomeService.sensor_s1 | |
SomeService.sensor_s2 | |
SomeService.sensor_s3 | |
IO.puts "\n* Run all sensors" | |
SomeService.run_sensors | |
IO.puts "\n* Run infinitely using continuations" | |
{_,cont} = SomeService.run_next | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,cont} = cont.() | |
{_,_cont} = cont.() | |
# output: | |
# | |
# sensors = [:sensor_s3, :sensor_s2, :sensor_s1] | |
# | |
# * Run sensors by name | |
# Sensor 1 | |
# Sensor 2 | |
# Sensor 3 | |
# | |
# * Run all sensors | |
# Sensor 3 | |
# Sensor 2 | |
# Sensor 1 | |
# | |
# * Run infinitely using continuations | |
# Sensor 3 | |
# Sensor 2 | |
# Sensor 1 | |
# Sensor 3 | |
# Sensor 2 | |
# Sensor 1 | |
# Sensor 3 | |
# Sensor 2 | |
# Sensor 1 | |
# Sensor 3 |
This was a really helpful example. Thank you!
You're welcome :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This was a really helpful example. Thank you!