Today we celebrate 15 years since Elixir’s first commit! To mark the occasion, we are glad to announce the first release candidate for Elixir v1.20, which performs type inference of all language constructs, with increasing precision.
In this blog post, we will break down exactly what this means, and what to expect in the short and medium term of the language evolution (roughly the next 15 months).
Read the full announcement here:
Release notes:
TL;DR: this release adds type inference of all constructs. At the moment, we expect false positives (warnings that should not be emited), please let us know if you run into those. Also, please let us know about performance. If Elixir v1.20 compiles slower than v1.19, then ping us too. `mix compile –force –profile time` is a good tool to measure it (paticularly the times reported at the end).
I found something very surprising … I have 2 Elixir versions installed using mise:
$ mise ls
Tool Version Source Requested
# …
elixir 1.19.5-otp-28
elixir 1.20.0-rc.0-otp-28 ~/.config/mise/config.toml 1.20.0-rc.0-otp-28
erlang 28.3 ~/.config/mise/config.toml latest
# …
On the first one 1.19.5-otp-28 the credo tool does not emits any issues, but in 1.20.0-rc.0-otp-28 it produces a false positive for only one check Credo.Check.Readability.ModuleDoc for all modules in my project.
Edit: I have found the issue. It appears that credo expected AST meta for block expression to be an empty list, but in 1.20.0-rc.0 it contains additional line and column information.
All other tools (ExUnit tests, ex_coveralls report and dialyzer report) does not seem to change (as expected)
The only one thing I would like to see fixed in dialyxer output are OTP 28 related warnings when building PLT, but as mentioned above it’s the same behaviour as in older Elixir releases and besides that the checks so far works without any problem.
xmerl_ucs.erl:58:1: Warning: missing specification for function is_iso10646/1
xmerl_ucs.erl:70:1: Warning: missing specification for function is_unicode/1
xmerl_ucs.erl:75:1: Warning: missing specification for function is_bmpchar/1
xmerl_ucs.erl:84:1: Warning: missing specification for function is_latin1/1
xmerl_ucs.erl:88:1: Warning: missing specification for function is_ascii/1
xmerl_ucs.erl:92:1: Warning: missing specification for function is_iso646_basic/1
xmerl_ucs.erl:108:1: Warning: missing specification for function is_visible_latin1/1
xmerl_ucs.erl:117:1: Warning: missing specification for function is_visible_ascii/1
xmerl_ucs.erl:122:1: Warning: missing specification for function to_ucs4be/1
xmerl_ucs.erl:125:1: Warning: missing specification for function from_ucs4be/1
xmerl_ucs.erl:128:1: Warning: missing specification for function from_ucs4be/2
xmerl_ucs.erl:131:1: Warning: missing specification for function to_ucs4le/1
xmerl_ucs.erl:134:1: Warning: missing specification for function from_ucs4le/1
xmerl_ucs.erl:137:1: Warning: missing specification for function from_ucs4le/2
xmerl_ucs.erl:141:1: Warning: missing specification for function to_ucs2be/1
xmerl_ucs.erl:144:1: Warning: missing specification for function from_ucs2be/1
xmerl_ucs.erl:147:1: Warning: missing specification for function from_ucs2be/2
xmerl_ucs.erl:150:1: Warning: missing specification for function to_ucs2le/1
xmerl_ucs.erl:153:1: Warning: missing specification for function from_ucs2le/1
xmerl_ucs.erl:156:1: Warning: missing specification for function from_ucs2le/2
xmerl_ucs.erl:161:1: Warning: missing specification for function to_utf16be/1
xmerl_ucs.erl:164:1: Warning: missing specification for function from_utf16be/1
xmerl_ucs.erl:167:1: Warning: missing specification for function from_utf16be/2
xmerl_ucs.erl:170:1: Warning: missing specification for function to_utf16le/1
xmerl_ucs.erl:173:1: Warning: missing specification for function from_utf16le/1
xmerl_ucs.erl:176:1: Warning: missing specification for function from_utf16le/2
xmerl_ucs.erl:181:1: Warning: missing specification for function to_utf8/1
xmerl_ucs.erl:184:1: Warning: missing specification for function from_utf8/1
xmerl_ucs.erl:193:1: Warning: missing specification for function from_latin9/1
xmerl_ucs.erl:483:1: Warning: missing specification for function to_unicode/2
xmerl_ucs.erl:523:1: Warning: missing specification for function is_incharset/2
My codebase is actually faster compiling with 1.20-rc (otp28) but only just couple dozen ms and well within margins of error. Nice job!
Only curiosity I have is that the compiler catches type mismatches in our tests, tests that were testing errors on wrong input types ha! Suppose in 1.20 we will be able to delete those!
Unless Elixir type system would generate a @spec or ex_doc would support Elixir’s Typespec natively dialyzer would have a great value at least in documentation purposes. From unknown types to mismatched ones. There are still lots of cases that dialyzer uses. Also it’s very useful when you assume that your complex logic always returns specific data and dialyzer warns sometimes it’s not a case. That’s very useful in code refactor when you forgot to update some small part of a bigger module.
I wonder if Elixir core team have any further plans about improvements to type system like mentioned ex_doc support or auto-generation of function @spec. In past we had discussions about new type definitions starting with $ character, but I’m not sure how those topics ended up like if those are on the official roadmap or so.
Any generation of @spec is highly unlikely. Typespecs lack negations like „anything, but an atom“ and therefore are unfit to represent the types inferred by the elixir typesystem.
Type signatures are on the roadmap though - see 3. in the ~15 months section. They‘ll surely become part of docs once they exist as well.
Yeah, I remember that the type system allowed more stuff. Support for negations would not look good, but is rather possible - it’s just about list every type except the excluded one.
Good to know they are on roadmap, but 15 months is a lot of time especially in our rapidly changing world. Within that time almost everything can change. On GitHub there is a discussion that if situation would go that way Tailwind sooner or later may become an abandonware as they loose the traffic (by 80% or so) to their documentation (which is the only place for their paid plans) due to increased LLM usage. Looks like they already lost 75% jobs there.
Only for the simple cases. Consider that there‘s also „anything, but a map with a :name key“ and you‘re thorougly in unrepresentable as typespec territory. You cannot write out all possible maps, which do not have a specific key.
1.20.rc.0 perf is good so far, compile times are marginally lower compared to 1.19.5. The typechecker highlighted some dead/unreachable code to clean up, and a few missing virtual field declarations on an Ecto schema.
As someone else already mentioned, some tests that were asserting on ArgumentError or FunctionClauseError being thrown started to warn. Maybe there could be situations in which one would want to keep such tests, then it would be good to be able to suppress those compilation warnings somehow.
I actually have this exact use-case in one of my projects. I have some common repo functions for Ecto Schema modules create/1, create!/1, read/1, read!/1, update/2, update!/2, delete/1, and delete!/1 for simple single-row non-join CRUD operations. I have these defined in a Models.Base module and I conditionally define the create, update, and delete operations based on a :read_only option to the __using__/1 macro. I’ve only just started doing this, so it’s not a big deal if I need to figure out an alternative, but I do like having tests in my test suite that assert my read-only schema modules throw an UndefinedFunctionError when calling any of the write operation functions. Just some extra piece of mind in case I forget the read_only: true when defining/updating a schema module. This is not a situation that would be caught by the type system since a call to create!/1 on a should-be-read-only module would only give a typing violation if I correctly remembered to put read_only: true.
Another situation that is more common/realistic is testing program inputs. A static type system can’t protect against invalid data coming into the program at runtime, so if the application takes any kind of user input, having negative test cases to assert bad inputs are handled gracefully is useful. This is a common issue in TypeScript projects because devs get it into their heads that the compiler will catch all the type errors, and then it crashes at runtime due to bad data input by the user. Even in Elixir where we “let it crash”, that only works in embedded applications that don’t have a user looking at the loading bar when the LiveView reloads, so negative tests are good.
All that being said, I am 100% in favor of better type inference if it doesn’t come with huge performance costs. The more the compiler can do for me without extra code/effort on my part the better. Thanks Elixir Team!
Indeed. For the record, in the Elixir repo, these rests are either being removed or tested by using the Process.get(:unused, wrong_arg) hack (examples).
To add to @sabiwara’s workaround, the type system won’t warn if the program input is within its valid type. For example, if you have a function that expects integers only from 2..26 (like Integer.parse/2), passing an invalid input such as 0 won’t trigger any warning. However, if you give a boolean, then a warning is emitted.
Re: negative tests. I’m currently using apply/3 for these UndefinedFunctionError tests since it’s a bit more intentional than a generic get on the module, but I don’t like the idea of using a workaround like that for a defined function where I’m passing in a boolean or string instead of an integer.
It’s actually a very common error if the FE is standalone JS and not server rendered for someone to accidentally call .toString() on a number before serializing, and then the JSON field gets parsed as "1” instead of 1 on the BE. For most applications, this isn’t a big deal, but I’ve ran into this when writing a very strict input validation layer before, so I think having a way to selectively disable type inference for test modules is a good feature to have. I don’t think we should have @type-ignore pragmas or anything that could be used anywhere we want cause that would encourage type hell, and Elixir’s dynamic() is much better than other langague’s any, so I don’t think we will need it in application code.
Some code is just intentionally written to violate application constraints though. Namely negative test cases.
Hopefully, fuzz testing and simulation testing gets easier to implement at small scales, so this problem would eventually go away, but for now, a workaround in the language or library layers instead of the user’s test code would be ideal.
Maybe this could be solved with a custom mix compiler for ExUnit modules that disables type inference (but not type checking in general)?
Would be great if the language tooling could somehow encourage this practice. I always advocate for doing all input parsing as close to the application boundary as possible at work and in community projects. It saves so much time later on to know that any data structure that makes it into the service layer is valid.
Not sure how that would be detected though. Maybe more of a linter thing than compiler warning since it’s more of a code style/pattern issue than an actual technical problem with the code?