[erlang-questions] Best practice for defining functions with edoc,erlc,eunit and the dialyzer
Zoltan Lajos Kis
kiszl@REDACTED
Wed Dec 2 11:31:20 CET 2009
Joe Armstrong wrote:
> 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
>
> ________________________________________________________________
> erlang-questions mailing list. See http://www.erlang.org/faq.html
> erlang-questions (at) erlang.org
>
>
One more thing that can be addressed is the order of exporting (and
defining) functions. My preference is to:
- 1, export API functions
- 2, export Behavior callback functions (a separate export line for each
behavior)
- 3, export internal functions. The functions that you _don't want to
export_, but Erlang makes you to do so in order to use them in spawns,
applys, etc.
Something like:
%% API
-export([start_link/0, update/0, get/1, put/2, ...]).
%% Behavior callbacks
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, ...]).
%% Internal functions
-export([spawnee/0, applyee/2, ...]).
I also like to implement the functions in the same order as they appear
in the export list.
In fact I also prefer to have my exported functions first, and have the
internal functions below. Test code follows at the end of the module.
The style of having internal functions directly below the exported ones
looks nice until you encounter a bunch of internal functions that are
used by multiple externals.
For me it usually leads to a guess-game of where it was defined...
Maybe there are things that does not need to be fixed :)
Regards,
Zoltan.
More information about the erlang-questions
mailing list