Skip to content

Instantly share code, notes, and snippets.

@mprymek
Last active July 22, 2022 09:18
Show Gist options
  • Save mprymek/8379066 to your computer and use it in GitHub Desktop.
Save mprymek/8379066 to your computer and use it in GitHub Desktop.
Elixir metaprogramming example
# 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
@sammy-hughes
Copy link

This was a really helpful example. Thank you!

@mprymek
Copy link
Author

mprymek commented Nov 19, 2021

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