Ok, this isn't actually all Erlang. In fact, by line-count, it's a Postscript project, but all of those lines were already written by someone else. Also, I'm not sure whether I'll get the same benefit here that I got out of my Strifebarge write-up, but it's the third such piece, so I've gone back and added labels to group them.
"Almost Literate Programming".
What I'm doing isn't quite the LP that Knuth advocates because it doesn't self-extract, share space with the executable source, or make use of variable labels to automatically update certain portions. However, it still gains me considerable reflective clarity about what the goal of the program is, and it hopefully conveys the essence to whoever happens to be reading. With that out of the way...
Generating Barcodes
As you may have noticed from the above links, there already exists a Postscript-based barcode generator which I'm going to use pretty shamelessly in order to generate bitmap barcodes of various descriptions. Taking a look at the actual code for that generator should make it obvious that you probably don't want to just echo the entire system every time you need to generate something[1]. We'll get to that though, lets start from the system side first. This is what a .app
declaration looks like in Erlang
# Makefile all: *.erl *.c make wand erlc -W *erl run: erl -name ps_barcode@127.0.1.1 -eval 'application:load(ps_barcode).' -eval 'application:start(ps_barcode).' wand: wand.c erl_comm.c driver.c gcc -o wand `pkg-config --cflags --libs MagickWand` wand.c erl_comm.c driver.c clean: rm wand rm *beam
%% ps_barcode.app {application, ps_barcode, [{description, "barcode image generator based on ps-barcode"}, {vsn, "1.0"}, {modules, [ps_barcode_app, ps_barcode_supervisor, barcode_data, wand, ps_bc]}, {registered, [ps_bc, wand, ps_barcode_supervisor]}, {applications, [kernel, stdlib]}, {mod, {ps_barcode_app, []}}, {start_phases, []}]}. -module(ps_barcode_app). -behaviour(application). -export([start/2, stop/1]). start(_Type, StartArgs) -> ps_barcode_supervisor:start_link(StartArgs). stop(_State) -> ok.
-module(ps_barcode_supervisor). -behavior(supervisor). -export([start/0, start_for_testing/0, start_link/1, init/1]). start() -> spawn(fun() -> supervisor:start_link({local, ?MODULE}, ?MODULE, _Arg = []) end). start_for_testing() -> {ok, Pid} = supervisor:start_link({local, ?MODULE}, ?MODULE, _Arg = []), unlink(Pid). start_link(Args) -> supervisor:start_link({local, ?MODULE}, ?MODULE, Args). init([]) -> {ok, {{one_for_one, 3, 10}, [{tag1, {wand, start, []}, permanent, brutal_kill, worker, [wand]}, {tag2, {ps_bc, start, []}, permanent, 10000, worker, [ps_bc]}]}}.
The Makefile
is not, strictly speaking, necessary, but a bunch of stuff needs to be done manually in its absence. The above code is approximately equivalent to a Lisp .asd
file, in that it tells Erlang what needs to be compiled/called in order to run the system I'm about to define[2].
{modules, [ps_barcode_app, ps_barcode_supervisor, barcode_data, wand, ps_bc]},
That line specifies which other modules we'll be loading as part of the application, as well as their start order (which is relevant for a certain supervision strategy).
{registered, [ps_bc, wand, ps_barcode_supervisor]},
That one specifies registered processes we expect.
{mod, {ps_barcode_app, []}},
That one tells Erlang which module's start
function to call in order to start the application, and what arguments to pass it as StartArgs
.
init([]) ->
{ok, {{one_for_one, 3, 10},
[{tag1,
{wand, start, []},
permanent,
brutal_kill,
worker,
[wand]},
{tag2,
{ps_bc, start, []},
permanent,
10000,
worker,
[ps_bc]}]}}.
That does something interesting; it defines how the supervisor should act, and how it should treat its child processes. {one_for_one, 3, 10}
means that if a supervised process errors, it should be restarted on its own up to 3 times in 10 seconds[3]. Both sub-processes are permanent
[4] worker
s[5]. The last interesting bit is the brutal_kill
/10000
part; that's the Shutdown
variable. It determines how the process should be terminated; brutal_kill
means "kill the process right away", an integer means "send the process a stop command and wait up to this many milliseconds, then kill it".
Lets follow the applications' start order and move on to
-module(barcode_data). -export([read_default_file/0, read_file/1]). -export([export_ets_file/1, import_ets_file/0]). export_ets_file(Table) -> ets:tab2file(Table, "ps-barcode-blocks"). import_ets_file() -> {ok, Tab} = ets:file2tab(filename:absname("ps-barcode-blocks")), Tab. read_default_file() -> read_file("barcode.ps"). read_file(Filename) -> {ok, File} = file:open(Filename, [read]), TableId = ets:new(ps_barcode_blocks, [ordered_set]), trim_flash(File), {ok, Tab} = read_all_blocks(File, TableId), file:close(File), Tab. trim_flash(IoDevice) -> read_until(IoDevice, "% --BEGIN TEMPLATE"). read_all_blocks(IoDevice, TableId) -> case Res = read_block(IoDevice) of [] -> {ok, TableId}; _ -> ets:insert(TableId, parse_block(Res)), read_all_blocks(IoDevice, TableId) end. read_block(IoDevice) -> read_until(IoDevice, "% --END "). parse_block([["%", "BEGIN", "PREAMBLE"] | Body]) -> {preamble, lists:append(Body)}; parse_block([["%", "BEGIN", "RENDERER", Name] | Body]) -> {list_to_atom(Name), renderer, lists:append(Body)}; parse_block([["%", "BEGIN", "ENCODER", Name] | Body]) -> parse_encoder_meta(Name, Body); parse_block(_) -> {none}. parse_encoder_meta (Name, Encoder) -> parse_encoder_meta(Name, Encoder, [], {[], [], []}). parse_encoder_meta (Name, [["%", "RNDR:" | Renderers] | Rest], Acc, {_, R, S}) -> parse_encoder_meta(Name, Rest, Acc, {Renderers, R, S}); parse_encoder_meta (Name, [["%", "REQUIRES" | Reqs] | Rest], Acc, {A, _, S}) -> parse_encoder_meta(Name, Rest, Acc, {A, Reqs, S}); parse_encoder_meta (Name, [["%", "SUGGESTS" | Suggs] | Rest], Acc, {A, R, _}) -> parse_encoder_meta(Name, Rest, Acc, {A, R, Suggs}); parse_encoder_meta (Name, [["%", "EXOP:" | Exop] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, [{def_arg, Exop} | Acc], Cmp); parse_encoder_meta (Name, [["%", "EXAM:" | Exam] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, [{example, string:join(Exam, " ")} | Acc], Cmp); parse_encoder_meta (Name, [["%" | _] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, Acc, Cmp); parse_encoder_meta (Name, Body, [DefArgs, Example], {A, R, S}) -> Reqs = [list_to_atom(strip_nl(X)) || X <- lists:append([A, R, S])], {list_to_atom(Name), encoder, {requires, Reqs}, Example, DefArgs, lists:append(Body)}. strip_nl(String) -> string:strip(String, right, $\n). read_until(IoDevice, StartsWith) -> read_until(IoDevice, StartsWith, []). read_until(IoDevice, StartsWith, Acc) -> case file:read_line(IoDevice) of {ok, "\n"} -> read_until(IoDevice, StartsWith, Acc); {ok, Line} -> case lists:prefix(StartsWith, Line) of true -> lists:reverse(Acc); false -> read_until(IoDevice, StartsWith, [process_line(Line) | Acc]) end; {error, _} -> error; eof -> lists:reverse(Acc) end. process_line(Line) -> case lists:prefix("% --", Line) of true -> split_directive_line(Line); false -> Line end. split_directive_line(Line) -> [X || X <- re:split(strip_nl(Line), "( |--)", [{return, list}]), X /= " ", X /= [], X /= "--", X /="\n"].
This is a reasonably simple reader program. The goal of it is to break that 17111 line .ps file into individual components. First, a preamble
(basic definitions that need to go into each file), then a set of renderer
s[6], and a rather large number of encoder
s[7]. These components are stored in an ETS table held in memory. The initial Postscript file only needs to be parsed once; the resulting ETS table is then exported to a file on disk so that it can just be loaded in the future.
Do note the nested case
statements there. Last time, I complained about the guards, and this is why. Really, I should have been able to write that as
... {ok, Line} where lists:prefix(StartsWith, Line) -> lists:reverse(Acc); {ok, Line} -> read_until(IoDevice, StartsWith, [process_line(Line) | Acc]); ...
but even though lists:prefix
is a perfectly functional predicate, it's not in that blessed subset of Erlang that can be called from within a guard sequence. The consequence, in this case, is that I have to bust out a second case
block, and waste six lines doing it. Moving onto sorting PS blocks...
parse_block([["%", "BEGIN", "PREAMBLE"] | Body]) -> {preamble, lists:append(Body)}; parse_block([["%", "BEGIN", "RENDERER", Name] | Body]) -> {list_to_atom(Name), renderer, lists:append(Body)}; parse_block([["%", "BEGIN", "ENCODER", Name] | Body]) -> parse_encoder_meta(Name, Body); parse_block(_) -> {none}.
The preamble
and renderer
s are really just named strings, but the encoder
s have more metadata about them.
parse_encoder_meta (Name, Encoder) -> parse_encoder_meta(Name, Encoder, [], {[], [], []}). parse_encoder_meta (Name, [["%", "RNDR:" | Renderers] | Rest], Acc, {_, R, S}) -> parse_encoder_meta(Name, Rest, Acc, {Renderers, R, S}); parse_encoder_meta (Name, [["%", "REQUIRES" | Reqs] | Rest], Acc, {A, _, S}) -> parse_encoder_meta(Name, Rest, Acc, {A, Reqs, S}); parse_encoder_meta (Name, [["%", "SUGGESTS" | Suggs] | Rest], Acc, {A, R, _}) -> parse_encoder_meta(Name, Rest, Acc, {A, R, Suggs}); parse_encoder_meta (Name, [["%", "EXOP:" | Exop] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, [{def_arg, Exop} | Acc], Cmp); parse_encoder_meta (Name, [["%", "EXAM:" | Exam] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, [{example, string:join(Exam, " ")} | Acc], Cmp); parse_encoder_meta (Name, [["%" | _] | Rest], Acc, Cmp) -> parse_encoder_meta(Name, Rest, Acc, Cmp); parse_encoder_meta (Name, Body, [DefArgs, Example], {A, R, S}) -> Reqs = [list_to_atom(strip_nl(X)) || X <- lists:append([A, R, S])], {list_to_atom(Name), encoder, {requires, Reqs}, Example, DefArgs, lists:append(Body)}.
This is not the most elegant function. In fact, now that I look at it, it seems like I could fairly easily replace that {A, R, S}
tuple with a list accumulator (and I will in the next version). What we're doing here is breaking apart an encoder
block, and pulling out
- the list of other blocks we need to output before this one[8]
- a piece of example data that this particular encoder can handle[9]
- the default arguments to passed to this
encoder
- the body code of this
encoder
The list of required blocks is exhaustive for each encoder
, so we don't need to recursively check requirements later, it's enough to store and act on all requirements of a given barcode.
-export([read_default_file/0, read_file/1]). -export([export_ets_file/1, import_ets_file/0]).
Those exported functions are really all that a user of this module should ever need to call; you're either processing a new revision of the ps
file, or you're importing the already exported ETS table derived from the ps
file, or you're exporting a new ETS table for later loading. Now that we've seen how we store the relevant data, lets take a look at
-module(wand). -behaviour(gen_server). -export([start/0, stop/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([process/1]). process(Filename) -> gen_server:call(?MODULE, {process_barcode, Filename}). handle_call({process_barcode, Filename}, _From, State) -> State ! {self(), {command, Filename}}, receive {State, {data, Data}} -> {reply, decode(Data), State} end; handle_call({'EXIT', _Port, Reason}, _From, _State) -> exit({port_terminated, Reason}). decode([0]) -> {ok, 0}; decode([1]) -> {error, could_not_read}; decode([2]) -> {error, could_not_write}. %%%%%%%%%%%%%%%%%%%% generic actions start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> gen_server:call(?MODULE, stop). %%%%%%%%%%%%%%%%%%%% gen_server handlers init([]) -> {ok, open_port({spawn, filename:absname("wand")}, [{packet, 2}])}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, State) -> State ! {self(), close}, ok. code_change(_OldVsn, State, _Extra) -> {ok, State}.
This is actually pretty much the same C Port file I used last time, except that this one has been re-written to use gen_server
, rather than being plain Erlang code. I still refuse to use that godawful file template they ship with their Emacs mode though[10]. All it does is call out to a C program named wand
to do the actual image processing involved in generating these barcodes. All you need to know is that we send it a barcodes' file name, and it quickly generates a high-res PNG version in the same folder.
Right, that's it for the periphery, lets finally dive into
-module(ps_bc). -behaviour(gen_server). -export([start/0, stop/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([help/0, help/1, write/3, write/5, generate/2, generate/3, change/1, make_tempname/0]). help() -> gen_server:call(?MODULE, help). help(BarcodeType) -> gen_server:call(?MODULE, {help, BarcodeType}). write(DestFolder, BarcodeType, Data) -> write(DestFolder, BarcodeType, Data, 200, 200). write(DestFolder, BarcodeType, Data, Width, Height) -> gen_server:call(?MODULE, {write, DestFolder, BarcodeType, Data, Width, Height}). generate(BarcodeType, Data) -> generate("/tmp/", BarcodeType, Data). generate(DestFolder, BarcodeType, Data) -> NameOfTempFile = write(DestFolder, BarcodeType, Data), wand:process(NameOfTempFile), NameOfTempFile. change(TableId) -> gen_server:call(?MODULE, {change, TableId}). handle_call(help, _From, State) -> {reply, ets:match(State, {'$1', encoder, '_', '_', '_', '_'}), State}; handle_call({help, BarcodeType}, _From, State) -> {reply, ets:match(State, {BarcodeType, encoder, '_', '$1', '_', '_'}), State}; handle_call({write, DestFolder, BarcodeType, Data, Width, Height}, _From, State) -> Fname = make_tempname(DestFolder), {ok, File} = file:open(Fname, [write, exclusive]), [[{requires, CompList}, {def_arg, ExArgs}]] = ets:match(State, {BarcodeType, encoder, '$1', '_', '$2', '_'}), file:write(File, io_lib:format("%!PS-Adobe-2.0\n%%BoundingBox: 0 0 ~w ~w\n%%LanguageLevel: 2\n", [Width, Height])), write_component(preamble, State, File), file:write(File, "\n/Helvetica findfont 10 scalefont setfont\n"), lists:map(fun (C) -> write_component(C, State, File) end, CompList), write_component(BarcodeType, State, File), write_barcode(File, BarcodeType, ExArgs, Data), file:close(File), {reply, Fname, State}; handle_call({change_table, Tab}, _From, _State) -> {reply, {watching_table, Tab}, Tab}. make_tempname() -> {A, B, C} = now(), [D, E, F] = lists:map(fun integer_to_list/1, [A, B, C]), lists:append(["tmp.", D, ".", E, ".", F]). make_tempname(TargetDir) -> filename:absname_join(TargetDir, make_tempname()). write_component(preamble, Table, File) -> [[Pre]] = ets:match(Table, {preamble, '$1'}), file:write(File, Pre); write_component(Name, Table, File) -> file:write(File, lookup_component(Name, Table)). write_barcode(File, datamatrix, _, Data) -> format_barcode_string(File, datamatrix, "", Data); write_barcode(File, BarcodeType, ExArgs, Data) -> format_barcode_string(File, BarcodeType, string:join(ExArgs, " "), Data). format_barcode_string(File, BarcodeType, ExArgString, DataString) -> io:format(File, "10 10 moveto (~s) (~s) /~s /uk.co.terryburton.bwipp findresource exec showpage", [DataString, ExArgString, BarcodeType]). lookup_component(Name, Table) -> Ren = ets:match(Table, {Name, renderer, '$1'}), Enc = ets:match(Table, {Name, encoder, '_', '_', '_', '$1'}), case {Ren, Enc} of {[], [[Res]]} -> Res; {[[Res]], []} -> Res end. %%%%%%%%%%%%%%%%%%%% generic actions start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% {local/global, Name}, Mod, InitArgs, Opts stop() -> gen_server:call(?MODULE, stop). %%%%%%%%%%%%%%%%%%%% gen_server handlers init([]) -> {ok, barcode_data:import_ets_file()}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}.
This is where the meat of the application resides, so I'll take my time with it.
First off, note that the init
function loads that ETS file we generated in barcode_data
.
init([]) -> {ok, barcode_data:import_ets_file()}.
That's where our data is stored, and we'll be looking up components by referring to it.
lookup_component(Name, Table) -> Ren = ets:match(Table, {Name, renderer, '$1'}), Enc = ets:match(Table, {Name, encoder, '_', '_', '_', '$1'}), case {Ren, Enc} of {[], [[Res]]} -> Res; {[[Res]], []} -> Res end.
This is (again) not the most elegant code. Really, what I'd want to do is look up Ren
first, check if it returned something, then check whether an Enc
exists. That would save me a look-up every once in a while[11]. Do take note that there's no clause to handle the event that a faulty index was passed; in that case, the process will fail with an unmatched pattern and promptly be restarted. This function is in turn used by write_component
to actually output the given block to a file
write_component(preamble, Table, File) -> [[Pre]] = ets:match(Table, {preamble, '$1'}), file:write(File, Pre); write_component(Name, Table, File) -> file:write(File, lookup_component(Name, Table)).
Before we tear into handle
, just one more note about the remaining interesting helper function
make_tempname() -> {A, B, C} = now(), [D, E, F] = lists:map(fun integer_to_list/1, [A, B, C]), lists:append(["tmp.", D, ".", E, ".", F]). make_tempname(TargetDir) -> filename:absname_join(TargetDir, make_tempname()).
Actually, two functions (make_tempname/0
and make_tempname/1
). I thought about just using os:cmd("mktemp")
instead, but decided against it. make_tempname
uses now()
to generate a unique temporary filename. It optionally takes a directory specification, in which case it creates an absolute filename in that directory.
By the way, that's how you handle optional arguments in Erlang. You create multiple functions with the same name, but with different arity and just write the matching expression for each. It's surprisingly elegant, and the only thing differentiating these from a single function declaration is that they're separated by .
rather than by ;
. Obviously, if you plan on extending such a function to the users of your module, you need to export all the arities you've defined. I wasn't being needlessly pedantic earlier, Erlang treats make_tempname/0
and make_tempname/1
as completely separate functions.
Right, now then.
handle_call(help, _From, State) -> {reply, ets:match(State, {'$1', encoder, '_', '_', '_', '_'}), State}; handle_call({help, BarcodeType}, _From, State) -> {reply, ets:match(State, {BarcodeType, encoder, '_', '$1', '_', '_'}), State}; handle_call({write, DestFolder, BarcodeType, Data, Width, Height}, _From, State) -> Fname = make_tempname(DestFolder), {ok, File} = file:open(Fname, [write, exclusive]), [[{requires, CompList}, {def_arg, ExArgs}]] = ets:match(State, {BarcodeType, encoder, '$1', '_', '$2', '_'}), file:write(File, io_lib:format("%!PS-Adobe-2.0\n%%BoundingBox: 0 0 ~w ~w\n%%LanguageLevel: 2\n", [Width, Height])), write_component(preamble, State, File), file:write(File, "\n/Helvetica findfont 10 scalefont setfont\n"), lists:map(fun (C) -> write_component(C, State, File) end, CompList), write_component(BarcodeType, State, File), write_barcode(File, BarcodeType, ExArgs, Data), file:close(File), {reply, Fname, State}; handle_call({change_table, Tab}, _From, _State) -> {reply, {watching_table, Tab}, Tab}.
The last directive there should probably be implemented as a handle_cast
rather than handle_call
[12]. The first two should probably return processed data rather than raw ETS results. Rest assured that mental notes have been made. The message help
returns a list of available encoder
s[13], while asking for help with a specific encoder
will return its example data. All the meat is in that extra large message handler in the middle.
Deep breath.
A message of {write, DestFolder, BarcodeType, Data, Width, Height}
will output Data
in a BarcodeType
barcode in the DestFolder
folder and format it to Width
xHeight
dimensions. That's actually going to get trickier. Right now, the dimensions are just assumed to be 200x200 in the initial PS, and that C module is expected to output a properly formatted PS file. There are a few problems with that though[14], so what I will ultimately want to do is have the C module return the appropriate dimensions and have ps_bc
change this initial file later. That's another TODO.
What the write
message actually does, in order is
- generates a tempfile name for the directory it was passed
- opens that
File
for output - looks up the required blocks in our ETS table
- writes the preamble to
File
- writes the required blocks to
File
- writes the barcode component to
File
- writes a Postscript directive invoking that component with
Data
toFile
- closes
File
- replies with the absolute tempfile name that it generated
And there you have it, we now have a barcode PS file in the specified location.
The rest of the functions here are either gen_server
pieces (which I won't go into), or interface functions (which I will)
help() -> gen_server:call(?MODULE, help). help(BarcodeType) -> gen_server:call(?MODULE, {help, BarcodeType}). write(DestFolder, BarcodeType, Data) -> write(DestFolder, BarcodeType, Data, 200, 200). write(DestFolder, BarcodeType, Data, Width, Height) -> gen_server:call(?MODULE, {write, DestFolder, BarcodeType, Data, Width, Height}). generate(BarcodeType, Data) -> generate("/tmp/", BarcodeType, Data). generate(DestFolder, BarcodeType, Data) -> NameOfTempFile = write(DestFolder, BarcodeType, Data), wand:process(NameOfTempFile), NameOfTempFile. change(TableId) -> gen_server:call(?MODULE, {change, TableId}).
This is a set of exported functions to let outside modules easily interact with the internal ps_bc
process. change
, help
and write
map to the corresponding handle_call
messages we looked at earlier[15]. generate
is something else. This is the principal function I expect to be called from outside the module, though AFAIK, there's no way to highlight that from within the code. To that end, it collects everything you need to create a barcode from start to finish; it accepts a BarcodeType
and Data
(and optionally a DestFolder
) and calls write/3
to create the directory, then wand:process
to create the corresponding PNG and rasterized PS file, and finally returns the tempfile name that it generated. That should probably actually return a list of absolute file-names it created rather than just the base name. Mental note number 6.
Whew! At the risk of pulling a Yegge, this piece is turning out a lot longer than I though it was going to be. Lets get it wrapped up quickly.
Nitrogen
Nitrogen is an Erlang web framework I've been playing with. I won't explain it in depth, just use it to show you how you'd go about invoking the above program for realsies. In fact, here's a nitrogen/rel/nitrogen/site/src/index.erl
that will call out to ps_barcode
to generate a barcode based on user input and let them download the bitmap and Postscript file:
%% -*- mode: nitrogen -*- -module (index). -compile(export_all). -include_lib("nitrogen_core/include/wf.hrl"). main() -> #template { file="./site/templates/bare.html" }. title() -> "Welcome to Nitrogen". body() -> #container_12 { body=[ #grid_8 { alpha=true, prefix=2, suffix=2, omega=true, body=inner_body() } ]}. inner_body() -> [ #h3 { text="PS Barcode Generator" }, #h1 { text="In MOTHERFUCKING ERLANG"}, #p{}, #textbox { id=barcode_data, text=get_example(qrcode)}, barcode_type_dropdown(qrcode), #button { id=button, text="Generate", postback=click }, #p{ id=result, body=[ #image { id=barcode_img }, #p { id=barcode_link } ]} ]. barcode_type_dropdown(DefaultType) -> Types = rpc:call('ps_barcode@127.0.1.1', ps_bc, help, []), #dropdown { id=barcode_type, value=DefaultType, postback=select_type, options=lists:map(fun ([T]) -> #option {text=T, value=T} end, Types) }. get_example(BarcodeType) -> [[{example,Example}]] = rpc:call('ps_barcode@127.0.1.1', ps_bc, help, [BarcodeType]), Example. event(click) -> [_, Fname] = re:split( rpc:call('ps_barcode@127.0.1.1', ps_bc, generate, [filename:absname("site/static/images"), list_to_atom(wf:q(barcode_type)), wf:q(barcode_data)]), "site/static", [{return, list}]), wf:replace(barcode_img, #image { id=barcode_img, image=string:concat(Fname, ".png"), actions=#effect { effect=highlight } }), wf:replace(barcode_link, #link { id=barcode_link, text="Download PS file", url=string:concat(Fname, ".ps") }); event(select_type) -> wf:set(barcode_data, get_example(list_to_atom(wf:q(barcode_type)))).
The actual calls to our application happen
get_example(BarcodeType) -> [[{example,Example}]] = rpc:call('ps_barcode@127.0.1.1', ps_bc, help, [BarcodeType]), Example.
here[16] and
... rpc:call('ps_barcode@127.0.1.1', ps_bc, generate, [filename:absname("site/static/images"), list_to_atom(wf:q(barcode_type)), wf:q(barcode_data)]), "site/static", [{return, list}]), ...
here. Recall that make run
on the Makefile
I defined earlier started a node named 'ps_barcode@127.0.1.1'
and started our application in it. So, if we want to use it from another Erlang node, all we have to do is start them both up using the same cookie
, and then use the built in rpc:call
function, specifying the appropriate node, module, function and arguments. The return message is going to be a response from our application.
The code shown here won't actually run on its own[17], I left out the C file[18], as well as the actual barcode.ps that the whole thing is based on. I'll act on the mental notes I've collected first, and then toss the whole thing up on my github for you to play with. The nitrogen module is minimal enough that I won't feel bad for leaving it out, but the one above should work with your copy of nitrogen.
It's actually just a minimally modified version of the default index.erl
file that comes with the framework, the only interesting pieces in it are the rpc:call
lines which demonstrate the hands-down most interesting thing about Erlang. The thing that justifies putting up with all the warts and annoyances[19]. I'll expand on that next time though, this was already more than enough stuff coming out of my mind.
Footnotes
1 - [back] - The complete file is 17111 lines, and we really only need about 800-1200 at the outside to generate a single specific barcode.
2 - [back] - Incidentally, I didn't do this first. I sort of wish I had in retrospect, because it would have saved me some dicking around with erl
, but I actually wrote the code first, then wrote the above based on it. Also incidentally, a lot of it doesn't seem like much of it will change on a project-by-project basis. That tells me that we're either working with the wrong abstractions, or there are tricky things you can do at this stage that I haven't yet grasped. It also tells me that I should probably write some generation scripts for it.
3 - [back] - one_for_all
and rest_for_one
are other possible strategies, _all
restarts all child processes rather than just the one that errored, and rest_
just restarts processes later in the start order.
4 - [back] - Which means they get restarted when they error, and hang around after they've finished their work.
5 - [back] - Which means that we have a pretty shallow supervision tree in this case, but we really don't need more.
6 - [back] - Routines that do general operations for a particular class of barcode, such as linear or matrix.
7 - [back] - Routines that do the job of converting a specific piece of data into a particular type of barcode, such as qrcode, code93 or datamatrix.
8 - [back] - renderer
s, required encoder
s and suggested encoders
.
9 - [back] - Some, like datamatrix and qrcode, can handle almost arbitrary string information, while others are restricted to a subset of ascii, and others require a specific number of numeric characters.
10 - [back] - As an aside here, that's one of the things that really rustles my jimmies about Erlang. I've gotten extremely used to including a pretty extensive documentation string with each Common Lisp function and method, knowing that a potential user will be able to make full use of any describe
calls they make. It's actually even better for methods, since you get the documentation for the generic
you define, as well as a compilation of all doc-strings for the related defmethod
calls. Erlang isn't having any of this shit. If you want to include doc-strings, you can damn well write Java-style precisely formatted comments and use a separate doc extractor to read them. I guess this is how most languages do it? It still seems stupid to have a system this dynamic that doesn't allow runtime documentation pokes. Sigh. Ok, let's get back to it.
11 - [back] - Which, granted, isn't really worth saving given how blazingly fast ETS is, but still.
12 - [back] - The only difference being that handle_cast
doesn't send a response message to its caller.
13 - [back] - Which we'll use later to give the user something to do about them.
14 - [back] - Specifically, since I'm using the Imagemagick API, the PS that it outputs is actually rasterised. That means it'll be much larger than the initial file and take that much longer to output. Literally the only advantage to it is that it properly sets the width and height of the document.
15 - [back] - Note that I do export help/0
, help/1
, write/3
and write/5
separately.
16 - [back] - Which, again, really should be expecting a naked string response rather than a raw ETS lookup record.
17 - [back] - The complete code doesn't quite work yet either. Most of it does what it's supposed to, but I've already found one odd case where things don't quite work the way they're supposed to. Tips and patches welcome.
18 - [back] - Which I was actually going to discuss, but this has gone on quite long enough already.
19 - [back] - At least, until I learn enough about it to put together an analogous system in Common Lisp :P
No comments:
Post a Comment