James_E

James_E

Collapsing/compressing redundant typespecs?

I’m writing a multiset library. It’s got 2 modules: one which works on raw “multisets” (which are maps of elements to quantities), and another which is just a Struct type wrapping the same functionality in much nicer ergonomics.

For the former module, all the methods support so-called “lax” multisets, where some element => 0 records may be included; but many methods include a strict \\ :lax :: :lax | :strict parameter that can be used to switch on better codepaths whenever the caller can guarantee the multiset isn’t “lax”.

I’m trying to write the type system to support that, so that Dialyzer will be helpful to any consumers. However, the type signature for one method, delete, has turned out suspiciously verbose; I’m wondering if there’s any way to shore this up with some syntax that more clearly semantically indicates “this type in the output is the same as this type in the input, and said type must in any case fit some constraint, and you should try fitting the allowed types in this particular priority order”, without this combinatorial explosion:

@type t(value) :: %{optional(value) => pos_integer()}
@type t() :: t(term)

@type t_lax(value) :: %{optional(value) => non_neg_integer()}
@type t_lax() :: t_lax(term)

@type t0(value) :: Enumerable.t({value, non_neg_integer()})
@type t0() :: t0(term)

# …

@spec delete(t(e), term) :: t(e) when e: term # non-lax output is guaranteed from non-lax input
@spec delete(t_lax(e), term) :: t_lax(e) when e: term
@spec delete(t(e), term, :all) :: t(e) when e: term # non-lax output is guaranteed from non-lax input
@spec delete(t_lax(e), term, :all) :: t_lax(e) when e: term
@spec delete(t(e), term, non_neg_integer()) :: t(e) when e: term # non-lax output is guaranteed from non-lax input
@spec delete(t_lax(e), term, non_neg_integer()) :: t_lax(e) when e: term
def delete(ms, element, count)

def delete(ms, element, :all), do: Map.delete(ms, element)

def delete(ms, element, count) when is_pos_integer(count) do
  case ms do
    %{^element => n1} ->
      n2 = n1 - count

      if n2 > 0 do
        %{ms | element => n2}
      else
        Map.delete(ms, element)
      end

    %{} ->
      ms
  end
end

def delete(ms, _, 0), do: ms

Most Liked

LostKobrakai

LostKobrakai

@spec delete(mset, term) :: mset when mset: t(term) | t_lax(term)

The specs you wrote would be merged to the following anyways:

@spec delete(t(e) | t_lax(e), term) :: t(e) | t_lax(e) when e: term

LostKobrakai

LostKobrakai

No. Multiple specs for a single function(/arity) will be merged where all the individual parameters and the return type are put into union. Only if you do the @spec fun(x) :: x when x: term notation do you “hint” – not sure how hard a hint – that input and output are of the same type

LostKobrakai

LostKobrakai

The hexdocs page describes the typespec syntax, not dialyzer – the type checker using the specs. These are really separate things. But I’m not aware of a comprehensive suite of docs around how dialyzer works. I essentially treat is as best effort checking, because really that’s what it does. E.g. it will switch out granular types, when getting bigger in complexity, for less granular types when checking. In the end the only guarantee dialyzer makes is that there is an issue (even if just with the typespecs) if it reports an issue. Exhaustiveness in finding issues was never a goal of dialyzer.

As for your example I don’t think there is a better way to write this given the distinct type parameters used between input and return value.

Last Post!

al2o3cr

al2o3cr

FWIW, distinguishing situations like:

@spec function(integer) :: atom
@spec function(atom) :: integer

and:

@spec function(integer) :: integer
@spec function(atom) :: atom

(both of which Dialyzer collapses to @spec function(integer | atom) :: integer | atom) is a key motivation for the set-theoretic type work:

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New

Other popular topics Top

jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement