> Are many of the other languages you've used strongly typed? That's the thing that gives me pause - I feel like I rely on the compiler a lot in languages where it can do a lot for me, and that I'd be really frustrated managing a large project without it.
As someone who has been writing Elixir professionally for 4-ish years, and who is otherwise about as big of a proponent of static types as is possible, this is simultaneously my biggest critique of Elixir, and also a critique that has ended up being more theoretical than not. I wish that it were statically typed, but in practice that hasn't been slowing me down.
Part of the reason for that is that while Elixir is dynamically typed, I've found that it's possible to pretend it is a statically typed language if you squint just right. By that, I mean making judicious use of typespecs [0], dialyzer [1], Norm [2], and just generally constraining the way you write code to mirror the sorts of code that you'd write in a language that offers algebraic data-types, like Haskell. I've been meaning to put together a blog post on what I mean by this, because I often hear people talking about how unusable Dialyzer is, so I feel like my team is a rare example of a large production app that requires that Dialyzer typechecks on every build. This makes large-scale refactoring of our app almost (but not quite) as easy as it would be in Haskell.
The macro system in Elixir also means that it's possible to write libraries like typeclass [3], that offer compile-time guarantees (using compile-time property tests!) that your implementations correctly implement a given interface. I think that as the language evolves, we'll be seeing a lot more examples of macros that move runtime logic into the compilation step, to offer compile-time guarantees and safety. For example, a few weeks ago, I prototyped an experimental library in a few hours that added support for compile-time OCaml style parameterized-modules. [4]
Obviously, all of these techniques don't get you quite as far as proper static typing would (though the Whatsapp team is working on static types for Erlang! [5]), but the rest of the language and the ecosystem is just so well thought out, that I've been okay with that!
To add to this, it's worth noting that Elixir would have difficulty _ever_ becoming statically typed because of how message-passing works. For example, if you look at Gleam [0] -- the recent project that's trying to create a statically typed language for the BEAM -- it can't handle message-passing [1].
And I totally agree with you here:
> I've found that it's possible to pretend it is a statically typed language if you squint just right.
My Elixir code tends to use a lot of pattern matching on structs. It's not real static typing, but it gets me a lot of mileage!
It would be difficult (or perhaps impossible) to type all of Erlang's message passing features, so we reduced scope to not include distributed message passing or hot upgrades- so the types of all actors in the system can be known up front in order to be checked.
In practice this is enough for the vast majority of applicatinos, and messages with runtime checks (like in regular Erlang) can be used for the other situations if required.
I've heard a number of times that static typing would be near impossible for Erlang because of how message-passing works, but just the other day I read a quote where one of the creators (I think it was Joe Armstrong, but not completely certain) indicated that the primary reason they decided against static typing was the hot code reloading, not the feasibility. or not primarily anyways.
Not disagreeing with you, I guess. It was just surprising/interesting to read.
I'm the author of Gleam, a statically typed language on the BEAM.
What you've said matches my findings. Messages can be typed, however hot code upgrades cannot be. For Gleam we've sacrified hot code loading in exchange for types, which I think is a good trade for most use cases.
Hi, thank you for contributing! I have gotten into the functional programming paradigm conceptually from working with react/redux. We do all of our development work using TypeScript, rather than JavaScript. Typescript isn’t really static typing, but more like type assertions on top of a dynamic language, but hot reloading works fine and is a core component of the workflow, especially with the ability to maintain state in between loads. I’m new to this world and haven’t yet tried Elixir, but what is it about the type system that prevents hot reloading? Is it because the underlying execution system is using types for something? Could a typescript-style language be built on top of Elixir and BEAM that would allow hot reloading and type assertions together?
Not touching on the reasons why the type system and hot code reloading are complex in the BEAM, but the hot code reloading dev workflows in other languages and the BEAM one are two completely different beasts.
Erlang/OTP was designed for systems where the amount of acceptable downtime is 0, you don't want phone calls failing because you have to fix a bug, the solution is a system where you can replace parts of it running live, if you replace a module all processes currently running it will still work normally but any new call to the module will use the updated version of it.
The hot loading of code we're talking about is mutating of running code in production, and it involves running multiple different versions of the same code at the same time in order to do a clean and incremental migration to the new version while continuing to serve traffic. It's a different set of constraints to hot loading JavaScript style and very difficult to get right. The nation problem is that you don't have any way of knowing what the the state of the running system is (it is mutable) so you cannot unify your new types with the existing system.
We could probably do hot loading in development using type assertions fairly happily.
I see, thank you for the response. I haven’t ever heard of something like this being possible in production. We typically have to do phased rollouts at a different level of the stack, it’s not something supported by the runtime!
It came from Ericsson needing to upgrade telephone switch firmware without taking the system down. These telephone switches were often expensive and in physically difficult to reach places so having a standby for a switch-over wasn't viable.
It’s a combination of message passing to distributed nodes AND hot reloading. The compiler guarantees would only work on your current node, but if you were sending something to another node in the cluster you have zero knowledge of the types without forcing a full contract exchange on connection.
> Type checking unrestricted message passing would be difficult, but I can imagine a subset that's typeable.
I'd love to see a discussion on this somewhere. I can totally imagine a world where it's true that "yes, you can't send messages of type X because the receiver can't pattern match on them in the way you expect, but what you're really trying to do is Y, and you _can_ send messages of type Z that accomplish Y."
That's hard to say. I've never worked on an engineering team of more than 10 people.
That being said, with the tools I described, you still have type annotations and automated type checking, so the documentation benefits of static types are still present.
Thanks for sharing all this! It's great to hear that it's not an issue in practice and they're some useful looking tools to check out when I get a chance to play with it. It definitely sounds great to work with :)
As someone who has been writing Elixir professionally for 4-ish years, and who is otherwise about as big of a proponent of static types as is possible, this is simultaneously my biggest critique of Elixir, and also a critique that has ended up being more theoretical than not. I wish that it were statically typed, but in practice that hasn't been slowing me down.
Part of the reason for that is that while Elixir is dynamically typed, I've found that it's possible to pretend it is a statically typed language if you squint just right. By that, I mean making judicious use of typespecs [0], dialyzer [1], Norm [2], and just generally constraining the way you write code to mirror the sorts of code that you'd write in a language that offers algebraic data-types, like Haskell. I've been meaning to put together a blog post on what I mean by this, because I often hear people talking about how unusable Dialyzer is, so I feel like my team is a rare example of a large production app that requires that Dialyzer typechecks on every build. This makes large-scale refactoring of our app almost (but not quite) as easy as it would be in Haskell.
The macro system in Elixir also means that it's possible to write libraries like typeclass [3], that offer compile-time guarantees (using compile-time property tests!) that your implementations correctly implement a given interface. I think that as the language evolves, we'll be seeing a lot more examples of macros that move runtime logic into the compilation step, to offer compile-time guarantees and safety. For example, a few weeks ago, I prototyped an experimental library in a few hours that added support for compile-time OCaml style parameterized-modules. [4]
Obviously, all of these techniques don't get you quite as far as proper static typing would (though the Whatsapp team is working on static types for Erlang! [5]), but the rest of the language and the ecosystem is just so well thought out, that I've been okay with that!
[0] https://hexdocs.pm/elixir/typespecs.html
[1] https://github.com/jeremyjh/dialyxir
[2] https://github.com/keathley/norm/
[3] https://github.com/witchcrafters/type_class
[4] https://github.com/QuinnWilton/daat/
[5] https://www.facebook.com/careers/jobs/229713254864749/