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
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
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 ![]()
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
flatmapimplementation, while for bigger sizes they usehashmapimplementation. 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
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
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
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 .
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









