From b06cc4e9fbe267d65ed95737f237de873944f366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 18 Jan 2012 09:54:37 +0100 Subject: [PATCH] Macros chapter is done. --- docs/1_introduction.md | 9 ++- docs/2_dipping_the_toes.md | 60 +++++++++++++----- docs/4_protocols_and_records.md | 10 ++- docs/5_macros.md | 98 ++++++++++++++++++++++++----- lib/string/inspect.ex | 2 +- test/elixir/elixir/errors_test.exs | 2 +- test/elixir/exunit/escaper_test.exs | 2 +- test/elixir/kernel/quote_test.exs | 1 - test/elixir/string/inspect_test.exs | 10 +-- 9 files changed, 151 insertions(+), 43 deletions(-) diff --git a/docs/1_introduction.md b/docs/1_introduction.md index a93ca13b65..f944476ac6 100644 --- a/docs/1_introduction.md +++ b/docs/1_introduction.md @@ -88,7 +88,12 @@ We can use the `is_binary` and `is_list` helpers to detect if a given string is iex> is_list 'string' true -Keep in mind that double-quoted and single-quoted strings are different. Most of the times, you want to use double-quoted strings. +Although they represent the same thing, double-quoted and single-quoted strings are different and best suited for different scenarios: + + iex> "string" == 'string' + false + +Most of the cases, developers should use double-quoted strings as their representation is more compact. We are going to discuss this subject with more detail in the next chapter. Elixir also provides `true` and `false` as booleans: @@ -102,7 +107,7 @@ Booleans are represented internally as atoms: iex> is_atom(true) true -Elixir also provides Port, References and PIDs as data types but they are going to be discussed when we start to talk about process communication. For now, let's take a look at the basic operators in Elixir before we move on to the next chapter. +Elixir also provides Port, References and PIDs as data types (usually used in process communication) but they are out of the scope of a getting started tutorial. For now, let's take a look at the basic operators in Elixir before we move on to the next chapter. ## 1.4 Operators diff --git a/docs/2_dipping_the_toes.md b/docs/2_dipping_the_toes.md index d156636375..f23cbacc6f 100644 --- a/docs/2_dipping_the_toes.md +++ b/docs/2_dipping_the_toes.md @@ -21,20 +21,20 @@ This means that accessing a tuple element is very fast (constant time) and can b On the other hand, updating a tuple is expensive as it needs to duplicate the tuple contents in memory. Updating a tuple can be done with the `setelem` function: iex> setelem { :a, :b, :c }, 1, :d - {:d, :b, :c} + {:d,:b,:c} > If you are an Erlang developer, you will notice that we used the `elem` and `setelem` functions instead of Erlang's `element` and `setelement`. The reason for this choice is that Elixir attempts to normalize Erlang API's to always receive the `subject` of the function as the first argument. Since updating a tuple is expensive, when we want to iterate, add or remove elements, we usually use lists. Since lists are linked, it means accessing the first element of the list is very cheap, however, accessing the n-th element will require the algorithm to pass to n-1 nodes before reaching the n-th. For this reason, Elixir allows you to easily retrieve the first element of the list (called head): iex> [h | t] = [1,2,3] - [1, 2, 3] + [1,2,3] iex> h 1 iex> t - [2, 3] + [2,3] iex> [h | t] - [1, 2, 3] + [1,2,3] iex> length [h | t] 2 @@ -45,7 +45,37 @@ In the example above, we have assigned the head of the list to `h` and the tail iex> List.flatten [1,[2],3] [4,5,6] -## 2.2 Calling Erlang functions +## 2.2 Lists and binaries + +In the previous chapter we have discussed double- and single-quoted strings. Double quoted strings are binaries while single-quoted strings are lists: + + iex> "string" == 'string' + false + iex> is_binary "string" + true + iex> is_list 'string' + true + +In fact, both double- and single-quoted representations are just a shorter representation of binaries and lists. Considering that `?a` in Elixir returns the ASCII integer for the letter `a`, we could also write: + + iex> <> + "abc" + iex> [?a, ?b, ?c] + 'abc' + +In such cases, Elixir is smart enough to detect all characters in the list and in the binary are printable and returns the quoted representation. However, adding a non-printable character forces them to be printed differently: + + iex> <> + <<97,98,99,1>> + + iex> [?a, ?b, ?c, 1] + [97,98,99,1] + +Since lists are implemented as linked lists, it means a string represented as list usually takes a lot of space in memory (in ASCII, it would be one byte for each character and another byte to point to the next character). For this reason, binary (double-quoted) strings is preferred unless you want to explicitly iterate over the string as a list. Iterating over a string as a list may be convenient when counting characters or doing other list like operations. + +Currently Elixir does not ship with any library for doing string manipulation, but this will be amended soon. + +## 2.3 Calling Erlang functions Elixir's plans is to provide a small standard library responsible for handling most basic structures (lists, ordered dicts, strings and so forth) and IO. That said, complex applications will require the developer to use Erlang's libraries. @@ -59,7 +89,7 @@ Erlang's OTP is very well documented and a developer should not have problems go * [OTP docs](http://www.erlang.org/doc/) * [Standard library docs](http://www.erlang.org/doc/man/STDLIB_app.html) -## 2.3 Pattern matching +## 2.4 Pattern matching When discussing lists, we saw the following example: @@ -128,7 +158,7 @@ Although pattern matching allow powerful constructs, its usage is limited. For i iex> Erlang.lists.flatten([1,[2],3]) = [1,2,3] ** error :illegal_pattern -## 2.4 Key-values +## 2.5 Key-values One of the first control flow constructs we usually learn is the conditional `if`. In Elixir, we can write `if` in three different ways, all equivalent: @@ -224,11 +254,11 @@ Which is then parsed as: A good rule of thumb is: when providing arguments to a function, always use curly brackets. -## 2.5 Control flow structures +## 2.6 Control flow structures In this section we are going to describe Elixir main control structures. -### 2.5.1 If +### 2.6.1 If Refreshing from the section above, all those calls are equivalent: @@ -260,7 +290,7 @@ Refreshing from the section above, all those calls are equivalent: In Elixir, all values except `false` and `nil` evaluates to true. So there is no need to convert them to false. -### 2.5.2 Other boolean operators +### 2.6.2 Other boolean operators In the previous chapter, we discussed the boolean operators `and`, `or`, `not`, `andalso` and `orelse`. Those operators are strict in the sense they only accept booleans as arguments. @@ -286,7 +316,7 @@ To work around this limitation, Elixir provides three operators with similar fun iex> !nil true -### 2.5.3 Case +### 2.6.3 Case In this section we have introduced pattern matching via the `=` operator. Sometimes however it is convenient to match an expression against several expressions until we find a matching one. For such cases, we use `case` (pun intended): @@ -364,7 +394,7 @@ In the example above, the second clause will only match if x is positive. The Er Custom macros can be invoked in guard clauses as long as they expand to expressions that are a subset of the ones described above. -### 2.5.4 Functions and loops +### 2.6.4 Functions and loops Throughout this guide, we have created many functions in examples. The syntax for creating functions is: @@ -420,7 +450,7 @@ Which is exactly the same as `[1, 2, 3]`. Remember lists are linked lists, so ea The `match:` syntax above is rarely used with functions, although we have discussed them in order to explain loops. Anonymous loops aren't used frequently as well as Elixir developers usually write named functions, as we will see in the next chapter. -### 2.5.5 Try +### 2.6.5 Try The next control-flow mechanism is `try/catch/after`: @@ -464,7 +494,7 @@ There is one particularity that applies to `try/catch/after` when compared to ot iex> new_var ** error :undef -### 2.5.6 Receive +### 2.6.6 Receive The last control-flow mechanism we are going to discuss is essential to Elixir's and Erlang's actor mechanism. In Elixir, every code run in processes that exchange messages between them. Those processes are not Operating System processes (they are actually quite light-weight) but called so since they do not share state with each other. @@ -495,7 +525,7 @@ You may not see exactly `<0.36.0>` back, but something similar. If there are no In most cases, we don't send messages directly with `<-` nor write `receive` control expressions. Instead, we use many of the abstractions provided by OTP which will be discussed later. -## 2.6 Default functions +## 2.7 Default functions Elixir ships with many default functions automatically available in the current scope. Besides all the control flow expressions seen above, Elixir also adds: `elem` and `setelem` to read and set values in tuples, `inspect` that returns the representation of a given data type as string and many others. [A list of all those functions with documentation and examples are available here](github.com/josevalim/elixir/tree/master/lib/elixir/macros.ex). diff --git a/docs/4_protocols_and_records.md b/docs/4_protocols_and_records.md index 5ad34baa28..38855182ad 100644 --- a/docs/4_protocols_and_records.md +++ b/docs/4_protocols_and_records.md @@ -112,7 +112,15 @@ The real benefit of protocols comes when mixed with records. For instance, one m In the example above, we have implemented `blank?` for the custom dictionary that simply delegates to `RedBlack.empty?`. Since records are simply tuples, the default implementation for records can be given in the tuple implementation. -With this note, we have finally finished this section which has described `defrecord`, `defprotocol` and `defimpl`. Next, we are going to discuss macros with `defmacro`! +### 4.2.3 Built-in protocols + +Elixir ships with three built-in protocols, they are: + +* Enum::Iterator - specifies an iteration contract for any data structure +* String::Inspect - specifies how to represent and convert data structures to strings +* List::Inspect - specifies how to convert data structures to lists + +You can check the source code of those files for more information about how the protocol is used and how to implement your own. With this, we have finally finished this section which has described `defrecord`, `defprotocol` and `defimpl`. Next, we are going to discuss macros with `defmacro`! [Chapter 3: Modules](https://github.com/josevalim/elixir/blob/master/docs/3_modules.md) | [Index](https://github.com/josevalim/elixir/blob/master/docs/0_index.md) | [Chapter 5: Macros](https://github.com/josevalim/elixir/blob/master/docs/5_macros.md) \ No newline at end of file diff --git a/docs/5_macros.md b/docs/5_macros.md index a0a3a47a03..1088ef54c4 100644 --- a/docs/5_macros.md +++ b/docs/5_macros.md @@ -1,23 +1,25 @@ -## THIS IS A DRAFT, WIP. COME BACK LATER PADAWAN. - -## Meta-programming in Elixir +# 5 Macros Elixir is an homoiconic language. Any Elixir program can be represented using its own data structures. This section describes the Elixir language specification for such data structures. +## 5.1 Building blocks of homoiconicity + The building block of Elixir homoiconicity is a tuple with three elements, for example: { :sum, 1, [1, 2, 3] } The tuple above represents a function call to sum passing 1, 2 and 3 as arguments. The tuple elements are: + { Tuple | Atom, Integer, List | Atom } + * The first element of the tuple is always an atom or another tuple in the same representation; * The second element of the tuple is always an integer representing the line number; -* The third element of the tuple are the arguments for the function call. The third argument may also be an atom (nil or quoted), meaning that it may be a variable call. +* The third element of the tuple are the arguments for the function call. The third argument may also be an atom (`nil` or `:quoted`), meaning that the tuple may not represent a function call, but a variable. You can get the representation of any expression by using the quote macro: - quote { sum(1, 2, 3) } - #=> { :sum, 0, [1, 2, 3] } + iex> quote { sum(1, 2, 3) } + { :sum, 0, [1, 2, 3] } Besides the tuple, Elixir has a few literals. Literals are data types that when quoted return themselves. They are: @@ -30,15 +32,22 @@ Besides the tuple, Elixir has a few literals. Literals are data types that when With those basic structures in mind, we are ready to define our own macro. -### Defining your own macro +## 5.2 Defining our own macro -A macro can be define using `defmacro`. For instance, we can define a macro called unless which works the same as Ruby's unless in just few lines of code: +A macro can be define using `defmacro`. For instance, we can define a macro called `unless` which works the same as Ruby's unless in just few lines of code: - defmacro unless(clause, options) do - quote { if(!unquote(clause), unquote(options)) } + defmodule MyMacro do + defmacro unless(clause, options) do + quote { if(!unquote(clause), unquote(options)) } + end end -In the example above, unless will be called receiving two arguments: a `clause` and `options`. However, note that unless won't receive its values, but its expressions. For example, if one calls: +Similarly to `if`, `unless` expects two arguments: a `clause` and `options`: + + if var, do: IO.puts "true" + unless var, do: IO.puts "false" + +However, since `unless` is a macro, it won't receive values when invoked, but instead, its expressions. For example, if one calls: unless 2 + 2 == 5, do: call_function() @@ -46,7 +55,7 @@ Our `unless` macro will receive the following: unless({:==, 1, [{:+, 1, [2, 2]}, 5]}, { :call_function, 1, [] }) -After being invoked, our `unless` macro will call `quote`, to return a tree representation of the `if` clause. This means we are transforming our `unless` in a `if`! +Then our `unless` macro will call `quote`, to return a tree representation of the `if` clause. This means we are transforming our `unless` in a `if`! However, there is a common mistake when quoting expressions which is that developers usually forget to `unquote` the proper expression. In order to understand what `unquote` does, let's simply remove it: @@ -58,20 +67,72 @@ When called as `unless 2 + 2 == 5, do: call_function()`, our `unless` would then { :if, 0, [{ :!, 0, [{:custom, 0, quoted}]}, do: {:options, 0, quoted}] } -Notice that the tree structure returned by unless is trying to access `custom` and `options` as variables instead of using the `2 + 2 == 5` and `call_function()` expressions we passed as parameters. This is because we forgot to unquote! If we add unquote back: +Notice that the tree structure returned by unless is trying to access `custom` and `options` as variables instead of using the `2 + 2 == 5` and `call_function()` expressions we passed as parameters. This is because we forgot to unquote. If we add `unquote` back: defmacro unless(clause, options) do quote { if(!unquote(clause), unquote(options)) } end -It would return: +Which will then return: { :if, 0, [{ :!, 0, [{:==, 1, [{:+, 1, [2, 2]}, 5]}]}, do: { :call_function, 1, [] }] } -In other words, unquote is a mechanism to inject expressions into the tree being quoted and is essential to the meta-programming mechanism. Elixir also provides `unquote_splicing`. +In other words, unquote is a mechanism to inject expressions into the tree being quoted and is essential to the meta-programming mechanism. Elixir also provides `unquote_splicing` allowing us to inject many expressions at once. -### Locals and macros +## 5.3 Macros hygiene + +Elixir macros follow Scheme conventions and are hygienic. This means a variable defined inside a macro won't conflict with a variable defined in the context the macro is inserted. For example: + + defmodule Hygiene do + defmacro no_interference do + quote { a = 1 } + end + end + + a = 13 + Hygiene.no_interference + a # => 13 + +In the example above, even if the macro injects `a = 1`, it does not affect the variable `a`. In case the macro wants to explicitly affect the context, it can use `var!`: + + defmodule Hygiene do + defmacro interference do + quote { var!(a) = 1 } + end + end + + a = 13 + Hygiene.interference + a # => 1 + +Macros hygiene only works because Elixir marks a variable as coming from the quote. For example, consider this: + + iex> quote { x } + { :x, 0, :quoted } + +Notice that the third element is :quoted. It means that x may be a function call with 0 arguments or a variable coming from a quote. On the other hand, an unquoted variable would have the third element equals to nil. Let's consider this final example: + + defmodule Hygiene do + defmacro quoted(x) do + quote do + { unquote(x), x, x() } + end + end + end + +In the example above, we have defined a macro called `quoted` that returns an unquoted variable, a quoted variable and a function call. Calling this macro will return: + + Hygiene.quoted(x) + #=> { + { :x, 1, nil }, + { :x, 1, :quoted }, + { :x, 1, [] } + } + +Summing up: if the third element is a list, it is certainly a function call. If not, it may be a variable or a function call. The variable is treated differently depending if it comes from a quote, or not. + +## 5.4 Local functions and macros In order to support recursion, macros cannot be called locally. For example, one cannot write: @@ -96,3 +157,8 @@ In order to access the macro, it needs to be defined in an outer module: require MyMacros::Support, import: true delegate [values: 1], to: List end + +With this note, we finish our introduction to macros. Next, let's move to the next chapter which will discuss several topics as native code compilation, partial application and others. + +[Chapter 4: Protocols & Records](https://github.com/josevalim/elixir/blob/master/docs/4_protocols_and_records.md) | [Index](https://github.com/josevalim/elixir/blob/master/docs/0_index.md) | +[Chapter 6: Other topics](https://github.com/josevalim/elixir/blob/master/docs/6_other_topics.md) \ No newline at end of file diff --git a/lib/string/inspect.ex b/lib/string/inspect.ex index e21b5d2107..49c2421742 100644 --- a/lib/string/inspect.ex +++ b/lib/string/inspect.ex @@ -104,7 +104,7 @@ defimpl String::Inspect, for: List do end def container_join([h|t], acc, last) do - acc = << acc | :binary, String::Inspect.inspect(h) | :binary, ', ' >> + acc = << acc | :binary, String::Inspect.inspect(h) | :binary, ?, >> container_join(t, acc, last) end diff --git a/test/elixir/elixir/errors_test.exs b/test/elixir/elixir/errors_test.exs index 4eb6641b34..bb21d60a7b 100644 --- a/test/elixir/elixir/errors_test.exs +++ b/test/elixir/elixir/errors_test.exs @@ -109,7 +109,7 @@ defmodule Elixir::ErrorsTest do end def test_already_compiled_module do - "{:module_already_compiled, \"could not call eval_quoted on module ::Record because it was already compiled\"}" = + "{:module_already_compiled,\"could not call eval_quoted on module ::Record because it was already compiled\"}" = format_catch 'Module.eval_quoted ::Record, quote { 1 }, [], __FILE__, __LINE__' end diff --git a/test/elixir/exunit/escaper_test.exs b/test/elixir/exunit/escaper_test.exs index 6a9b0439cb..8a1a6cb861 100644 --- a/test/elixir/exunit/escaper_test.exs +++ b/test/elixir/exunit/escaper_test.exs @@ -5,6 +5,6 @@ defmodule ExUnit::EscaperTest do def test_escape do escaped = ExUnit::Escaper.escape({ :{}, 1, [2, { :_, 2, false }] }) - "{2, _}" = inspect elem Code.eval_quoted(escaped, [], __FILE__, __LINE__), 1 + "{2,_}" = inspect elem Code.eval_quoted(escaped, [], __FILE__, __LINE__), 1 end end \ No newline at end of file diff --git a/test/elixir/kernel/quote_test.exs b/test/elixir/kernel/quote_test.exs index 63f390faaa..ac3801b6a1 100644 --- a/test/elixir/kernel/quote_test.exs +++ b/test/elixir/kernel/quote_test.exs @@ -12,7 +12,6 @@ defmodule Kernel::QuoteTest::Hygiene do defmacro read_interference do quote { 10 = var!(a) } end - end defmodule Kernel::QuoteTest do diff --git a/test/elixir/string/inspect_test.exs b/test/elixir/string/inspect_test.exs index 02b38d1962..81d5cfbb8a 100644 --- a/test/elixir/string/inspect_test.exs +++ b/test/elixir/string/inspect_test.exs @@ -92,13 +92,13 @@ defmodule String::Inspect::TupleTest do use ExUnit::Case def test_basic do - "{1, \"b\", 3}" = inspect({ 1, "b", 3 }) - "{1, \"b\", 3}" = to_binary({ 1, "b", 3 }) + "{1,\"b\",3}" = inspect({ 1, "b", 3 }) + "{1,\"b\",3}" = to_binary({ 1, "b", 3 }) end def test_record_like do - "{:foo, :bar}" = inspect({ :foo, :bar }) - "{:foo, :bar}" = to_binary({ :foo, :bar }) + "{:foo,:bar}" = inspect({ :foo, :bar }) + "{:foo,:bar}" = to_binary({ :foo, :bar }) end def test_empty do @@ -111,7 +111,7 @@ defmodule String::Inspect::ListTest do use ExUnit::Case def test_basic do - "[1, \"b\", 3]" = inspect([ 1, "b", 3 ]) + "[1,\"b\",3]" = inspect([ 1, "b", 3 ]) <<1,98,3>> = to_binary([ 1, "b", 3 ]) end