How to dynamically create modules with functions - macros

On compilation stage I can easily produce functions with:
defmodule A1 do
defmodule A2 do
Enum.each %{m: 42}, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end
end
end
IO.puts A1.A2.m
#⇒ 42
Also, I can produce modules with functions from within a function call:
defmodule B1 do
def b2! do
defmodule B2 do
# enum is for the sake of future example
Enum.each %{m1: 42}, fn {_k, v} ->
# def b2(), do: unquote(v) WON’T WORK (WHY?), BUT
#v v
def b2(), do: #v
end
end
end
end
B1.b2! # produce a nested module
IO.puts B1.B2.b2 # call a method
#⇒ 42
Now my question is: how can I dynamically produce a module with dynamically created function names, e. g.:
defmodule B1 do
def b2! do
defmodule B2 do
Enum.each %{m1: 42, m2: 3.14}, fn {k, v} ->
#k k
#v v
def unquote(#k)(), do: #v # THIS DOESN’T WORK
end
end
end
end
NB I was able to achieve what I wanted with
defmodule B1 do
def b2! do
defmodule B2 do
Enum.each %{m1: 42, m2: 3.14}, fn {k, v} ->
ast = quote do: def unquote(k)(), do: unquote(v)
Code.eval_quoted(ast, [k: k, v: v], __ENV__)
end
end
end
end
but it seems to be quite hacky.

I believe this happens due to nested macro invocations (def and defmodule are both macros). If you place an unquote there, it unquotes from the top level def:
defmodule B1 do
k = :foo
v = :bar
def b2! do
defmodule B2 do
def unquote(k)(), do: unquote(v)
end
end
end
B1.b2!
IO.inspect B1.B2.foo
prints
:bar
The Module.create/3 recommends using that function to dynamically create modules when the body is an AST. With that, the code becomes much more elegant than the hacky solution using Code.eval_quoted/3:
defmodule B1 do
def b2! do
ast = for {k, v} <- %{m1: 42, m2: 3.14} do
quote do
def unquote(k)(), do: unquote(v)
end
end
Module.create(B1.B2, ast, Macro.Env.location(__ENV__))
end
end
B1.b2!
IO.inspect B1.B2.m1
IO.inspect B1.B2.m2
Output:
42
3.14

Related

Using guards in Elixir macros

I am working on macro which would take a function and add some additional functionality. Eg.:
This:
defstate this_works(a, b) do
a + b + 1
end
Should be converted to this:
def this_works(a, b) do
IO.puts("LOGGING whatever")
a + b + 1
end
This is what I have so far. Try running this piece of code in iex:
defmodule MyMacro do
defmacro defstate(ast, do: block) do
{fn_atom, _} = Macro.decompose_call(ast)
quote do
def unquote(fn_atom)(var!(a), var!(b)) do
IO.puts("LOGGING")
unquote(block)
end
end
end
end
defmodule Test1 do
import MyMacro
defstate this_works(a, b) do
a + b + 1
end
end
Test.this_works(1, 2)
This works as expected.
Now, this module does not compile:
defmodule Test2 do
import MyMacro
defstate this_fails(a, b)
when 1 < 2
when 2 < 3
when 3 < 4 do
a + b + 1
end
end
The only change is that I added a guard and macro is unable to deal with that.
How can I improve MyMacro.defstate to make it work with a function with any number of guards?
If you inspect fn_atom with the defstate this_fails(a, b) when 1 < 2, you'll see that it's :when instead of :this_fails. This is because of how when expressions are represented in the Elixir AST:
iex(1)> quote do
...(1)> def foo, do: 1
...(1)> end
{:def, [context: Elixir, import: Kernel],
[{:foo, [context: Elixir], Elixir}, [do: 1]]}
iex(2)> quote do
...(2)> def foo when 1 < 2, do: 1
...(2)> end
{:def, [context: Elixir, import: Kernel],
[{:when, [context: Elixir],
[{:foo, [], Elixir}, {:<, [context: Elixir, import: Kernel], [1, 2]}]},
[do: 1]]}
You can fix this using some pattern matching:
defmodule MyMacro do
defmacro defstate(ast, do: block) do
f = case ast do
{:when, _, [{f, _, _} | _]} -> f
{f, _, _} -> f
end
quote do
def unquote(ast) do
IO.puts("LOGGING #{unquote(f)}")
unquote(block)
end
end
end
end
defmodule Test do
import MyMacro
defstate this_works(a, b) do
a + b + 1
end
defstate this_works_too(a, b) when a < 2 do
a + b + 1
end
end
defmodule A do
def main do
IO.inspect Test.this_works(1, 2)
IO.inspect Test.this_works_too(1, 2)
IO.inspect Test.this_works_too(3, 2)
end
end
A.main
Output:
LOGGING this_works
4
LOGGING this_works_too
4
** (FunctionClauseError) no function clause matching in Test.this_works_too/2
The following arguments were given to Test.this_works_too/2:
# 1
3
# 2
2
a.exs:24: Test.this_works_too/2
a.exs:33: A.main/0
(elixir) lib/code.ex:376: Code.require_file/2
(I also changed the unquote after def to make sure the when clause is preserved.)
The call to defstate is expanded at compile time to the things in the quote block from your defmacro. As such, guard expressions will not be applied to the macro call directly, because at compile time, the function you're defining inside is not called.
So you have to grab the :when tuple yourself and add the guards yourself:
defmodule MyMacro do
defmacro defstate({:when, _, [ast, guards]}, do: block) do
{fn_atom, _} = Macro.decompose_call(ast)
quote do
def unquote(fn_atom)(var!(a), var!(b)) when unquote(guards) do
IO.puts("LOGGING")
unquote(block)
end
end
end
end
Note how I match for a {:when, _, [ast, guards]} tuple now.
When you call a macro with a guard, it will put the original ast inside the first item of the arguments list, and the guard expression inside the second item.
Note that you'll still have to define a catch-all macro definition below this one in case you want to use your macro without guard clauses.

Pass keyword argument to a macro in julia

I'd like to pass a keyword argument to a macro in julia. I try to parse the kwargs manually and build the Expr but got errors. Do I miss something?
macro sort(x, kv)
#show x,kv
d = []
if kv.head == :(=)
# push!(d, (kv.args[1], kv.args[2])) # fail
push!(d, (kv.args[1], esc(kv.args[2]))) # fail also
end
ex = quote
sort($x; $(d...))
end
Meta.show_sexpr(ex)
ex
end
x = collect(1:5)
rev=true
#sort x rev=rev
Output:
(x, kv) = (:x, :(rev = rev))
(:block,
(:line, 9, Symbol("REPL[36]")),
(:call, :sort, (:parameters, (:rev, :($(Expr(:escape, :rev))))), :x)
)ERROR: TypeError: non-boolean (Expr) used in boolean context
Stacktrace:
[1] (::Base.#kw##sort!)(::Array{Any,1}, ::Base.#sort!, ::Array{Int64,1}) at ./<missing>:0
[2] #sort#8(::Array{Any,1}, ::Function, ::Array{Int64,1}) at ./sort.jl:546
[3] (::Base.#kw##sort)(::Array{Any,1}, ::Base.#sort, ::Array{Int64,1}) at ./<missing>:0
[4] macro expansion at ./REPL[36]:9 [inlined]
[5] anonymous at ./<missing>:?
Edit: I am able to build keyword args using :parameters Expr. My generated Expr looks like the normal one. But strangely, I got the error "syntax: invalid syntax (parameters (kw (outerref rev) true))"
julia> macro m(f, x, ks...)
#show f x ks
ex = Expr(:call, f, x)
par = Expr(:parameters )
push!(ex.args, par)
for kv∈ks
k,v = kv.args
push!(par.args, Expr(:kw, k, v))
end
Meta.show_sexpr(ex)
ex
end
#m (macro with 1 method)
julia>
julia> Meta.show_sexpr(:(sort(1:9; rev=true)))
(:call, :sort, (:parameters, (:kw, :rev, true)), (:(:), 1, 9))
julia> #m sort 1:9 rev=true
f = :sort
x = :(1:9)
ks = (:(rev = true),)
(:call, :sort, (:(:), 1, 9), (:parameters, (:kw, :rev, true)))ERROR: syntax: invalid syntax (parameters (kw (outerref rev) true))

Ecto schema with a tsrange field

I have a Postgres table with a tsrange column, and I'd like to include that in my Ecto module's schema. I see that Postgrex.Range exists. I've tried this:
schema "clients" do
field :valid_at, Postgrex.Range
...
end
But that gives me this error:
** (ArgumentError) invalid or unknown type Postgrex.Range for field :valid_at
lib/ecto/schema.ex:1785: Ecto.Schema.check_type!/3
lib/ecto/schema.ex:1473: Ecto.Schema.__field__/4
Any suggestions? I'm using Phoenix 1.3 and the Ecto master branch.
I think you should create a custom type for tsrange to work with Ecto.
defmodule YourApp.TimestampRange do
#behaviour Ecto.Type
def type, do: :tsrange
def cast([lower, upper]) do
{:ok, [lower, upper]}
end
def cast(_), do: :error
def load(%Postgrex.Range{lower: lower, upper: upper}) do
{:ok, [lower, upper]}
end
def dump([lower, upper]) do
{:ok, %Postgrex.Range{lower: lower, upper: upper, upper_inclusive: false}}
end
def dump(_), do: :error
end
About inclusive boundaries checkout PostgreSQL documentation
and then in your app you can use:
schema "clients" do
field :valid_at, YourApp.TimestampRange
...
end
Looks like #TheAnh has the right approach, but here is what actually wound up working for me:
defmodule Myapp.TsRange do
#behaviour Ecto.Type
def type, do: :tsrange
def cast(nil), do: {:ok, nil}
def cast([lower, upper]), do: {:ok, [lower, upper]}
def cast(_), do: :error
def load(%Postgrex.Range{lower: lower, upper: upper}) do
lower = lower |> to_datetime
upper = upper |> to_datetime
case [lower, upper] do
[nil, nil] -> {:ok, [nil, nil]}
[{:ok, lower}, {:ok, upper}] -> {:ok, [lower, upper]}
_ -> :error
end
end
def load(_), do: :error
def dump([lower, upper]) do
{:ok, %Postgrex.Range{lower: lower |> from_datetime,
upper: upper |> from_datetime,
upper_inclusive: false}}
end
def dump(_), do: :error
defp to_datetime(nil), do: nil
defp to_datetime({{y, m, d}, {h, min, s, ms}}) do
NaiveDateTime.new(y, m, d, h, min, s, ms)
end
defp from_datetime(nil), do: nil
defp from_datetime(dt) do
{{dt.year, dt.month, dt.day}, {dt.hour, dt.minute, dt.second, elem(dt.microsecond, 0)}}
end
end

elixir string to method arguments in macro

I'm messing around with macros in elixir purely for fun and not profit :). So I have a comma separated string and I want to dynamically create a function with that list as arguments i.e.
defmacro __using__(opts) do
args = "a,b,c"
quote do
def test(unquote(args)) do
IO.inspect("#{a},#{b},#{c}")
end
end
end
The problem I have is that the method created is: test("1,2,3") and not test(1,2,3) for obvious reasons. So how do I convert a string into a method argument?
Chris
There's probably a better way but one way to convert "a,b,c" to something that can be injected into def's argument list would be to use Code.string_to_quoted!/1 and unquote_splicing:
defmacro __using__(_opts) do
args = "a,b,c"
args = Code.string_to_quoted!("[#{args}]")
quote do
def test(unquote_splicing(args)) do
IO.inspect(unquote(args))
end
end
end
Note that due to hygiene, you'll have to use args to access the variables and you cannot directly access a, b, or c inside the def.
Full Example:
defmodule Macros do
defmacro __using__(_opts) do
args = "a,b,c"
args = Code.string_to_quoted!("[#{args}]")
quote do
def test(unquote_splicing(args)) do
IO.inspect(unquote(args))
end
end
end
end
defmodule Main do
use Macros
def main do
test("a", "b", "c")
test(1, 2, 3)
end
end
Main.main
Output:
["a", "b", "c"]
[1, 2, 3]

Best way to read lines in groups in a flat file

I have a file which consists of the groups of lines. Each group represents a event. The end of the group is denoted by "END". I can think of using a for loop to loop through the lines, store the intermediate lines and emit the group when "END" is encounter.
But since I would like to do it in Scala. I am wondering if someone can suggest a more functional way to accomplish the same thing?
----------
A
B
C
END
----------
D
E
F
END
----------
Just define an iterator to return groups
def groupIterator(xs:Iterator[String]) =
new Iterator[List[String]]
{ def hasNext = xs.hasNext; def next = xs.takeWhile(_ != "END").toList}
Testing (with an Iterator[String], but Source.getLines will return you an Iterator for the lines of your file)
val str = """
A
B
C
END
D
E
F
END
""".trim
for (g <- groupIterator(str.split('\n').toIterator)) println(g)
//> List(A, B, C)
//| List(D, E, F)