mrkurt

mrkurt

Closest thing to static type checking?

I’m reading through the type annotations features and trying to figure out how to enforce type checking in a Phoenix app. I’m pretty sure it has something to do with the dialyzer. :wink:

Given a project with functions like this, how can I type check it before releasing? The VS Code extension gives me convenient yellow squigglies when my annotations don’t map the derived types. It’s a start, but I want it to be more strict.

  @type presence_entry :: {any(), %{metas: list(%{ atom() => any() })}}
  @spec presence_by_region(list(presence_entry)) :: %{any() =>  non_neg_integer()}
  def presence_by_region(presence) do
    result = presence
              |> Enum.map(&(elem(&1,1)))
              |> Enum.flat_map(&Map.get(&1, :metas))
              |> Enum.filter(&Map.has_key?(&1, :region))
              |> Enum.group_by(&Map.get(&1, :region))
              |> Enum.sort_by(&(elem(&1, 0)))
              |> Map.new(fn {k,v}-> {k, length(v) } end)

    result
  end

Most Liked

ityonemo

ityonemo

I’ve taken a bit of a break from it (life is busy since I’m moving cities and I am architecting code at work) but I’m working on a compile-time static type checker for elixir. It should be more powerful/useful than dialyzer since it takes some stances on what types mean and it will have type literals and subtraction types.

It’s nowhere near there yet though, ha.

Adzz

Adzz

The thing to keep in mind is that fundamentally you can’t. Elixir is not typed, if you come from another typed language you should let go of that expectation or you will be disappointed.

HOWEVER, there are tools to help solve similar problems. Dialyzer is one, but it is not a static type checker, it does success typing. It’s important to know the difference, its limitations and what it aims to do.

Similarly, there are libraries like Norm and Hammox that can give you extra safety if you need it but again they approach the problem differently.

Finally if you want a statically typed language on the BEAM check out Gleam or Caramel.

axelson

axelson

Scenic Core Team

The general starting point would be to add dialyxir to your project:

https://github.com/jeremyjh/dialyxir/

That will give you a mix dialyzer task that you can use for CI to fail the build. Although if you don’t start this at the beginning of a project it can be quite difficult to add later because it can be very difficult to understand all the dialyzer errors, dialyzer is rather hamstrung since it has to rely on success typing.

Last Post!

mattludwigs

mattludwigs

I think maybe the spec could be:

# I am not sure what that first item is in the tuple but if it is an id I might
# make a type like: @type id() :: non_neg_integer() | String.t() assuming that
# maybe the id could be either of those types then I would replace the any() with
# id() in the presence_entry() type. 
@type presence_entry() :: {any(), Phoenix.Presence.presence()}

# I am going to make a new type here for the region, but I am making assumptions as I don't know the rest of the code and the type could be something else.
@type region() :: String.t()

@spec presence_by_region([presence_entry()]) :: %{region() => non_neg_integer()}

Dialyzer always assumes the most generic types possible, so since the code provided does not explicitly call anything on the any() it just assumes this can be anything, as the function will work if you pass anything into it for that parameter. I have found over the years that the more explicit you can be in telling Diazlyer your intentions the better it will type check across the codebase. That is why I recommend making a new type for the any()s - region() and id().

While Dialyzer isn’t very strict by default you can try to add a few flags to your Dialyzer configuration and just be explicit as possible. Plus I have found that being super explicit in types helps provide documentation on how the code is intended to work.

Another thing I might consider with this code is turning the presence entry into a struct and providing some function to make a Phoenix presence into this more known data structure (please note I am making a few assumptions here around naming, field, and types).

defmodule PresenceUser do

  @type id() :: non_neg_integer() | String.t()

  @type region() :: String.t()

  @type t() :: %__MODULE__{
          id: id(),
          region: region() | nil
        }

  defstruct id: nil, region: nil

  @spec from_presence_entry(presence_entry()) :: t()
  def from_presence_entry({id, presence}) do
    region = get_in(presence, [:metas, :region])
    %__MODULE__{id: id, region: region}
  end
end

## then the new spec for presence_by_region/1

@spec presence_by_region([PresenceUser.t()]) :: %{PresenceUser.region() => non_neg_integer()}

The exact names and types might be different depending on your codebase. I might even move the from_presence_entry/1 up a level in the API.

The reason I would do something like this is that not only is the code explicit to the reader, which is nice, it tells Dialyzer a lot of information, which as the codebase grows will help make Dialyzer seem more strict. However, I know that the community has various degrees of opinions on structs and Dialyzer. While this way of writing typed Elixir requires more typing I found it very useful over the years.

Here’s an example of the Dialyzer flags we set on most of our projects: vintage_net_mobile/mix.exs at main · nerves-networking/vintage_net_mobile · GitHub

Where Next?

Popular in Questions Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
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
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36820 110
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
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
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
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