Fl4m3Ph03n1x

Fl4m3Ph03n1x

How to typespec a float range in Elixir?

Background

I want to create a percentage type using floats, something like:

@type percentage :: 0.0..1.0

Now, you will know that this is not a valid typespec, because I am using 0.0.
The (incorrect) fix there would be to change the spec to:

@type percentage :: 0..1

But this now means I have a range of integers from 0 to 1. I do not want integers, I want floats.
I have also checked the Range.t() option, but this is not what I want. My API requires floats, not an object of type Range.

Question

How can I fix my spec so it works?

Marked As Solved

Fl4m3Ph03n1x

Fl4m3Ph03n1x

I decided to use float and guards in the function. It is not the ideal situation, but its better than having no checks at all.

For those of you reading only the solution, @hissssst provided detailed reasons as to why the solution I wanted to achieve is not possible (see How to typespec a float range in Elixir? - #4 by hst337)

Also Liked

jhogberg

jhogberg

Erlang Core Team

But dialyzer has a type system

Sure, but only if we stretch the definition into uselessness. If we look at dialyzer through the lens of it having a type system, values are lifted to the type level which more or less turns everything into a tautology:

“The type of 1 is 1, the type of :foo is :foo.”
“The return type of bar(1234) is the result of bar(1234).”
“The argument types for quux/2 are those for which it produces a value.”

The latter has funny implications for server loops and other code that doesn’t return normally by design. Oh, and as the type of a value is its value, doesn’t that imply that nearly all functions have dependent types? After all, 1 + 2 returns 3 and that’s a type all of its own :wink:

Understanding that dialyzer only reasons about sets of possible values is crucial to working with it.

For example, if Erlang/Elixir was a statically typed language, maps there would be dependent. When their size is less than 33 items, they use flatmap implementation, while for bigger sizes they use hashmap implementation. So, in this example the type depends on the size of the map

That detail is hidden and could still be under a dependent type system, perhaps a better example would be list_to_tuple/1 where the arity of the tuple depends of the length of the provided list.

How so? How could I make this work?

By teaching dialyzer that all numbers can have ranges, not just integers. It should be about as simple as making the integer range logic in lib/dialyzer/erl_types.erl allow any kind of number as lower and upper bound and change all uses to match that. That’s what we do in the Erlang compiler (which has a separate implementation of dialyzers underlying algorithm) and it works just fine.

jhogberg

jhogberg

Erlang Core Team

This has nothing to do with dependent types and it would be trivial for dialyzer and other tools to support this. If we were so inclined we could even support things like :foo .. :bar to cover the range of all atoms from :foo to :bar, or why not {any(), 53, float()} .. {12, atom(), map()} and other arbitrary ranges? There’s nothing stopping us from doing so.

The problem is that Dialyzer’s type system doesn’t.

dialyzer does not have a “type system,” it’s untyped and only reasons about sets of possible values (which may be infinite). That you cannot express arbitrary sets of possible values through @type annotations (@value_constraint would have been a better name) is nothing more than a limitation in the current implementation of the “type language.”

jhogberg

jhogberg

Erlang Core Team

dialyzer doesn’t know how to do this for Erlang either. The Erlang compiler has a separate implementation of the same underlying idea as dialyzer that it uses for optimizing code, which is capable of handling float ranges.

Last Post!

dimitarvp

dimitarvp

Exactly what I did, and what a funny coincidence that I just had to code something super similar in the last two days:

  @type double :: float()

  @double_min -1.7976931348623157e308
  @double_max 1.7976931348623157e308

  defguard is_double(x) when is_float(x) and x >= @double_min and x <= @double_max

  @spec do_stuff(double()) :: nil
  def do_stuff(x) when is_double(x) do
    dec = Decimal.from_float(x)
    if Decimal.compare(dec, @decimal_double_min) != :lt and
         Decimal.compare(dec, @decimal_double_max) != :gt do
      # the float is in range, let's do stuff!
    else
      # out of bounds, report error.
    end

    # do something else and return.
    nil
  end

As you said, it’s definitely better than nothing. Basically everything except the typespec itself is covered. I am nervous about using >= and <= in the guard but my tests have shown that the comparisons are stable for these boundary values .

Where Next?

Popular in Questions Top

baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
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
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
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

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 131117 1222
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40165 209
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement