Combining function spec and its header. I feel like there must be some reason behind it (other than “specs are latest add-ons that aren’t a part of original Erlang”).
What could that be? And what would it take to allow combined specs/headers?
Well… Not to say that it would make it impossible, but multi-claused functions or functions with complex patterns instead of just variables as arguments can make this kind of thing a bit… less clear (?)
For instance…
leave( _ :: pid() | [pid()], Group :: group() ) when map_size(Group) == 0 -> ok.
…;
leave( Pid :: pid(), #{Pid := _} :: group() ) -> ok.
…;
leave( [Pid | Pids] :: [pid()], #{Pid := _} :: group() ) -> ok.
…;
…
I’m pretty sure this is the exact reason. Combined with pattern matching and multiple clauses this gets very messy.
To enable something like this, IMO, we’d need to forgo multi-head functions likely in favour of in-line case, something similar to OCaml function - though there you can conveniently match only on the last argument.
Either way, this would have been a significant syntax change that probably would touch more than just this.
It also has a possible downside of mixing the specification of a function with its implementation, at least conceptually, but maybe even practically (if you move a clause around, for example).
I would actually prefer to skip the -spec funName bit:
- Group :: group(), PidOrPids :: pid() | [pid()] -> ok.
leave(Group, PidOrPids) ->
leave(?DEFAULT_SCOPE, Group, PidOrPids).
In this case, it means it is the spec for the next to be defined function.
I used - as a prefix but it could be anything else. But I do think it would remove some of the repetition (specially with multiple clauses were you already end-up repeating the function name).
I’m with @michalmuskala and @starbelly that this is bound to make things very messy. It adds a lot of chatter to function heads that is in the way of reading.
Also, in my view a spec serves two purposes.
One is type checking, but this is done with dialyzer, a different tool. The specs (IMO) have nothing to do with the language itself, that is, even if there are no specs or the actual code totally violates the spec, it will happily compile without any complaints. For me, this is clear a reason not to sprinkle specs into actual code.
The other purpose of specs, at least in my view, is to provide some sort of description of the function in terms of what types it expects as arguments and what it returns, without reading any of the code. With specs clearly separated from the code, this is easy, it’s all in one place. But with specs mixed into the function heads, I will have to examine all of its clauses and put it all together in my head.
With things like that, resolving the semantics of what this actually means is a bigger issue. AFAIK right now for both Dialyzer and EqWAlizer this is exactly the same as writing:
-spec f(integer(), number()) -> number().
The extra variables and when is mostly “decorative”