Cannot create a spec in Erlang

Viewed 455

I'm trying to create a really simple spec in an Erlang module but I get this error.

spec for undefined function compare/2

Here's my code:

-module(spec_example).
-spec compare(any(), any()) -> less | equal | greater.

-record(heap_node, { item :: any(),
                     children :: [#heap_node{}] }).

-record(priority_queue, { root :: #heap_node{} | nil,
                          comparer :: compare() }).

I can't define a compare function here because it will be provided as a parameter from outside. I've found similar examples in GitHub and I guess they all work fine.

I've tried this both in a module and a header file but the error is the same. I must be missing something really basic.

3 Answers

The example you cite is a header file (.hrl), not a source file (.erl) and they are importing it at the top of files that should be behaving a certain way.

This seems to be a way of enforcing that a handful of modules all behave the same way. Which puzzles me, because this kind of behavioral definition is exactly what behaviors exist to handle.

Also, note that their api.hrl also imports wf.hrl and then these are imported all over the place in the project. From what I can tell glancing at that code there really is no reason for that to not have been defined as a behavior. (There may have been a reason, but it is not obvious from glancing at the code... and I can't think of a good reason to avoid using behaviors for this case.)

Function specs

When defining a function spec you need to have an underlying function to annotate with it. So this, all by itself, is illegal:

-spec add(integer(), integer()) -> integer().

But this is legal:

-spec add(integer(), integer()) -> integer().

add(A, B) ->
    A + B.

An example of using types, specs, edoc, etc.: https://github.com/zxq9/zuuid

Defining Behaviors

So "What's a behavior?" Let's say you want to have a place in your program where you dynamically choose what module to call out to, and you must expect that module to behave the same way other, similarly defined ones do. If you need to call foo:frob/2 it should work the exact same way (in terms of types) as bar:frob/2. So we would write a behavior definition somewhere, inside foo_bar.erl maybe, and then declare our modules foo.erl and bar.erl to be behaviors of the type foo_bar:

-module(foo_bar).

-callback frob(integer(), inet:socket()) -> ok.

And then later...

-module(foo).
-behavior(foo_bar).

frob(A, B) ->
    % ...

If module foo lacks a function frob/2 of the specified type, then a warning will be thrown because it is failing to match the behavior it has declared.

This can be super useful for simplifying the way you check, manage, and keep organized inter-module API compatibility. Also, note that you can define as many callbacks as you want on a module, and you can declare as many behaviors as you want in a module.

See: How to create and use a custom Erlang behavior?

-spec can only type top level functions that are declared in the current module, not anonymous functions that are passed as arguments to the top level functions or stored in records. For those, you can use -type with the fun(X) -> Y syntax:

-type compare() :: fun((any(), any()) -> less | equal | greater).

-record(heap_node, { item :: any(),
                     children :: [#heap_node{}] }).

-record(priority_queue, { root :: #heap_node{} | nil,
                          comparer :: compare() }).

If I now create a priority_queue with a comparer being a one arity function:

main() ->
  #priority_queue{root = nil, comparer = fun(_) -> equal end}.

and run Dialyzer, I get:

Record construction #priority_queue{root::'nil',comparer::fun((_) -> 'equal')} violates the declared type of field comparer::fun((_,_) -> 'equal' | 'greater' | 'less')

I can't define a compare function here because it will be provided as a parameter from outside.

For this case you need -type, as Dogbert mentions in the comments:

-type compare() :: fun((any(), any()) -> less | equal | greater).

-record(priority_queue, { root :: #heap_node{} | nil,
                          comparer :: compare() }).

Because you want to describe the type of a value, not the specification of a function named compare in your module.

Related