Referencing a function's return type in another function's @spec

Greetings everyone.

I searched everywhere for this but couldn’t quite find what I’m looking for, which is a way to reference a function’s return type in another function’s @spec. Allow me to demonstrate:

Below is a representation of what I’d like to do: to capture the fun type’s return_type property (which I wrote as return_type for this example)

defmodule MyStruct do
    defstruct [:data]
    @type t(member) :: %__MODULE__{
        data: [member]
    }
end

defmodule MyFunctions do
    @spec transform(list, fun) :: MyStruct.t(fun.return_type)
    def transform(raw_content, map_fn), do: %MyStruct{data: Enum.map(raw_content, map_fn)}
end

The snippet above would state that the return type of transform is a %MyStruct{} struct whose data field’s type is an array produced by the output of the second argument, map_fn.

Although I can solve this by using the struct() type instead of fun.return_type, I’d like to be as precise as possible with these types.

Appreciate any feedback. Ty!

1 Like

The spec for List.foldl is similar to what you’re looking for:

or the spec (mind the Erlang typespec notation) of :lists.map:

-spec map(Fun, List1) -> List2 when
      Fun :: fun((A) -> B),
      List1 :: [A],
      List2 :: [B],
      A :: term(),
      B :: term().

In both specs, the return type is extracted via pattern-matching.

4 Likes

I would write it like this: (this is as specific as I could made it)

defmodule MyStruct do
  @type member :: any()
  @type t(member) :: %__MODULE__{
          data: [member]
        }
  defstruct [:data]
end

defmodule MyFunctions do
  @spec transform(list, (Enum.element() -> member)) :: MyStruct.t(member)
        when member: MyStruct.member()
  def transform(raw_content, map_fn) when is_function(map_fn, 1),
    do: %MyStruct{data: Enum.map(raw_content, map_fn)}
end

and you can replace Enum.element() and any() to whatever suits your needs better.

Note the addition of is_function(map_fn, 1) guard in the function.

3 Likes

Sadly @type’ s do not accept guards, otherwise we could make it even more specific: adding when member: member() to MyStruct.t/1

Thanks for the input @al2o3cr and @eksperimental!

I ended up using a simple struct as the typespec. My main wish was to have a generic statement to save me from having to add function guards for each type supported by that function. If they were two or three, it would be ok, but for a dozen types I’d be writing spaghetti guard statements.

Nonetheless, I made some elegant changes by plugging guards into the code. Thank you again! :heart:

1 Like