What's the syntax for reusing a function @type in a @spec

TL;DR how do I reuse a functional @type inside a @spec?

I have become the maintainer of a module that performs scoring:


@spec score_feature_a(float, any) :: float
def score_feature_a(score, data), do: ...

@spec score_feature_b(float, any) :: float
def score_feature_b(score, data), do: ...

@spec score_feature_c(float, any) :: float
def score_feature_c(score, data), do: ...

end

I’d like to know how each score function contributed to the final score.
So I want to change the type being changed from a float to a list of tagged floats.
But I might changed the type again so ideally, I’d like to DRY it up to
reduce future typing.
So my thought was to encapsulate the function’s type using @type and reuse it in @spec.
The rereading the typespec documentation several times, I discovered
what I think is the correct syntax for the function @type:

defmodule Score do

@type scoring :: [{atom,float}]
@type scorer :: (scoring, any->scoring)

@spec ??
def score_feature_a(score, data), do: ...

...

end

But how can I reuse my scorer type in the @spec for score_feature_a ?

I scoured the docs, googled, SOed and asked on the Elixir slack to no avail.
Cheers,
Guy

1 Like

Hi @rugyoga

You don’t need the scorer type.
what you need is just

@type score :: [{atom, float}]
@type data :: any

@spec score_feature_a(score, data) :: score
def score_feature_a(score, data), do: ...
...
end

I think you have mixed up concepts… your scorer type what it does is represent an anonymous function of arity 2. Unless you are passing or returning an anonymous function you shouldn’t use that.
To spec regular functions you just do it the way I have shown in my exmaple above.

Note: be careful to include spaces after a comma and surrounding operators with a while space as in the -> case.

1 Like

And since your score matches de definition of a keyword list, you could express it like this.:
@type score :: keyword(float)

But I want the scorer type.
Because when I decide all my scoring functions will have a different signature,
I only want to change that in one place: @type scorer …
Are you saying i can’t reuse the scorer type within the @spec of my scoring functions?
That’s disappointing.

What does the scorer type represent? the return value of your functions?

The scorer type represents the type of my scoring functions:

(scoring, any->scoring)

i.e. takes a scoring and a data parameter and returns a new scoring

it takes a list of tuples where the first element of the tuple is an atom and the second one is a float, and returns a list of tuples?

If so, what you need is

@type scoring :: keyword(float)
@spec score_feature_a(scoring, data) :: scoring
def score_feature_a(score, data), do: ...

@spec score_feature_b(scoring, data) :: scoring
def score_feature_b(score, data), do: ...

that represents an anonymous function.

@spec foo :: (scoring, any->scoring)
def foo() do
  fn(score, data) ->
    # modify score based on data
    new_score
  end
end
1 Like

A concrete example would be:

  def score_feature_a(scoring, %{ "a" => items}) do
    [{:feature_a, 2.0 * length(items)} | scoring]
  end
  def score_feature_a(scoring, _), do: scoring

score_feature_a has type scorer.
How do I express that in the @spec (by reusing scorer) ?

Exactly as I have described it above.

I think you are a bit confused with what functions return.
Functions returns values, they do not necessarily need to return a function. The body of the spec describes the function, as we are already describing it here score_feature_a(scoring, data) so the return value (what comes after the ::) is only scoring,

A concrete example·

@spec double(integer) :: integer
def double(x), do: x * 2

While you are trying express it like this.

@spec double(integer) :: (integer -> integer)
def double(x), do: x * 2

I am not trying to express it like that.
That was an early attempt to illustrate what I wanted by what didn’t work.

I’m trying to figure out if there’s a way to reuse a functional @type in a function’s @spec.
I’m guessing it’s the quirk arising from the fact that Elixir has two ways to denote functions:

  • anonymous functions, e.g fn scoring, data -> data end
  • named functions, e.g. def score_feature_a(scoring, data), do: data
    and maybe they have subtly different types so maybe I can’t.
    The docs are very slim on functional types.
    There’s a mention in the grammar and that’s it.
    Someone else even posted complaining about it.

I’ve coded in many functional languages and even written a compiler or two and this
is the first language I’ve come across that distinguishes between those two use case.
In all the others,

f = fn a, b -> b end

would be equivalent to

def f(a, b), do b end

and they would have the same type.

So if I can’t use the type of the anonymous function not,
is there a way to encapsulate and reuse the named function’s type?
I’m beginning to think not…

1 Like

I see what you mean now. I don’t think that out of the box that is possible, at least with the specs defined through @spec and Dialyzer. Maybe you can look into alternatives to Dialyzer.

An option would be to create a library that will read a custom spec attribute, transform that compile time and convert it to regular spec. I think the TypeCheck library may do something like that. GitHub - Qqwy/elixir-type_check: TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. I haven’t read its source code.