Skip to content

Latest commit

 

History

History
217 lines (155 loc) · 8.37 KB

2012-04-21-hello-macros.markdown

File metadata and controls

217 lines (155 loc) · 8.37 KB
layout title subtitle author category excerpt
post
Building a Web Framework. Part I
Hello Macros
Alexei Sholik
Web dev
Since we haven't seen much new stuff in the Elixir land this week, I've decided to do something a little different. We'll build our own web framework to get a feel of the Elixir power. In this first part we'll take a closer look at macros and we'll see how they can be useful when it comes to building a friendly syntax for our users.

Since we haven't seen much new stuff in the Elixir land this week, I've decided to do something a little different. We'll build our own web framework to get a feel of the Elixir power. In this first part we'll take a closer look at macros and we'll see how they can be useful when it comes to building a friendly syntax for our users.

Here's an example of what we are aiming at:

defmodule HelloServer do
  use Feb, root: "assets"

  get "/" do
    { :ok, "Hello world!" }
  end

  get "/demo", file: "demo.html"

  post "/" do
    { :ok, "You're posted!\nYour data: #{inspect _data}" }
  end
end

The Foundation

To keep this post short and simple, we won't cover every single aspect of web framework development right away. Instead, we'll first focus on one specific task — defining a domain specific language (DSL) using Elixir macros. Let's start by stepping through the code above, one piece at a time.

You might remember from the Getting Started guide that use is just a syntactic sugar that expands into a call to the __using__ macro. In our case, the expansion will look like this

require Feb
Feb.__using__(__MODULE, [root: "assets"])

Let's forget about the root option for now, we'll deal with it later. Here's how our __using__ macro is going to be defined:

defmodule Feb do
  defmacro __using__(module, _opts) do
    quote do
      import Feb, only: [get: 2, post: 2]

      def start do
        Feb.start unquote(module)
      end
    end
  end
end

One important thing to understand here is that this macro is called inside the module which invokes the use directive (HelloServer in our case). When the macro is called, its return value is then evaluated inside the calling module. Thus, our HelloServer ends up with a definition for the start method which will in turn call Feb.start. Too see this in action, let's first define the start method in Feb:

def start(_) do
  IO.puts "Executing Feb.start"
end

And here's the code for our HelloServer so far:

defmodule HelloServer do
  use Feb
end

Now let's try this out in the shell. I'm using a Makefile to simplify the compilation-related tasks. You can grab a copy from the project's GitHub repo. Notice also how the project directory is organized.

$ make
$ elixir -e 'HelloServer.start'
Executing Feb.start

Everything is working as expected. Now let's move to the fun part.

Writing Method Macros

Let's look at the first get definition and think a bit about its meaning.

get "/" do
  { :ok, "Hello world!" }
end

The important thing to know about Elixir definitions is that they are executable. The Elixir compiler does not compile the code in the strict sense of the word, it is executing the code. So when you put IO.puts "Hello" inside a module definition, you'll see Hello printed in the shell when the code is compiled.

So, in this case, it's just a call to the get macro defined in Feb. In fact, we could write it as

Feb.get("/", do: { :ok, "Hello world!" })

and would be exactly the same thing. The reason we are allowed to omit the Feb. part is because we have imported the get macro earlier, see the definition of __using__.

OK, so now that when we understand what's actually going on, let's look at the definition:

defmacro get(path, [do: code]) do
  quote do
    def handle(:get, unquote(path), _data) do
      unquote(code)
    end
  end
end

Our get macro accepts two arguments, the second one is pattern-matched to extract the block of code between do and end. The macro produces a definition of the handle method with three arguments. The _data argument is not used here, it will be used in the post method later. So, basically, instead of calling get we could write the following definition in HelloServer to achieve the same effect:

def handle(:get, "/", _data) do
  { :ok, "Hello world!" }
end

That's all there is to it. We can check this in the shell.

$ make
$ iex
iex> HelloServer.handle :get, "/", nil
{:ok,"Hello world!"}

The code for the post macro is pretty straightforward as well:

defmacro post(path, [do: code]) do
  quote hygiene: false do
    def handle(:post, unquote(path), _data) do
      unquote(code)
    end
  end
end

One new thing here is hygiene: false. What this does is it allows us to define variables that will be accessible by the caller. We do this here so that we can then reference the _data variable in our HelloServer implementation:

post "/" do
  { :ok, "You're posted!\nYour data: #{inspect _data}" }
end

Check that it works as expected:

$ make
$ iex
iex> HelloServer.handle :post, "/", "abc"
{:ok,"You're posted!\nYour data: \"abc\""}

Good. We're almost done with the basics. I haven't covered the second get definition yet. Let's recall what it looked like:

get "/demo", file: "demo.html"

By using this method we're letting the framework know that we want to send the contents of the demo.html file back to the client. In order to support this second form we need to define another clause for our get macro:

defmacro get(path, [file: bin]) when is_binary(bin) do
  quote do
    def handle(:get, unquote(path), _data) do
      full_path = File.join([static_root(), unquote(bin)])
      case File.read(full_path) do
      match: { :ok, data }
        { :ok, data }
      else:
        { :error, "404 Not Found" }
      end
    end
  end
end

Having written the code for reading from a file in the framework, we free our users from the need to do so themselves.

Wrapping Up

I haven't yet provided the definition for static_root, let me fix this. It has to do with the root option mentioned at the beginning of the post. Remember what our first invocation of use looked liked

# In module HelloServer
use Feb, root: "assets"

The [root: "assets"] Keyword is what ends up in the _opts argument of our __using__ macro. So let's rewrite that macro to include the definition for the static_root method.

# In module Feb
defmacro __using__(module, opts) do
  root_val = Keyword.get(opts, :root, ".")

  quote do
    import Feb, only: [get: 2, post: 2]

    def start do
      Feb.start unquote(module)
    end

    defp static_root, do: unquote(root_val)
  end
end

We read the value of the root key from the opts Keyword. If it's nil, the current directory becomes the root.

The post is already getting quite long, so let's wrap up and do the final test drive to make sure everything works as expected.

$ make
$ iex
iex> import HelloServer
[]
iex> handle :get, "/", nil
{:ok,"Hello world!"}
iex> handle :post, "/", :data
{:ok,"You're posted!\nYour data: :data"}
iex> handle :get, "/demo", nil
{:error,"404 Not Found"}

Oops, looks like I've forgotten to create the demo.html file. Let's do this real quick. Create the file in the assets directory and paste the following contents into it:

<html>
  <head>
    <title>Demo</title>
  </head>
  <body>
    <h1>Hello world!</h1>
  </body>
</html>

Now let's try that again:

iex> handle :get, "/demo", nil
{:ok,"<html>\n<head>\n  <title>Demo</title>\n</head>\n<body>\n  <h1>Hello world!</h1>\n</body>\n</html>\n\n\n"}

Awesome! This concludes the first part in the series. Don't forget to grab the code from GitHub.If you have any questions or corrections, send a message to the mailing list or stop by the #elixir-lang channel on irc.freenode.net.

See you there.