Dyalizer error on MapSet

Background

I am reading “Functional Web Development with Elixr, OTP, and Phoenix” and there is a code sample that is setting of an error with dialyzer:

def new(type, %Coordinate{} = upper_left) do
  with  [_|_] <- offsets = offsets(type),
    %MapSet{} <- coords = add_coordinates(offsets, upper_left)
  do
    {:ok, %Island{coordinates: coords, hit_coordinates: MapSet.new()}}
  end
end

Error

The error affects the following line of my with statement:

%MapSet{} <- coords = add_coordinates(offsets, upper_left)

lib/islands_engine/island.ex:17: The pattern #{‘struct’:=‘Elixir.MapSet’} can never match the type ‘Elixir.MapSet’ | {‘error’,‘invalid_coordinate’}

According to my understanding, this means I will never get a MapSet as as result, which is not true. How can I fix this?

MapSet.t/0/1 is opaque, you are not allowed to match on it.

Aside of this, it seems as if the @spec of add_coordinates/2 has some typo… MapSet instead of MapSet.t.

1 Like

So, using a with how can I make sure that what I am receiving is actually a MapSet ?

Also, this is my add_corrdinate/2 function:


@spec add_coordinates([tuple], %Coordinate{}) :: MapSet.t | {:error, :invalid_coordinate}
  defp add_coordinates(offsets, upper_left) do
    Enum.reduce_while(offsets, MapSet.new(), fn(offset, acc) ->
      add_coordinate(acc, upper_left, offset)
    end)
  end

When I have structs, I use %MyStruct{} in the @spec. Is this incorrect? How should I do it?

Kernel.defstruct/1:

defmodule User do
  defstruct name: "John", age: 25
  @type t :: %User{name: String.t, age: non_neg_integer}
end

So the type is User.t().

Aside: Typed Elixir structs without boilerplate

So, using a with how can I make sure that what I am receiving is actually a MapSet ?

The idiom isn’t

value | {:error, reason}

but

{:ok, value} | {:error, reason}

which sometimes degenerates to

{:ok, value} | :error

It’s :ok/:error results that gave rise to with.

Basically you can’t from the mapset itself. Just like the ets example in a previous thread of yours a mapset is opaque to tell you to not depend on the format of the data. To an extrem MapSet might be reimplemented to not be a struct anymore and it should not break your code. You need to trust that everything you receive from any of the functions in the MapSet module, which returns t will give you a valid MapSet. If you need to differenciate a valid result from something not valid you need to handle that without looking at the mapset shape.

2 Likes

An amazing reply, although quite confusing (to me). Not because you are not clear, but because the way I interpret it translates to: “the book goes against the idiom of the language it teaches”.

You can probably guess why I am confused, such a statement would be … problematic at best :stuck_out_tongue:

I just don’t understand why would the author deviate from the community guidelines. Maybe they didn’t exist back then? Or am I missing some edge case?


@LostKobrakai Thank you for confirmation ! I see now the way to go is to change the API of the underlying functions to be more idiomatic !

Truth be told neither of the style guides have an opinion about it. It’s just something that because of my exposure to Haskell that dawned on me. And as such not even the Erlang libraries are consistent about it.

value | null
value | :error
value | {:error, reason}

all have the same problem. A bare value has no constant reminder that failure is a possibility in the way the {:ok, value} does.

2 Likes

There are people out there not using dialyzer and while in theory a MapSet should be able to change to not being a struct it’s very unlikely. The convention of {:ok, value} | {:error, term} is quite common in erlang/elixir, but in both languages you also see people use value | {:error, term} so this isn’t fully unheard of as well. Given a struct as only possible valid result can be easily matched against the author might have not cared much for the difference.

You’ve opened a lot of threads on the forum, which resulted in you asking why authors didn’t do a thing or another differently like they did and I think you should give them a little bit of slack. Nobody is perfect and code examples in books always need to be evaluated in terms of supporting a certain point a book is trying to make at that section and less in terms of “this is perfect code to write”. I can understand that it’s not the best possible outcome in that you hit an issue with dialyzer, but on the other hand the code at hand does work and it probably will do so in the future. But dialyzer cannot evaluate how deeply you’re looking into an opaque type and how sever that violation actually is.

Also expecting that the content in books is perfect is expecting too much. Twenty years ago books were typically written about topics that moved at a comparatively glacial pace giving the author time to become an absolute expert before writing a book.

These days technical books are obsolete the moment they hit the presses. It doesn’t give the author much time to become competent in the topic and write the book.

I also would much prefer that all books reflect all best practices. But some best practices only make sense against some fairly complicated background - background that may get in the way of the initial understanding for neophytes.

In other cases the book may span so many topics that it would be difficult for any one author to be aware of all the best practices (and keeping in mind that even best practices require context). So in the full stack context you can run into authors that are more back-end or front-end oriented.

So reading (modern) books you have to accept that authors are human and treat everything with a healthy amount of skepticism and never rely on one single source to come to a conclusion.

I find that when I work through a book that I need to go on a lot of side quests on the web. Sometimes because a book’s description lacks clarity for me personally, other times because something seems off. In some cases it simply means that I don’t have the background necessary to understand why things are the way they are but sometimes things are off. And even then some differences are simply matters of opinion.

But in the case of MapSet, you can’t match it using a with and dialyzer, as this example shows. I understand this is not an issue for people who don’t use dialyzer or gradualizer or what_have_you_lyzer but given I have access to these tools I personally insist in extracting the benefits they offer and on understanding why they work they way they do. This post is me trying to understand that. If I hadn’t used dialyzer, I would have not learned about MapSet being an opaque type because the code was running perfectly fine and I didn’t have a tool to complain about code quality :smiley:

I don’t expect perfection from anyone, but I do enjoy finding out things that don’t match and exposing them. Part of the reason I love this community is that you guys have really helped me with the exposing part. I now inquire a lot more and I try to follow the overall advice @peerreynders gave me a few months ago.

I think I am making progress in my posts, at least I suspect the number of people wanting to kill me has been dropping :stuck_out_tongue: I still have a long way to go though.

Me asking “Why author Z Y X, did thing A instead of B” will always be something I do. I admire people who write books. I see them as being in another level. So when my idols do things I don’t understand or that just outright conflict with what I think is correct, I really do want to know why. It’s not about expecting perfection from them, it’s about understanding them so perhaps, one day in the future, I can too write a book myself or be recognized as somewhat good in what I do for a living :smiley:

I understand this is not an issue for people who don’t use dialyzer or gradualizer or what_have_you_lyzer

You have to understand that using Dialyzer impacts the way you write code - you are forced to become much more type aware the more heavily you use it. I find that many developers with a dynamic language background don’t typically get into “thinking in types”.

1 Like