Writing Specs - what is the best form?

Hi There,

I am occasionally writing specs for functionality and I’ve noticed that there are - at least - two forms to writing them, using when (btw why when and not where?):

-spec obtain_columns(
        AllRows,
        FirstRowIsHeaderRow
) -> Result when AllRows :: list(),
                 FirstRowIsHeaderRow :: boolean() | <<>>,
                 Result :: {list(), [list()]}.

the second form, which I prefer because there is less repetition:

-spec obtain_columns(
        AllRows :: list(),
        FirstRowIsHeaderRow :: boolean() | <<>>
) -> Result :: {list(), [list()]}.

I’ve noticed that the OTP documentation uses the first form - is there a reason for that?

Is there a preference towards one over the other? Does it matter or can I choose :wink: I.e. am I over thinking this!

Is mixing these forms in the same codebase a good idea - probably not but just wondering and also I want to be consistent.

1 Like

The when format looks much better in edoc.

1 Like

Isn’t there a way to convert format 1 to format 2 when generating documentation? Or better said, is it possible to add a step into the edoc process that does this conversion?

Format 2 - for me - has unnecessary duplication of parameter names and also when does not fly with me (perhaps my slight autistic nature) - I want to write where not when!

2 Likes

Personally, I prefer a third style…

-nominal value() :: string() | number() | …. %% Whatever you accept there
-nominal row() :: [value()].
-nominal table() :: [row()].
-nominal column_name() :: string().
-nominal headers() :: [column_name()].

-export_type([value/0, row/0, table/0, column_name/0, headers/0]).

-spec obtain_columns(table(), boolean()) -> {headers(), table()}.
obtain_columns(Table, FirstRowIsHeaderRow) -> …

In other words, my choice would be to define (and export) actual types for the things I use in the function specs and leave variables for the code.

5 Likes

Would you include the -nominal part in the erlang .erl file or would you outsource that to a header .hrl file?

As I see it, you are basically defining a set of types that can be reused elsewhere in other specs right?

I think you have already answered my question :slight_smile:

Definitely it seems to be an approach that has better reuse and less repetition, thanks for pointing me in that direction :+1:

1 Like

Nominals and opaques must be placed in the .erl file with -export_type. Putting it in .hrl will lead to multiple different and incompatible nominals being defined as the current module name is significant.

4 Likes

Certainly :wink:

In other words, what @jhogberg said :slight_smile:

1 Like

This should be added to the documentation.

BTW: are nominals opaque?

That’s a great list, thanks for that!

And:

-type attributes in header files should be avoided.

I just did some reading and came across this:

Assuming that these types are exported from module 'mod', you can refer to them from other modules using remote type expressions like the following:

mod:my_struct_type()
mod:orddict(atom(), term())

So if I defined a single module - something like -module(project_types) - then I could define all my types there and reference them all over my specs.

Is that something that is done in Erlang projects?

The larger aim - for me - is not to get bogged down in typing when refactoring code, i.e., I want the advantages of “strict typing” but still being able to refactor code easily … if that makes sense.

I really like the fact that the compiler does not enforce types but I see the advantage of knowing that function signatures match usage.

EDIT: Sorry, RTFMing a little further - elvis_core/doc_rules/elvis_style/export_used_types.md at main · inaka/elvis_core · GitHub - exporting types from modules is what I should be doing instead of separating them out into their own modules. Got it!

Yes, but usually it shares the name with your app.

So, if you are building an app to… let’s say… :thinking: …I don’t know… :roll_eyes: …maybe… play 4-in-a-row

Let’s say you call your app fiar, for instance. Then you may want to have a facade module called fiar that exposes the app main API, with functions like start, stop, etc… In that case, you would put your types in that module and you would export them (unlinke what those links show :face_with_peeking_eye:)… So you can later use them from the outside as fiar:match() or fiar:user().

FWIW, you should also check this talk:

It’s somewhat related.

Yep, in my case that would have been ered_types but I’ll start with exporting my types first.

Ah, I like this:

-type match() :: fiar_match:id().
-type user() :: fiar_user_repo:user().
-type player() :: fiar_match:player().
-type username() :: fiar_user:username().
-type pass() :: fiar_user:pass().

Creating module types via a form of “type aliasing”.

Going off-topic: I like what you did with fiar:

For a procedural learning, fiar is divided into small and separate issues, grouped by iterations.

Iteration 1: learn the basic estructure of an erlang application, tests, process as a gen_server, and supervisor with the simple_one_for_one strategy.

Iteracion 2: create the fiar application module, README.md and conecting to MySQL with sumo_db.

Iteration 3: Provide a RESTful API to let users play, adding a basic authentication.

Iteration 4: Add SSE support and create a basic website with standard SSE support to let users play the game using already existing RESTful API.

I’m trying to promote breadboard programming which is kind of the same (iterating over code) but done visually. So that you start with one solution and then copy it and modify it to make it work better and then you repeat that, each time refining the solution but you keep the old versions around. Something like you would do when working with a breadboard.

The whole idea is based on Erlang-Red, hence ered_ :wink:

1 Like

(1) FWIW, my company uses the first form. We use the white space the same way as OTP (I checked the lists module):

-spec obtain_columns(AllRows, FirstRowIsHeaderRow) -> Result when
      AllRows :: list(),
      FirstRowIsHeaderRow :: boolean() | <<>>,
      Result :: {list(), [list()]}.

I wasn’t a fan of this form at first, but soon I started to feel that it’s nice and clean.

(2) I assume that type specs use when (as opposed to where) because when was already a keyword when type specs were added.

1 Like