From 84adefa331c4159d432d22840663c38f155cd4c1 Mon Sep 17 00:00:00 2001 From: Erlang/OTP Date: Fri, 20 Nov 2009 14:54:40 +0000 Subject: The R13B03 release. --- lib/stdlib/doc/src/ms_transform.xml | 651 ++++++++++++++++++++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 lib/stdlib/doc/src/ms_transform.xml (limited to 'lib/stdlib/doc/src/ms_transform.xml') diff --git a/lib/stdlib/doc/src/ms_transform.xml b/lib/stdlib/doc/src/ms_transform.xml new file mode 100644 index 0000000000..9f178b426c --- /dev/null +++ b/lib/stdlib/doc/src/ms_transform.xml @@ -0,0 +1,651 @@ + + + + +
+ + 20022009 + Ericsson AB. All Rights Reserved. + + + The contents of this file are subject to the Erlang Public License, + Version 1.1, (the "License"); you may not use this file except in + compliance with the License. You should have received a copy of the + Erlang Public License along with this software. If not, it can be + retrieved online at http://www.erlang.org/. + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + the License for the specific language governing rights and limitations + under the License. + + + + ms_transform + Patrik Nyblom + Bjarne Dacker + 1 + Bjarne Däcker + + 99-02-09 + C + ms_transform.sgml +
+ ms_transform + Parse_transform that translates fun syntax into match specifications. + + +

This module implements the parse_transform that makes calls to + ets and dbg:fun2ms/1 translate into literal + match specifications. It also implements the back end for the same + functions when called from the Erlang shell.

+

The translations from fun's to match_specs + is accessed through the two "pseudo + functions" ets:fun2ms/1 and dbg:fun2ms/1.

+

Actually this introduction is more or less an introduction to the + whole concept of match specifications. Since everyone trying to use + ets:select or dbg seems to end up reading + this page, it seems in good place to explain a little more than + just what this module does.

+

There are some caveats one should be aware of, please read through + the whole manual page if it's the first time you're using the + transformations.

+

Match specifications are used more or less as filters. + They resemble usual Erlang matching in a list comprehension or in + a fun used in conjunction with lists:foldl etc. The + syntax of pure match specifications is somewhat awkward though, as + they are made up purely by Erlang terms and there is no syntax in the + language to make the match specifications more readable.

+

As the match specifications execution and structure is quite like + that of a fun, it would for most programmers be more straight forward + to simply write it using the familiar fun syntax and having that + translated into a match specification automatically. Of course a real + fun is more powerful than the match specifications allow, but bearing + the match specifications in mind, and what they can do, it's still + more convenient to write it all as a fun. This module contains the + code that simply translates the fun syntax into match_spec terms.

+

Let's start with an ets example. Using ets:select and + a match specification, one can filter out rows of a table and construct + a list of tuples containing relevant parts of the data in these + rows. Of course one could use ets:foldl instead, but the + select call is far more efficient. Without the translation, one has to + struggle with writing match specifications terms to accommodate this, + or one has to resort to the less powerful + ets:match(_object) calls, or simply give up and use + the more inefficient method of ets:foldl. Using the + ets:fun2ms transformation, a ets:select call + is at least as easy to write as any of the alternatives.

+

As an example, consider a simple table of employees:

+ +-record(emp, {empno, %Employee number as a string, the key + surname, %Surname of the employee + givenname, %Given name of employee + dept, %Department one of {dev,sales,prod,adm} + empyear}). %Year the employee was employed +

We create the table using:

+ +ets:new(emp_tab,[{keypos,#emp.empno},named_table,ordered_set]). +

Let's also fill it with some randomly chosen data for the examples:

+ +[{emp,"011103","Black","Alfred",sales,2000}, + {emp,"041231","Doe","John",prod,2001}, + {emp,"052341","Smith","John",dev,1997}, + {emp,"076324","Smith","Ella",sales,1995}, + {emp,"122334","Weston","Anna",prod,2002}, + {emp,"535216","Chalker","Samuel",adm,1998}, + {emp,"789789","Harrysson","Joe",adm,1996}, + {emp,"963721","Scott","Juliana",dev,2003}, + {emp,"989891","Brown","Gabriel",prod,1999}] +

Now, the amount of data in the table is of course to small to justify + complicated ets searches, but on real tables, using select to get + exactly the data you want will increase efficiency remarkably.

+

Lets say for example that we'd want the employee numbers of + everyone in the sales department. One might use ets:match + in such a situation:

+
+1> ets:match(emp_tab, {'_', '$1', '_', '_', sales, '_'}).
+[["011103"],["076324"]]    
+

Even though ets:match does not require a full match + specification, but a simpler type, it's still somewhat unreadable, and + one has little control over the returned result, it's always a list of + lists. OK, one might use ets:foldl or + ets:foldr instead:

+ +ets:foldr(fun(#emp{empno = E, dept = sales},Acc) -> [E | Acc]; + (_,Acc) -> Acc + end, + [], + emp_tab). +

Running that would result in ["011103","076324"] + , which at least gets rid of the extra lists. The fun is also quite + straightforward, so the only problem is that all the data from the + table has to be transferred from the table to the calling process for + filtering. That's inefficient compared to the ets:match + call where the filtering can be done "inside" the emulator and only + the result is transferred to the process. Remember that ets tables are + all about efficiency, if it wasn't for efficiency all of ets could be + implemented in Erlang, as a process receiving requests and sending + answers back. One uses ets because one wants performance, and + therefore one wouldn't want all of the table transferred to the + process for filtering. OK, let's look at a pure + ets:select call that does what the ets:foldr + does:

+ +ets:select(emp_tab,[{#emp{empno = '$1', dept = sales, _='_'},[],['$1']}]). +

Even though the record syntax is used, it's still somewhat hard to + read and even harder to write. The first element of the tuple, + #emp{empno = '$1', dept = sales, _='_'} tells what to + match, elements not matching this will not be returned at all, as in + the ets:match example. The second element, the empty list + is a list of guard expressions, which we need none, and the third + element is the list of expressions constructing the return value (in + ets this almost always is a list containing one single term). In our + case '$1' is bound to the employee number in the head + (first element of tuple), and hence it is the employee number that is + returned. The result is ["011103","076324"], just as in + the ets:foldr example, but the result is retrieved much + more efficiently in terms of execution speed and memory consumption.

+

We have one efficient but hardly readable way of doing it and one + inefficient but fairly readable (at least to the skilled Erlang + programmer) way of doing it. With the use of ets:fun2ms, + one could have something that is as efficient as possible but still is + written as a filter using the fun syntax:

+ +-include_lib("stdlib/include/ms_transform.hrl"). + +% ... + +ets:select(emp_tab, ets:fun2ms( + fun(#emp{empno = E, dept = sales}) -> + E + end)). +

This may not be the shortest of the expressions, but it requires no + special knowledge of match specifications to read. The fun's head + should simply match what you want to filter out and the body returns + what you want returned. As long as the fun can be kept within the + limits of the match specifications, there is no need to transfer all + data of the table to the process for filtering as in the + ets:foldr example. In fact it's even easier to read then + the ets:foldr example, as the select call in itself + discards anything that doesn't match, while the fun of the + foldr call needs to handle both the elements matching and + the ones not matching.

+

It's worth noting in the above ets:fun2ms example that one + needs to include ms_transform.hrl in the source code, as this is + what triggers the parse transformation of the ets:fun2ms call + to a valid match specification. This also implies that the + transformation is done at compile time (except when called from the + shell of course) and therefore will take no resources at all in + runtime. So although you use the more intuitive fun syntax, it gets as + efficient in runtime as writing match specifications by hand.

+

Let's look at some more ets examples. Let's say one + wants to get all the employee numbers of any employee hired before the + year 2000. Using ets:match isn't an alternative here as + relational operators cannot be expressed there. Once again, an + ets:foldr could do it (slowly, but correct):

+ [E | Acc]; + (_,Acc) -> Acc + end, + [], + emp_tab). ]]> +

The result will be + ["052341","076324","535216","789789","989891"], as + expected. Now the equivalent expression using a handwritten match + specification would look something like this:

+ +

This gives the same result, the is in + the guard part and therefore discards anything that does not have a + empyear (bound to '$2' in the head) less than 2000, just as the guard + in the foldl example. Lets jump on to writing it using + ets:fun2ms

+ + E + end)). ]]> +

Obviously readability is gained by using the parse transformation.

+

I'll show some more examples without the tiresome + comparing-to-alternatives stuff. Let's say we'd want the whole object + matching instead of only one element. We could of course assign a + variable to every part of the record and build it up once again in the + body of the fun, but it's easier to do like this:

+ + Obj + end)). ]]> +

Just as in ordinary Erlang matching, you can bind a variable to the + whole matched object using a "match in then match", i.e. a + =. Unfortunately this is not general in fun's translated + to match specifications, only on the "top level", i.e. matching the + whole object arriving to be matched into a separate variable, + is it allowed. For the one's used to writing match specifications by + hand, I'll have to mention that the variable A will simply be + translated into '$_'. It's not general, but it has very common usage, + why it is handled as a special, but useful, case. If this bothers you, + the pseudo function object also returns the whole matched + object, see the part about caveats and limitations below.

+

Let's do something in the fun's body too: Let's say + that someone realizes that there are a few people having an employee + number beginning with a zero (0), which shouldn't be + allowed. All those should have their numbers changed to begin with a + one (1) instead and one wants the + list ,}]]]> created:

+ +ets:select(emp_tab, ets:fun2ms( + fun(#emp{empno = [$0 | Rest] }) -> + {[$0|Rest],[$1|Rest]} + end)). +

As a matter of fact, this query hit's the feature of partially bound + keys in the table type ordered_set, so that not the whole + table need be searched, only the part of the table containing keys + beginning with 0 is in fact looked into.

+

The fun of course can have several clauses, so that if one could do + the following: For each employee, if he or she is hired prior to 1997, + return the tuple }]]>, for each hired 1997 + or later, but before 2001, return }]]>, for all others return }]]>. All except for the ones named Smith as + they would be affronted by anything other than the tag + guru and that is also what's returned for their numbers; + }]]>:

+ + {guru,E}; + (#emp{empno = E, empyear = Y}) when Y < 1997 -> + {inventory, E}; + (#emp{empno = E, empyear = Y}) when Y > 2001 -> + {newbie, E}; + (#emp{empno = E, empyear = Y}) -> % 1997 -- 2001 + {rookie, E} + end)). ]]> +

The result will be:

+ +[{rookie,"011103"}, + {rookie,"041231"}, + {guru,"052341"}, + {guru,"076324"}, + {newbie,"122334"}, + {rookie,"535216"}, + {inventory,"789789"}, + {newbie,"963721"}, + {rookie,"989891"}] +

and so the Smith's will be happy...

+

So, what more can you do? Well, the simple answer would be; look + in the documentation of match specifications in ERTS users + guide. However let's briefly go through the most useful "built in + functions" that you can use when the fun is to be + translated into a match specification by ets:fun2ms (it's + worth mentioning, although it might be obvious to some, that calling + other functions than the one's allowed in match specifications cannot + be done. No "usual" Erlang code can be executed by the fun being + translated by fun2ms, the fun is after all limited + exactly to the power of the match specifications, which is + unfortunate, but the price one has to pay for the execution speed of + an ets:select compared to ets:foldl/foldr).

+

The head of the fun is obviously a head matching (or mismatching) + one parameter, one object of the table we select + from. The object is always a single variable (can be _) or + a tuple, as that's what's in ets, dets and + mnesia tables (the match specification returned by + ets:fun2ms can of course be used with + dets:select and mnesia:select as well as + with ets:select). The use of = in the head + is allowed (and encouraged) on the top level.

+

The guard section can contain any guard expression of Erlang. + Even the "old" type test are allowed on the toplevel of the guard + (integer(X) instead of is_integer(X)). As the new type tests (the + is_ tests) are in practice just guard bif's they can also + be called from within the body of the fun, but so they can in ordinary + Erlang code. Also arithmetics is allowed, as well as ordinary guard + bif's. Here's a list of bif's and expressions:

+ + The type tests: is_atom, is_constant, is_float, is_integer, + is_list, is_number, is_pid, is_port, is_reference, is_tuple, + is_binary, is_function, is_record + The boolean operators: not, and, or, andalso, orelse + The relational operators: >, >=, <, =<, =:=, ==, =/=, /= + Arithmetics: +, -, *, div, rem + Bitwise operators: band, bor, bxor, bnot, bsl, bsr + The guard bif's: abs, element, hd, length, node, round, size, tl, + trunc, self + The obsolete type test (only in guards): + atom, constant, float, integer, + list, number, pid, port, reference, tuple, + binary, function, record + +

Contrary to the fact with "handwritten" match specifications, the + is_record guard works as in ordinary Erlang code.

+

Semicolons (;) in guards are allowed, the result will be (as + expected) one "match_spec-clause" for each semicolon-separated + part of the guard. The semantics being identical to the Erlang + semantics.

+

The body of the fun is used to construct the + resulting value. When selecting from tables one usually just construct + a suiting term here, using ordinary Erlang term construction, like + tuple parentheses, list brackets and variables matched out in the + head, possibly in conjunction with the occasional constant. Whatever + expressions are allowed in guards are also allowed here, but there are + no special functions except object and + bindings (see further down), which returns the whole + matched object and all known variable bindings respectively.

+

The dbg variants of match specifications have an + imperative approach to the match specification body, the ets dialect + hasn't. The fun body for ets:fun2ms returns the result + without side effects, and as matching (=) in the body of + the match specifications is not allowed (for performance reasons) the + only thing left, more or less, is term construction...

+

Let's move on to the dbg dialect, the slightly + different match specifications translated by dbg:fun2ms.

+

The same reasons for using the parse transformation applies to + dbg, maybe even more so as filtering using Erlang code is + simply not a good idea when tracing (except afterwards, if you trace + to file). The concept is similar to that of ets:fun2ms + except that you usually use it directly from the shell (which can also + be done with ets:fun2ms).

+

Let's manufacture a toy module to trace on

+ +-module(toy). + +-export([start/1, store/2, retrieve/1]). + +start(Args) -> + toy_table = ets:new(toy_table,Args). + +store(Key, Value) -> + ets:insert(toy_table,{Key,Value}). + +retrieve(Key) -> + [{Key, Value}] = ets:lookup(toy_table,Key), + Value. +

During model testing, the first test bails out with a + {badmatch,16} in {toy,start,1}, why?

+

We suspect the ets call, as we match hard on the return value, but + want only the particular new call with + toy_table as first parameter. + So we start a default tracer on the node:

+
+1> dbg:tracer().
+{ok,<0.88.0>}
+

And so we turn on call tracing for all processes, we are going to + make a pretty restrictive trace pattern, so there's no need to call + trace only a few processes (it usually isn't):

+
+2> dbg:p(all,call).
+{ok,[{matched,nonode@nohost,25}]}    
+

It's time to specify the filter. We want to view calls that resemble + )]]>:

+
+3> dbg:tp(ets,new,dbg:fun2ms(fun([toy_table,_]) -> true end)).
+{ok,[{matched,nonode@nohost,1},{saved,1}]}    
+

As can be seen, the fun's used with + dbg:fun2ms takes a single list as parameter instead of a + single tuple. The list matches a list of the parameters to the traced + function. A single variable may also be used of course. The body + of the fun expresses in a more imperative way actions to be taken if + the fun head (and the guards) matches. I return true here, but it's + only because the body of a fun cannot be empty, the return value will + be discarded.

+

When we run the test of our module now, we get the following trace + output:

+ ) call ets:new(toy_table,[ordered_set]) ]]> +

Let's play we haven't spotted the problem yet, and want to see what + ets:new returns. We do a slightly different trace + pattern:

+
+4> dbg:tp(ets,new,dbg:fun2ms(fun([toy_table,_]) -> return_trace() end)).
+

Resulting in the following trace output when we run the test:

+ ) call ets:new(toy_table,[ordered_set]) +(<0.86.0>) returned from ets:new/2 -> 24 ]]> +

The call to return_trace, makes a trace message appear + when the function returns. It applies only to the specific function call + triggering the match specification (and matching the head/guards of + the match specification). This is the by far the most common call in the + body of a dbg match specification.

+

As the test now fails with {badmatch,24}, it's obvious + that the badmatch is because the atom toy_table does not + match the number returned for an unnamed table. So we spotted the + problem, the table should be named and the arguments supplied by our + test program does not include named_table. We rewrite the + start function to:

+ +start(Args) -> + toy_table = ets:new(toy_table,[named_table |Args]). +

And with the same tracing turned on, we get the following trace + output:

+ ) call ets:new(toy_table,[named_table,ordered_set]) +(<0.86.0>) returned from ets:new/2 -> toy_table ]]> +

Very well. Let's say the module now passes all testing and goes into + the system. After a while someone realizes that the table + toy_table grows while the system is running and that for some + reason there are a lot of elements with atom's as keys. You had + expected only integer keys and so does the rest of the system. Well, + obviously not all of the system. You turn on call tracing and try to + see calls to your module with an atom as the key:

+
+1> dbg:tracer().
+{ok,<0.88.0>}
+2> dbg:p(all,call).
+{ok,[{matched,nonode@nohost,25}]}
+3> dbg:tpl(toy,store,dbg:fun2ms(fun([A,_]) when is_atom(A) -> true end)).
+{ok,[{matched,nonode@nohost,1},{saved,1}]}
+

We use dbg:tpl here to make sure to catch local calls + (let's say the module has grown since the smaller version and we're + not sure this inserting of atoms is not done locally...). When in + doubt always use local call tracing.

+

Let's say nothing happens when we trace in this way. Our function + is never called with these parameters. We make the conclusion that + someone else (some other module) is doing it and we realize that we + must trace on ets:insert and want to see the calling function. The + calling function may be retrieved using the match specification + function caller and to get it into the trace message, one + has to use the match spec function message. The filter + call looks like this (looking for calls to ets:insert):

+
+4> dbg:tpl(ets,insert,dbg:fun2ms(fun([toy_table,{A,_}]) when is_atom(A) -> 
+                                    message(caller()) 
+                                  end)). 
+{ok,[{matched,nonode@nohost,1},{saved,2}]}    
+

The caller will now appear in the "additional message" part of the + trace output, and so after a while, the following output comes:

+ ) call ets:insert(toy_table,{garbage,can}) ({evil_mod,evil_fun,2}) ]]> +

You have found out that the function evil_fun of the + module evil_mod, with arity 2, is the one + causing all this trouble.

+

This was just a toy example, but it illustrated the most used + calls in match specifications for dbg The other, more + esotheric calls are listed and explained in the Users guide of the ERTS application, they really are beyond the scope of this + document.

+

To end this chatty introduction with something more precise, here + follows some parts about caveats and restrictions concerning the fun's + used in conjunction with ets:fun2ms and + dbg:fun2ms:

+ +

To use the pseudo functions triggering the translation, one + has to include the header file ms_transform.hrl + in the source code. Failure to do so will possibly result in + runtime errors rather than compile time, as the expression may + be valid as a plain Erlang program without translation.

+
+ +

The fun has to be literally constructed inside the + parameter list to the pseudo functions. The fun cannot + be bound to a variable first and then passed to + ets:fun2ms or dbg:fun2ms, i.e this + will work: ets:fun2ms(fun(A) -> A end) but not this: + F = fun(A) -> A end, ets:fun2ms(F). The later will result + in a compile time error if the header is included, otherwise a + runtime error. Even if the later construction would ever + appear to work, it really doesn't, so don't ever use it.

+
+

Several restrictions apply to the fun that is being translated + into a match_spec. To put it simple you cannot use anything in + the fun that you cannot use in a match_spec. This means that, + among others, the following restrictions apply to the fun itself:

+ + Functions written in Erlang cannot be called, neither + local functions, global functions or real fun's + Everything that is written as a function call will be + translated into a match_spec call to a builtin function, so that + the call is_list(X) will be translated to {'is_list', '$1'} ('$1' is just an example, the numbering may + vary). If one tries to call a function that is not a match_spec + builtin, it will cause an error. + Variables occurring in the head of the fun will be + replaced by match_spec variables in the order of occurrence, so + that the fragment fun({A,B,C}) will be replaced by + {'$1', '$2', '$3'} etc. Every occurrence of such a + variable later in the match_spec will be replaced by a + match_spec variable in the same way, so that the fun + fun({A,B}) when is_atom(A) -> B end will be translated into + [{{'$1','$2'},[{is_atom,'$1'}],['$2']}]. + +

Variables that are not appearing in the head are imported + from the environment and made into + match_spec const expressions. Example from the shell:

+
+1> X = 25.
+25
+2> ets:fun2ms(fun({A,B}) when A > X -> B end).
+[{{'$1','$2'},[{'>','$1',{const,25}}],['$2']}]
+
+ +

Matching with = cannot be used in the body. It can only + be used on the top level in the head of the fun. + Example from the shell again:

+
+1> ets:fun2ms(fun({A,[B|C]} = D) when A > B -> D end).
+[{{'$1',['$2'|'$3']},[{'>','$1','$2'}],['$_']}]
+2> ets:fun2ms(fun({A,[B|C]=D}) when A > B -> D end).
+Error: fun with head matching ('=' in head) cannot be translated into 
+match_spec 
+{error,transform_error}
+3> ets:fun2ms(fun({A,[B|C]}) when A > B -> D = [B|C], D end).
+Error: fun with body matching ('=' in body) is illegal as match_spec
+{error,transform_error}        
+

All variables are bound in the head of a match_spec, so the + translator can not allow multiple bindings. The special case + when matching is done on the top level makes the variable bind + to '$_' in the resulting match_spec, it is to allow a more + natural access to the whole matched object. The pseudo + function object() could be used instead, see below. + The following expressions are translated equally:

+ +ets:fun2ms(fun({a,_} = A) -> A end). +ets:fun2ms(fun({a,_}) -> object() end). +
+ +

The special match_spec variables '$_' and '$*' + can be accessed through the pseudo functions object() + (for '$_') and bindings() (for '$*'). + as an example, one could translate the following + ets:match_object/2 call to a ets:select call:

+ +ets:match_object(Table, {'$1',test,'$2'}). +

...is the same as...

+ +ets:select(Table, ets:fun2ms(fun({A,test,B}) -> object() end)). +

(This was just an example, in this simple case the former + expression is probably preferable in terms of readability). + The ets:select/2 call will conceptually look like this + in the resulting code:

+ +ets:select(Table, [{{'$1',test,'$2'},[],['$_']}]). +

Matching on the top level of the fun head might feel like a + more natural way to access '$_', see above.

+
+ Term constructions/literals are translated as much as is + needed to get them into valid match_specs, so that tuples are + made into match_spec tuple constructions (a one element tuple + containing the tuple) and constant expressions are used when + importing variables from the environment. Records are also + translated into plain tuple constructions, calls to element + etc. The guard test is_record/2 is translated into + match_spec code using the three parameter version that's built + into match_specs, so that is_record(A,t) is translated + into {is_record,'$1',t,5} given that the record size of + record type t is 5. + Language constructions like case, if, + catch etc that are not present in match_specs are not + allowed. + If the header file ms_transform.hrl is not included, + the fun won't be translated, which may result in a + runtime error (depending on if the fun is valid in a + pure Erlang context). Be absolutely sure that the header is + included when using ets and dbg:fun2ms/1 in + compiled code. + If the pseudo function triggering the translation is + ets:fun2ms/1, the fun's head must contain a single + variable or a single tuple. If the pseudo function is + dbg:fun2ms/1 the fun's head must contain a single + variable or a single list. +
+

The translation from fun's to match_specs is done at compile + time, so runtime performance is not affected by using these pseudo + functions. The compile time might be somewhat longer though.

+

For more information about match_specs, please read about them + in ERTS users guide.

+
+ + + parse_transform(Forms,_Options) -> Forms + Transforms Erlang abstract format containing calls to ets/dbg:fun2ms into literal match specifications. + + Forms = Erlang abstract code format, see the erl_parse module description + _Options = Option list, required but not used + + +

Implements the actual transformation at compile time. This + function is called by the compiler to do the source code + transformation if and when the ms_transform.hrl header + file is included in your source code. See the ets and + dbg:fun2ms/1 function manual pages for + documentation on how to use this parse_transform, see the + match_spec chapter in ERTS users guide for a + description of match specifications.

+
+
+ + transform_from_shell(Dialect,Clauses,BoundEnvironment) -> term() + Used when transforming fun's created in the shell into match_specifications. + + Dialect = ets | dbg + Clauses = Erlang abstract form for a single fun + BoundEnvironment = [{atom(), term()}, ...], list of variable bindings in the shell environment + + +

Implements the actual transformation when the fun2ms + functions are called from the shell. In this case the abstract + form is for one single fun (parsed by the Erlang shell), and + all imported variables should be in the key-value list passed + as BoundEnvironment. The result is a term, normalized, + i.e. not in abstract format.

+
+
+ + format_error(Errcode) -> ErrMessage + Error formatting function as required by the parse_transform interface. + + Errcode = term() + ErrMessage = string() + + +

Takes an error code returned by one of the other functions + in the module and creates a textual description of the + error. Fairly uninteresting function actually.

+
+
+
+
+ -- cgit v1.2.3