Mason jars
I think government lockdowns turned me into a hipster coder, because I can’t stop falling in love with programming tools from a bygone era.
Recently, I stumbled into a pre-UNIX turing complete program called ‘dc,’ or Desk Calculator. It works on a stack, and one interacts with it using postfix (reverse polish) notation.
I decided to use it for my taxes this year, and was overjoyed in how fun it was to script with.
I noticed the interactive mode was exceeding simple to deal with and I thought this would make for a great resource OTP could tap into via ports.
After a little troubleshooting, this start/stop Erlang service came into fruition with a surprising small amount of code. Here’s the module:
-module(dc).
-export([start/0, stop/0, cmd/1]).
-export([init/1]). % N.B. expose for spawn
-define(EXE, "/usr/bin/dc").
-define(BP, 300). % N.B. backpressure for `f' command
%%
%% api routines
%%
cmd(<<A/bitstring>>) ->
U = erlang:self(),
dc ! {call, U, A},
cmd_acc([]).
cmd_acc(As) ->
receive
{dc, quit} -> {quit, pretty(As)};
{dc, A} ->
cmd_acc([ A | As ]);
done ->
pretty(As)
end.
start() -> proc_lib:spawn(?MODULE, init, [?EXE]).
stop() -> dc ! stop.
init(A) ->
U = erlang:self(),
erlang:register(dc, U),
V = erlang:open_port({spawn, A}, [
eof, binary, {line, 255}]),
loop(V).
%%
%% business routines
%%
loop(A) ->
receive
{call, U, V} ->
W = erlang:self(),
A ! {W, {command, V}},
call_acc(U, A),
loop(A);
stop ->
U = erlang:self(),
A ! {U, close}, % N.B. `close' is first class
receive
{A, closed} -> % N.B. first class
erlang:exit(normal)
end
end.
call_acc(A, B) ->
receive
{B, eof} -> % N.B. call was to quit (`...q')
A ! {dc, quit},
erlang:exit(normal);
{B, {data, U}} -> % N.B. `data' is first class
A ! {dc, U},
call_acc(A, B)
after ?BP ->
A ! done
end.
%%
%% support routines
%%
pretty(As) -> pretty_acc(As, []).
pretty_acc([], Acc) -> Acc;
pretty_acc([ {eol, <<A/bitstring>>} | As ], Acc) ->
pretty_acc(As, [ A | Acc ]).
I like to open top
to make sure the OS process is
coming and going as expected:
$ top -U nato -g dc # using OpenBSD
With that, I erlc
and open an Erlang shell:
erl -eval 'dc:start()'
All fired up, I can see that the OS all a sudden has my ‘dc’ process. I can now go to work on my calculations in the Erlang shell:
1> dc:cmd(<<"3 5 9 +p">>). % 14
2> dc:cmd(<<"*p">>). % 42
Of course, the fun truly gets started when one starts to lean on ‘dc’ registers, recursion, etc. Here’s factorial one-thousand:
1> dc:cmd(<<"1000[d1-d1<U*]dsUxp">>).
And you thought Erlang had the world’s smallest fac/1
implementation!
When dealing with large numbers, the ‘dc’ program spits out lines for easier handling, so this module code accumulates this all into a list to be joined/converted into an integer/float where appropriate.
I’m tickled that I can now use ‘dc’ in my favorite programming language for tricky and odd one-off numerical tasks that might be pedantic to write in Erlang.