Best practice for defining functions with edoc,erlc,eunit and the dialyzer

Joe Armstrong erlang@REDACTED
Wed Dec 2 10:28:58 CET 2009


Best practice for writing documenting and testing code

I'd like to try and define "best practice" for writing documenting and
testing Erlang code. I want to use:

    - only the tools supplied in the OTP release

So I use:

    - eunit for unit testing
    - the dialyzer for checking my code
    - edoc for documenting things
    - type specifications for specifying types

These tools do not completely "play together" in a satisfactory manner,
so I'd like to define what I thing is "best practice" and hope that by doing
so the tools will converge.

Let's suppose I want to define the good 'ol factorial. Here's a module
called elib1_best.erl. I've written it in such a way that it can be
processed by erlc,eunit,edoc and the dialyzer - read the footnotes
in brackets for an explanation.

       -module(elib1_best). %% [1]

       %% elib1_best: Best practice template for library modules [2]
       %% Time-stamp: <2009-12-02 09:43:12 ejoearm> [3]

       %%----------------------------------------------------------------------
       %% Copyright (c) 2009 Joe Armstrong <erlang@REDACTED> [4]
       %% Copyright (c) 2009 Whoomph Software AB
       %%
       %% Permission is hereby granted, free of charge, to any person
       %% obtaining a copy of this software and associated documentation
       %% files (the "Software"), to deal in the Software without
       %% restriction, including without limitation the rights to use, copy,
       %% modify, merge, publish, distribute, sublicense, and/or sell copies
       %% of the Software, and to permit persons to whom the Software is
       %% furnished to do so, subject to the following conditions:
       %%
       %% The above copyright notice and this permission notice shall be
       %% included in all copies or substantial portions of the Software.
       %%
       %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
       %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
       %% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
       %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
       %% BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
       %% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
       %% CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
       %% SOFTWARE.
       %%-----------------------------------------------------------------------

       -include_lib("eunit/include/eunit.hrl"). %% [5]

       -export([fac/1]).  %% [6]


       %% @doc fac(N) computes factorial(N) using a fast
       %% iterative algorithm. [7]

       -spec fac(integer()) -> integer(). [8]

       fac(N) when is_integer(N), N >= 0 -> fac1(N, 1).

       fac1(0, K) -> K;
       fac1(N, K) -> fac1(N-1, K*N).

       fac_test() ->  %% [9]
           6  = fac(3),
	       24 = fac(4).

	       %% Notes:
	       %% [1] - module on line 1
	       %% [2] - module comment
	       %% [3] - Time stamp auto generated by emacs.
               %%       Must be near start of file
	       %% [4] - Copyright (I always forget this, but adding a
               %%       copyright reduces the pain later
	       %% [5] - Needed for eunit
	       %% [6] - use export and NOT compile(export_all)
	       %% [7] - @doc comes first
	       %% [8] - -spec comes immediately *before* the function
	       %% [9] - test cases come immediately after the function

	       %% end of module
...

Now let's see what happens:

    1) Compiling

    erlc  +warn_missing_spec -o ../ebin -W elib1_best.erl

    ./elib1_best.erl:0: Warning: missing specification for function test/0
    ./elib1_best.erl:44: Warning: missing specification for function fac_test/0

Best practice is to support type specifications for all exported
functions.  But eunit magically adds a function test/0 and I really
don't want to have to add manual exports and type specs for
fac_test/0.

    [A fix is needed to erlc here, OR eunit can add type specs,
     I think the latter is better - erlc should not need to know about eunit]

   2) Dialyzing

dialyzer --src elib1_best.erl
  Checking whether the PLT /home/ejoearm/.dialyzer_plt is up-to-date... yes
  Proceeding with analysis...
Unknown functions:
  eunit:test/1
 done in 0m0.32s
done (passed successfully)

    This is ok - I could add eunit to my plt if I wanted ...
    the dialyzer warns for missing functions so I don't need to run
    xref

   3) Documentation

    I'll run edoc on everything in the current directory putting the
results in ../doc

      > erl -noshell -eval "edoc:application(lib, \".\", [{dir,\"../doc\"}])" \
  	             -s init stop

    This works fine and ../doc/elib1_best.html has the documentation
    but now edoc has not found my nice -spec declaration and thinks that
    fac has type: fac(N) -> any()

    Why: because edoc and erlc don't use the same type parser.

    Current best practice is to use -spec (in code) and
    not @spec (in edoc comments)

     [A fix is needed here to edoc, to understand -spec's]

4) Testing

   1> eunit:test(elib1_best, [verbose]).
   ======================== EUnit ========================
   elib1_best: fac_test (module 'elib1_best')...ok
   =======================================================
   Test passed.
   ok

   Great ...

Now for questions.

    1) Does this represent best practice? Is this the best way to
       write code? - can anybody improve on this?

       [And yes I know about quickcheck, but I'm only concerned
        with SW in the OTP release]

    2) If I write like this can I assume that one day edoc
       and eunit and erlc will converge so that I get correctly displayed
       types in edoc and no warnings in erlc etc?

    3) Does anything else have to be fixed?

    4) Improvements..

       I can think of one. I have some code to convert .erl to
       .html with correctly colored code and hyperlinks etc.
       So I can "surf" the code. It would be nice to have hooks
       into edoc so I can call this code

That's all for now ...

/Joe


More information about the erlang-questions mailing list