Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Possible to make DSL function available only within other function?

116 views
Skip to first unread message

hen...@nyh.se

unread,
May 29, 2015, 2:09:54 PM5/29/15
to elixir-l...@googlegroups.com
In order to learn Elixir macros better, I superficially reimplemented a part of Ecto.Migration, peeking at the original implementation: https://gist.github.com/henrik/25516815e6680e1c7a82

For the purposes of this question, let's assume it only makes sense to call "add/2" within a "create table(:foo) do" block (I have no idea if that's actually true of Ecto).

The way this is currently implemented in my Gist (and, I think, the way it's implemented in Ecto), the function is available to be called elsewhere as well:

defmodule MyMigration do
  use Ecto.Migration

  add :can_be_called_here, :string

  def change do
    add :can_also_be_called_here, :foo

    create table(:foo) do
      add :name, :string
    end
  end
end

If this had been a Ruby DSL, one would probably have used instance_eval to make an "add" method available only within that block and nowhere else.

Two questions:

1. Whether or not this is a good idea – just for the sake of learning the boundaries – can this technically be achieved in Elixir? Would one have to do some seriously evil AST slicing-and-dicing?

2. Is there a philosophical difference between Ruby/Elixir or OO/functional in this? Is it unidiomatic and indicative of a more OO-centric, less functional, view of things to consider where this function is available?

Peter Hamilton

unread,
May 29, 2015, 2:33:18 PM5/29/15
to elixir-l...@googlegroups.com
We do have scope in Elixir. Therefore, I would say that it is applicable to "to consider where this function is available".

How can we manipulate scope?

Functions within a module are callable throughout the module. Only functions exported are available outside the module. In Elixir, we have the handy `def` to mean "define and export" and `defp` meaning "just define, don't export".

You might be able to leverage this by putting create in a separate module and having add be private in that module.

We can also import into our current scope via `import`. This is contained to the immediate parent (it can be called in a function definition and it only applies for that function).

So another solution is to inject import into the AST. That shouldn't be too hard and is probable the easier approach. Here's a rough working example:

```
defmodule AddModule do
  def add(a, b) do
    IO.inspect "yay"
    a + b
  end
end

defmodule Foo do
  defmacro with_add(code) do
    imp = quote do
      import AddModule
    end
    [imp | code]
  end
end

defmodule Test do
  require Foo

  def run do
    Foo.with_add do
      IO.inspect add(1, 2)
    end
  end
end

Test.run
```

output:

"yay"
3


--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/515950a5-a136-4305-9bb8-f398996e3a6c%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
May 29, 2015, 2:37:11 PM5/29/15
to elixir-l...@googlegroups.com
1. Whether or not this is a good idea – just for the sake of learning the boundaries – can this technically be achieved in Elixir? Would one have to do some seriously evil AST slicing-and-dicing?

Inside macros, you have access to an environment as __ENV__ and there you can check if you are inside a module or function by looking at the module and function fields respectively.
  
2. Is there a philosophical difference between Ruby/Elixir or OO/functional in this? Is it unidiomatic and indicative of a more OO-centric, less functional, view of things to consider where this function is available?

There is a philosophical difference between in that in Elixir meta-programming is mostly compile time and therefore lexical while Ruby is runtime based.

This means in Elixir, once you import a function, it is available and it doesn't matter if you are outside a module, inside one, or inside a function. If you want it just inside the function then, well, import it inside the function and not elsewhere. As Peter showed, you can even import it inside blocks and use it in only one branch of an if expression.

So they are just functions that, when imported, we can invoke them directly and it is lexical. We don't really care where we are importing them really. For this reason, import is drastically different from something like include in Ruby, which effectively affects the context it is being included in.

José Valim

unread,
May 29, 2015, 2:42:42 PM5/29/15
to elixir-l...@googlegroups.com
Btw, this is an excellent question. I struggled so much about this when designing the language.

Ultimately I landed on import being lexical which definitely simplifies things. You don't need to worry about how import affect each scope because it is always explicit: it affects everything below the import in the same file.

Drew Olson also has a good blog post about this: http://blog.drewolson.org/the-value-of-explicitness/



José Valim
Skype: jv.ptec
Founder and Lead Developer

Henrik Nyh

unread,
May 29, 2015, 3:01:22 PM5/29/15
to elixir-l...@googlegroups.com
On Fri, May 29, 2015 at 8:33 PM, Peter Hamilton <petergh...@gmail.com> wrote:
Here's a rough working example:

Peter, thank you! That's a great explanation and an easy to follow example. 
 
    imp = quote do
      import AddModule
    end
    [imp | code]

And I particularly enjoyed this bit – didn't realize you could do that instead of unquote(code).

On Fri, May 29, 2015 at 8:36 PM, José Valim <jose....@plataformatec.com.br> wrote:

Inside macros, you have access to an environment as __ENV__ and there you can check if you are inside a module or function by looking at the module and function fields respectively.

Aha, thank you. Should that be  __CALLER__, to get the context outside the macro itself?

If you want it just inside the function then, well, import it inside the function and not elsewhere. As Peter showed, you can even import it inside blocks and use it in only one branch of an if expression.

I've seen "lexical scope" come up a few times but my Ruby-addled brain hadn't made this connection :)

Ultimately I landed on import being lexical which definitely simplifies things. You don't need to worry about how import affect each scope because it is always explicit: it affects everything below the import in the same file.

Yeah, it seems powerful and simple. I'll read Drew Olson's post right now.

Thank you both so much, Peter and José!

José Valim

unread,
May 29, 2015, 3:13:29 PM5/29/15
to elixir-l...@googlegroups.com
> Aha, thank you. Should that be  __CALLER__, to get the context outside the macro itself?

Yes, I am glad you caught my mistake. :)




José Valim
Skype: jv.ptec
Founder and Lead Developer

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages