3

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.

Ufuk Hacıoğulları
  • 37,978
  • 12
  • 114
  • 156

3 Answers3

5

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?

zxq9
  • 13,020
  • 1
  • 43
  • 60
  • Thanks for this great answer @zxq9. It make the spec error all clear. Could you also suggest me how I can define a function signature (takes 2 parameters and returns one of those 3 values) so I can reuse it in other parts of the code? Is that possible or common in Erlang code? – Ufuk Hacıoğulları Sep 25 '17 at 23:07
  • @UfukHacıoğulları Speccing is per function and is not reusable. The place you will have similar (but not exactly the same) spec occurring in code is when you have an interface function on a gen_server on the top (that makes, say, a `gen_server:call/2`, and the implementation of a handler for that call internally will usually have similar, but not exactly the same spec. If you have a common API several modules need to match, then you should define a behavior that applies to them all. Otherwise the same spec recurring in code many times is a sign that you probably could refactor. – zxq9 Sep 26 '17 at 05:36
  • 2
    @UfukHacıoğulları This can become quite a long discussion and mods will remove it from comments. If you want to talk through an example or your specific case where you find recurring specs come to the [Erlang chat room](https://chat.stackoverflow.com/rooms/75358/erlang-otp). – zxq9 Sep 26 '17 at 05:39
4

-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')
Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • Type definition is exactly what I was looking for. I tried it too but apparently got the syntax wrong. Then I thought only specs work for functions. Thanks to you and @zxq9 for these great answers. – Ufuk Hacıoğulları Sep 26 '17 at 09:39
1

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.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487