Are inverse typespecs possible?

I have a function that can take any term as 2nd argument - except integers.

So currently I have a guard on the function when not is_integer(term) (and that will stay anyway), but I wondered if it was somehow possible to have an inverse typespec for clarity, like

@spec f(not(integer())) :: integer()
# (CompileError) ...: type not/1 undefined
1 Like

No, it is not possible. Negative types are hard in general.


It’s possible to generate @spec with macro. However the generated @spec would be like: use a, b and c rather than use everything other than d. This means that in such macro you would need to remove argument value from predefined list of all expected cases. Therefore it’s not recommended.

Let’s say that we are working with just a strings:

defmodule Example do
  # short for: ["a", "b", "c", "d"]
  @all_cases ~w(a b c)

  def sample(value), do: List.delete(@all_cases, "d")

iex> Example.sample("d")
["a", "b", "c"]

If you got this then think that for @spec you would need to work on AST. Hope you see that’s not worth for such small thing.

If you really want to do it anyway then here you can learn something about AST and macros:

Important: At the end please keep in mind that your documentation would be extremely hard to read due to long @spec values!

1 Like

But how would you extract all types using the Macro? Which types the Macro would see would depend on when it is compiled in the build process, I don’t see how that would work.

I think that’s the crux - the idea is to make docs more expressive, not less.

I agree, especially with a dynamic language at compile time, though it’s not impossible, especially since typespecs are compile-time only anyway.

I’d have to add that I don’t have high hopes of a positive answer, I just wanted to ask before giving up entirely.

1 Like

Macro would not “see” anything. You just need to generate something like:

is_nil(term) or is_list(term) or …

In AST representation it looks like:

# single call:
{:is_nil, [], [term])
# "or" call:
{:or, [], [x, y]
# "or" with two items:
{:or, [], [{:is_nil, [], [term]}, {:is_list, [], [term]}]}
# nested "or" is in the first argument:
{:or, [], [{:or, [], [x, y]}, z]}

You can preview AST using quote/2 and preview generated code using Macro.to_string/2

Well … this is generally 100% right, but in Elixir only ingenuity (and of course implementation time) limits you. For example nothing stops you from forking ex_doc and check for optional extra attribute:

defmodule Example do
  Module.register_attribute(__MODULE__, :special_spec, persist: true)
  @special_spec quote do: not is_integer(term)
  @spec unquote(MyApp.spec_macro(@special_spec))

In such way you can keep ex_doc output clean and have valid @spec (for example for dialyzer). If I remember correctly hex uploads .html files, so there is no need to worry about forking ex_doc if you really want to.

As said generally it’s just more work and in typical case it’s simply not worth. However if you for any reason really want to have such thing then nothing stops you. As long as your code stays clean and you would follow good practices then nobody would complain.

So if you ask if you can do something like inverse typespec then answer is yes. However how it would look like depends on how much time and effort you are able to spend. You can just generate @spec and it would be 100% valid (dialyzer should not complain), but it would not look good in ex_doc. You can fork ex_doc and support extra module attribute, but it would take some extra time. You can …, but keep in mind that … etc.

Also if you have a good idea for implementing something in Elixir feel free to propose it here or on mailing list. You just keep in mind that Elixir core follows some rules. If you propose something which does not conflict with them it would be accepted. Like @hauleth said some things are hard in general. If you however find a good implementation solution then community is open to your idea.

As I have already said in another thread. Programmers don’t think like computers i.e. only 0 and 1 cases. Of course we have good and bad practices, but everything else is up to us. Look at documentation, check what you need to do (here specific AST) and in which way you can provide it (here macro). As long as code would be clean (I recommend credo here) it’s definitely ok. Only you have a right to limit yourself.

It’s why I said that’s generally possible, but also I don’t recommend it. At end it’s your decision what and how much you want to do. Compare cons with pros and choose your preferred way. It’s nothing bad if it’s generally not worth, but you would implement it anyway. Even if it would be a coding challenge as long as you have fun with programming there is nothing wrong about it.

I don’t quite know what to make of your response, so maybe some clarifications:

I am / was looking for a possible solution within the system and don’t want to fork ExDoc, Elixir nor Erlang

This is also not a proposal for inverse typespecs in Elixir, but just a question to find out whether it was possible to have them in the current implementation, maybe using macros or special side-effects of typespecs that are not obvious to me or that I have simply overlooked. The pseudo-code was simply to illustrate clearly what I want to achieve.

Manually maintaining a list of all possible types is not a solution for me.

Well … In exactly this question macro does not look so go, but while you should use macros only when required you will sooner or later use them.

Yeah, forking a library does not looks best especially if you would say it to beginner. Many would ask why this was not implemented before, but you also need to remember that there is no rule to cover all edge cases. Again sooner or later you will be in case in which you will need to fork something or create some library on yourself.

Same for me, but I just said that’s possible in some way. Sometimes you could be in case when implementing something may be useful, but also would take too much effort to make it worth for most developers. Look that most of people in the world are not rich and lots of rich people bough something which others don’t even think about. Same thing goes to development. A good example may be an UI library which could be a replacement for GTK+ or Qt. It’s definitely not worth for single devloper, but may be extremely useful in future. Big corporations are spending huge amount of money to force something and they could still have loses month by month.

Just some people would go their way and it does not must mean that it’s a bad way. Here comes my answer. As a developer I’m really focused on looking for possible edge-cases. I said that’s somehow possible, but for typical use case I don’t recommend it and your reply makes sense for me, but in case I would not highlight “don’t recommend” part properly. Everything goes to you i.e. how far you are able to go.

After some time I thought about it and I found that part of my answer does not makes sense. I somehow forgot about @type and @typedoc which are enough for making even most complicated types readable, so using them there is no need to fork anything.

defmodule Example.TypespecWorkarounds do
  @typedoc "Workaround for not(type) by simply defining all other types."
  @type not_integer :: atom | String.t | …
  # …

defmodule Example do
  alias __MODULE__.Workarounds

  @spec sample(Workarounds.not_integer()) :: …
  def sample(not_integer) when is_not(integer) do
    # …

This looks enough clean for me. Again instead of declaring every not_* type you could create a macro to generate all of them. This way it would be also readable and does not need so much effort.

I know that this is not an answer you are looking for, but it’s most closest and offer what you need (at least it should be enough for dialyzer and it’s enough clear for other developers too.

Again if there is no answer for your question look if some solution could offer what you need. Don’t give up too easily. Maybe Elixir does not handle not(type), but it does not stops you for declare it in some way.

1 Like

That’s the way to go @Eiji

@type not_integer() :: atom() | bitstring() | float() | fun() | identifier() | map() | maybe_improper_list() | tuple()

The problem is that this list is not comprehensive, it only covers built in types. There are an infinite number of possible other types because the typespec system allows the creation of opaque types.