Sync’ up! … without getting drained

mar 23

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.