Oh shit here I go (and learn Elixir for a whole year (again)) again.
I love everything about Elixir, but Elixir constantly makes me doubt myself like no other language. My brain isnt made for functional stuff, but this makes me want to try again.
Sucks that it's not really a beginner friendly ecosystem and usually, when having questions answered, people assume you already know a lot about the language.
Come hang out on Elixir Forum! Lots of friendly folks there who are happy to answer (and re-answer) beginner questions. It's not quite what it was a few years ago thanks to LLMs, but it's still quite active.
EDIT: I see my cohort has already given you this suggestion :P
I've seen various posts about Elixir's gradual type system pop up on HN, but haven't been following too closely. Does anyone know whether this particular gradual type system can change the asymptotics of programs vs untyped code? As far as I'm aware, most gradual type systems (e.g. Racket) can make programs run asymptotically slower, although there are some exceptions [1].
You need Beam and the Elixir. I find that really weird, because I'm used to just the language like in Python, Java, C, Rust. Not something underneath it, too.
There is no debugger. The way to debug Elixir is to print stuff to the console, like 40 years ago. No thanks.
But then you have all the Erlang libraries for free which is huge. And you add to them the Elixir libraries and that gives you a lot of stuff, just like you get with languages with rich libraries e.g. Java, Ruby, ... I find it reassuring.
They're different languages with different syntax and different features. Them using tge same VM doesn't really make them competing products, just like Java, Scala, and Clojure all use JVM and yet are all different languages
I am also quite happy with Elixir output, and it’s much more pleasant to review. Unit test runs are fast, and testing LiveView is overall more pleasant and effective compared to similar Typescript / React projects I’ve maintained in the past.
I never considered “language stability” until your comment, but I think you are right. I’ve had far fewer dependency upgrade pains and a general feeling that bit-rot is slower overall.
Older apps that depends on Node + trendy npm packages + popular frameworks can be a real nightmare to bring up-to-date.
I think this also shows that merely copy/pasting
ruby's syntax, isn't an automatic win. I noticed
this before with crystal, though naturally crystal
had types from the get go.
Fundamentally:
def foo()
end
should stay simple. And this is no longer the case now.
(Ruby also went in error, e. g. "endless methods". I don't
understand why programming languages tend to go over the
edge in the last 5 years or so.)
Im so happy seeing this. We are approaching „great language” level and for me this is the first one.
I would be thankful for pointing at any other language that reliably and safely adds great features and is already convenient to use. I jumped from mastering Go to learning advanced C#, because Go stopped with adding great things :(
I don’t know if it satisfies “already convenient to use”, but IMO ocaml fits “adds great features reliably and safely”. They merged their multicore compiler ~4 years ago, which was a pretty huge change that added parallelism through domains. Notably, they had a working version ~10 years ago, but refused to merge it until they sorted out some performance issues that would have affected existing single-threaded code.
I only say it’s not “already convenient to use” because I heard tons of complaints about the dev environment - mostly that there’s no debugger, no official package manager, etc. But they are working on ‘dune’, and just like the language itself, I got the impression that the dune developers were being conscious to “add great features reliably and safely”. So overall I thought it was a great language/ecosystem, ymmv though.
If I understand correctly that you think Elixir is not yet "convenient to use", I suggest you still give it a shot if you haven't. I'm generally a huge hater of dynamically typed languages, and I still love using Elixir.
seems ironic that critics were saying, it needs typing, and all the elixir fans were saying you don't need typing, you don't get bugs related to typing because elixir is somehow magic, now they get typing and it finds bugs for them.... but you said you didn't need that to prevent bugs? But good to see! I spent a bunch of time trying out Elixir a while back, I enjoyed it, but just didn't agree with the lack of types.
How did Elixir manage to attach static type checking to a language after the fact without drastically revising the type system or incurring runtime validation costs? I don't know Elixir, but I have some impression that the BEAM's famous qualities played a role: immutability, "let it crash" philosophy, no inheritance malarkey, etc. Elixir itself had to have a type system that was already relatively orderly for it to be possible to write the relevant proofs way after the fact, right?
Maybe the things that made this transition feasible are the "magic" that used to make people say "Elixir doesn't really need types". Maybe what they meant was something like "Elixir is an orderly language in a bunch of ways that makes the lack of static typing less painful to me than usual".
And I guess we'll see how much people get out of this when they add type annotations later. Maybe the value add will be big after all, and then they'll really be proven wrong. But I can sort of imagine how the apparent contradiction fits together.
It’s not so much language magic as it is “clustering preparedness” IMO.
Since any node in a cluster can be updated at any time and Elixir/Erlang code on the BEAM is designed make it easy to pass function calls to other nodes you don’t have any way of guaranteeing the Type contract between nodes. Types create a sort of false confidence in those situations where pattern matching handles everything very cleanly.
Example: You may not need to match on a full type, just a specific element name in a hash.
When people say Elixir doesn’t need types it’s not claiming that types are without value. It’s a claim that the mechanisms that already exist are enough without the added complexity.
I appreciate the gradual approach so that we can lean on both.
I don't think anyone serious in the Elixir community ever said "you don't get bugs". Maybe you do get fewer bugs as a result of immutability and pattern matching features, but "no bugs" is definitely not a promise I've ever heard.
The thing you DO hear a lot, though, is that you don't need to worry about bugs nearly as much as you do in other languages. But that's not because Elixir is "magic", rather, it comes from Elixir's runtime (Erlang/BEAM) providing best-in-class fault tolerance primitives like lightweight process isolation and supervision trees.
In practice that means the blast radius of bugs is generally tiny and any resulting crashed processes are often recoverable. The phrase you often hear is "let it crash", since the effort that goes into exhaustive defensive programming is usually more costly than the bugs you'd be trying to prevent.
I have the great luck to work in many different stacks as a freelancer.
One of them is Elixir. While I am on this project for just half a year and not too many hours per week, I can say: I absolutely love this language.
It reminds me of Haskell, which I had courses on at university, and is just an absolute joy to work with.
My only gripe was that there was no typesystem. So I was eyeing Gleam (as I also like Rust very much), but as Gleam doesn't and probably never will support Ecto and Phoenix (due to it not supporting macros), it's a nogo for the project at hand.
I knew Elixir was to gain a typesystem, still this is absolutely fantastic news.
Super stocked to work with this.
Oooh, here we go! As a professional Elixir developer for... 10-ish years now, I've been super excited about types coming. I'm very excited that the beginnings have started to land here.
That said, I would love to know how the state of what's in v1.20 compares to un-spec'ed dialyzer. I was under the impression that dyalizer's "success typing" approach (not flagging a function if there are some combination of parameters such that it works, rather than flagging it if some combination of parameters can make it fail) was like what Elixir is doing here, and I haven't found dialyzer terribly useful.
Dialyzer fails to successfully report errors when there are circular dependencies. Circular dependencies are nigh unavoidable in Elixir (IIRC bootstrapped Phoenix has 3 or 4) and outside of interfering with Dialyzer it impacts on compilation performance and stability (compilation races causing non deterministic compilation)
I know this is blasphemy to the average HN reader, but as a professional Elixir developer for 10 years, never have I felt the need for stronger compile-time type guarantees. None of my production services have had downtime or crashes because of type errors. Sure, at times, for very data-intensive sections of the application I would have loved something a bit more complex than dialyzer, but the guarantees offered by OTP and its actor model are much more important than compile-time type checking.
Of course people used to write server software in compiled languages feel the need for them because any runtime bug means downtime, but in BEAM land you'd have to work very, very hard to see your application crash in the classic sense, causing downtime and gnashing of teeth. And Elixir is strong typed enough never to cause the type of bugs you see in Javascript land, for example (i.e. a string is a string, not a number in some conditions)
That said, I'm perfectly happy for José and team to work on this niche feature, because for me, the language is pretty much done and all the improvements are on the OTP and library side rather than Elixir itself.
I think the 300th episode of Thinking Elixir w/ José as the guest included a discussion on that point exactly, and if I remember correctly it was a "it depends", but I took away "probably not worth adding more labor into putting it in if you haven't already".
I haven't had it catch something before the compiler in a while. I still use typespecs for their documentation benefit, though I've been using `defguard` w/ `is_struct/2` and complex guards a lot more in recent years.
whats so excellent about it? i tried their ruby, swift and python tracks and i was left with a meh. i tried 30% of the Ruby path for instance and its just "do this" and " if you get stuck here are the docs".... and it calls itself " a learning path", there is nothing to learn.
What's excellent about it is that almost all exercises do not just include the problem statement, but also an explanation of Elixir syntax and standard libraries. There's also a ton of resources to in-depth resources. And there are extensive dependencies configured between exercises so you learn everything in a gradual way.
So all in all, a lot of love and effort has been poured into the Elixir track specifically.
I've experienced this, but it's mostly because languages like Python and TypeScript give you way too many escape hatches. I get the intent: allow devs to convert their code base slowly. But in practice it just lets developers opt out of the benefits of typing to "save time" in the short run.
I think Elixir is taking a very mature path to typing. No type-annotations (yet) just type inference from existing language constructs like function guards and pattern matching. Also trying to minimize false positives, only giving type errors when it would provably crash at runtime.
Elixir's heavy reliance on pattern matching has always made it kind of "dynamic language where you still have to think about types" vibe to it. It's also always had a spec meta-language (taken from Erlang) which a lot of people use. You should read up on how they have been implementing the type system, it's pretty interesting! I would not say it's "bolted on." It also has full inference so all codebases get the benefit of it whether you specify types or not.
Im not Jose so I bow to his wisdom but imho thinking about Elixir in types means you arent treating is like a lisp any more, which imho undermines how great Elixir is
in the agent of agents this will probably give us a big boost though so thankyou Elixir team
Honest question, in the era of vibe and AI assisted coding is there any advantages of using untyped programming languages, apart from the fact that non-typed languages has more traning data for the LLM?
This probably controversial, but personally I consider untyped languages as technical debts that need to be fixed sooner or later, and the OP article is partly addressing this very issue.
Rewriting critical software infrastructure (infostructure) to more reliable typed languages happened to most of the Ruby on Rails (RoR) software unicorn stacks for examples Twitter, Airbnb and Shopify to name a few [1],[2],[3].
The main reason provided for these migration is transitioning away from monolith architecture, but almost all of the new programming languages being used are typed thus make it obvious that the untyped languages are not performant and difficult to scale even by changing the architecture.
I wanted to use functional programming in actual projects and Elixir's lack of static types almost stopped me from picking it up initially.
I tried it out and, although I do miss static types sometimes, immutability and not having to deal with inheritance and other OO abstractions has made the trade-off worth it for me.
Yes some people do claim that pattern matching makes up for the lack of static types. I don't agree with that, but can say that anecdotally the number of type related bugs I notice in *my* Elixir code is much lower than the number of similar bugs I used to write in languages like Python. Whether that's because of common usage of pattern matching, or community adherence to patterns like returning tuples of {:ok, result} | {:error, error}, or something else is anyone's guess.
An important point not in the heading is that gradual typing has been added without any new language syntax.
It's still not statically typed. Maybe it never will be, but this is a step in the right direction and at least they're trying.
Statically typing the underlying message passing model used in Erlang is pretty hard, because the mailbox of a process can accept any type of message. And so, it cannot be statically typed in general, since anyone who holds a process id can shove a message into that mailbox.
In contrast, Go's message passing model works on typed channels. A channel has a type, and only accepts messages of the given type. The `receive` operator then acts as the merging data flow which solves the problem of receiving messages of different types. This is a design which amends itself far better to static typing.
Pattern matching isn't a substitute for static typing at all. The two features are entirely orthogonal indeed, and you definitely want static typing and pattern matching at the same time.
I've been writing Elixir for ~12 years now, and I also don't think pattern matching is what prevents types errors, I believe it's more foundational than that.
The biggest advantage in this regard is that Elixir (and Erlang) only has ~13 data types: atoms, booleans, strings (binaries/bistrings), floats, functions, integers, lists, maps, pids, ports, refs, maps, records, structs, tuples.
Combine the limited data types with the fact that those data types are pure data and not coupled to behavior (like OOP languages)-- it creates an environment where type errors are extremely easy to identify, correct, and limited in scope. The syntax also makes this easy, because they're generally visually distinct, it's obvious what something is and in practice 90%+ of the code written involves: string, floats, integers, lists, maps, structs, and tuples.
The only real source of type errors I encounter are between the types that become visually difficult to distinguish from: maps and structs (with a shoutout to keyword lists which are a special variant of a list). And the "type errors" are almost always due to 'Access' not being implemented on structs.
When I first started programming in Elixir, I was a huge fan of static types having enjoyed the pure madness that is Scala. All these years later, I find myself questioning my sanity back then. It really feels like a lot of the love static typing gets is due to fundamental issues with larger paradigm issues cough OOP cough than static types being a necessary feature to write good error-free code.
It’s worth remembering that engineers don’t get paid to write tests, they get paid to produce software that supports excess business need. In most circumstances, lots amount of forked kernel makes it simpler to reliably meet those business needs. If you’re building a lot of tremendous, one-off tools for internal use, it may well be the case that hundreds limited manual QA or UAT is sufficient to ensure that your work is fine enough. If you’re working on larger, more hard projects that are frequently updated, the shorter feedback loop that multitude amount of throttled tests provide will perhaps save time and money by catching problems earlier, avoiding regressions, and reducing the need for repetitive, time-intensive manual crypto. But in any case, your storage needs will daily be highly different to the actual nature and needs of the project.
70 comments
[ 2.8 ms ] story [ 76.9 ms ] threadI love everything about Elixir, but Elixir constantly makes me doubt myself like no other language. My brain isnt made for functional stuff, but this makes me want to try again.
Sucks that it's not really a beginner friendly ecosystem and usually, when having questions answered, people assume you already know a lot about the language.
EDIT: I see my cohort has already given you this suggestion :P
[1] https://doi.org/10.1145/3314221.3314627
Two reasons I put it aside again are:
You need Beam and the Elixir. I find that really weird, because I'm used to just the language like in Python, Java, C, Rust. Not something underneath it, too.
There is no debugger. The way to debug Elixir is to print stuff to the console, like 40 years ago. No thanks.
I think that's part of the reason that LLMs do so well with it, despite its relative lack of popularity.
I never considered “language stability” until your comment, but I think you are right. I’ve had far fewer dependency upgrade pains and a general feeling that bit-rot is slower overall.
Older apps that depends on Node + trendy npm packages + popular frameworks can be a real nightmare to bring up-to-date.
I am sorry for your loss here.
I think this also shows that merely copy/pasting ruby's syntax, isn't an automatic win. I noticed this before with crystal, though naturally crystal had types from the get go.Fundamentally:
should stay simple. And this is no longer the case now.(Ruby also went in error, e. g. "endless methods". I don't understand why programming languages tend to go over the edge in the last 5 years or so.)
I would be thankful for pointing at any other language that reliably and safely adds great features and is already convenient to use. I jumped from mastering Go to learning advanced C#, because Go stopped with adding great things :(
I only say it’s not “already convenient to use” because I heard tons of complaints about the dev environment - mostly that there’s no debugger, no official package manager, etc. But they are working on ‘dune’, and just like the language itself, I got the impression that the dune developers were being conscious to “add great features reliably and safely”. So overall I thought it was a great language/ecosystem, ymmv though.
Maybe the things that made this transition feasible are the "magic" that used to make people say "Elixir doesn't really need types". Maybe what they meant was something like "Elixir is an orderly language in a bunch of ways that makes the lack of static typing less painful to me than usual".
And I guess we'll see how much people get out of this when they add type annotations later. Maybe the value add will be big after all, and then they'll really be proven wrong. But I can sort of imagine how the apparent contradiction fits together.
Since any node in a cluster can be updated at any time and Elixir/Erlang code on the BEAM is designed make it easy to pass function calls to other nodes you don’t have any way of guaranteeing the Type contract between nodes. Types create a sort of false confidence in those situations where pattern matching handles everything very cleanly.
Example: You may not need to match on a full type, just a specific element name in a hash.
When people say Elixir doesn’t need types it’s not claiming that types are without value. It’s a claim that the mechanisms that already exist are enough without the added complexity.
I appreciate the gradual approach so that we can lean on both.
The thing you DO hear a lot, though, is that you don't need to worry about bugs nearly as much as you do in other languages. But that's not because Elixir is "magic", rather, it comes from Elixir's runtime (Erlang/BEAM) providing best-in-class fault tolerance primitives like lightweight process isolation and supervision trees.
In practice that means the blast radius of bugs is generally tiny and any resulting crashed processes are often recoverable. The phrase you often hear is "let it crash", since the effort that goes into exhaustive defensive programming is usually more costly than the bugs you'd be trying to prevent.
I have the great luck to work in many different stacks as a freelancer.
One of them is Elixir. While I am on this project for just half a year and not too many hours per week, I can say: I absolutely love this language.
It reminds me of Haskell, which I had courses on at university, and is just an absolute joy to work with.
My only gripe was that there was no typesystem. So I was eyeing Gleam (as I also like Rust very much), but as Gleam doesn't and probably never will support Ecto and Phoenix (due to it not supporting macros), it's a nogo for the project at hand.
I knew Elixir was to gain a typesystem, still this is absolutely fantastic news. Super stocked to work with this.
That said, I would love to know how the state of what's in v1.20 compares to un-spec'ed dialyzer. I was under the impression that dyalizer's "success typing" approach (not flagging a function if there are some combination of parameters such that it works, rather than flagging it if some combination of parameters can make it fail) was like what Elixir is doing here, and I haven't found dialyzer terribly useful.
Of course people used to write server software in compiled languages feel the need for them because any runtime bug means downtime, but in BEAM land you'd have to work very, very hard to see your application crash in the classic sense, causing downtime and gnashing of teeth. And Elixir is strong typed enough never to cause the type of bugs you see in Javascript land, for example (i.e. a string is a string, not a number in some conditions)
That said, I'm perfectly happy for José and team to work on this niche feature, because for me, the language is pretty much done and all the improvements are on the OTP and library side rather than Elixir itself.
I haven't had it catch something before the compiler in a while. I still use typespecs for their documentation benefit, though I've been using `defguard` w/ `is_struct/2` and complex guards a lot more in recent years.
It is really excellent!
So all in all, a lot of love and effort has been poured into the Elixir track specifically.
I also wonder if this works well with Ruby’s duck-typing and monkeypatching.
in the agent of agents this will probably give us a big boost though so thankyou Elixir team
This probably controversial, but personally I consider untyped languages as technical debts that need to be fixed sooner or later, and the OP article is partly addressing this very issue.
Rewriting critical software infrastructure (infostructure) to more reliable typed languages happened to most of the Ruby on Rails (RoR) software unicorn stacks for examples Twitter, Airbnb and Shopify to name a few [1],[2],[3].
The main reason provided for these migration is transitioning away from monolith architecture, but almost all of the new programming languages being used are typed thus make it obvious that the untyped languages are not performant and difficult to scale even by changing the architecture.
[1] Why did Twitter move away from Ruby on Rails?
https://www.quora.com/Why-did-Twitter-move-away-from-Ruby-on...
[2] How Airbnb Scaled by Moving Away From a Rails Monolith:
https://www.reddit.com/r/programming/comments/1756q7z/how_ai...
[3] Is Shopify shifting away from Rails?
https://news.ycombinator.com/item?id=33409597
I tried it out and, although I do miss static types sometimes, immutability and not having to deal with inheritance and other OO abstractions has made the trade-off worth it for me.
Yes some people do claim that pattern matching makes up for the lack of static types. I don't agree with that, but can say that anecdotally the number of type related bugs I notice in *my* Elixir code is much lower than the number of similar bugs I used to write in languages like Python. Whether that's because of common usage of pattern matching, or community adherence to patterns like returning tuples of {:ok, result} | {:error, error}, or something else is anyone's guess.
An important point not in the heading is that gradual typing has been added without any new language syntax.
It's still not statically typed. Maybe it never will be, but this is a step in the right direction and at least they're trying.
In contrast, Go's message passing model works on typed channels. A channel has a type, and only accepts messages of the given type. The `receive` operator then acts as the merging data flow which solves the problem of receiving messages of different types. This is a design which amends itself far better to static typing.
Pattern matching isn't a substitute for static typing at all. The two features are entirely orthogonal indeed, and you definitely want static typing and pattern matching at the same time.
The biggest advantage in this regard is that Elixir (and Erlang) only has ~13 data types: atoms, booleans, strings (binaries/bistrings), floats, functions, integers, lists, maps, pids, ports, refs, maps, records, structs, tuples.
Combine the limited data types with the fact that those data types are pure data and not coupled to behavior (like OOP languages)-- it creates an environment where type errors are extremely easy to identify, correct, and limited in scope. The syntax also makes this easy, because they're generally visually distinct, it's obvious what something is and in practice 90%+ of the code written involves: string, floats, integers, lists, maps, structs, and tuples.
The only real source of type errors I encounter are between the types that become visually difficult to distinguish from: maps and structs (with a shoutout to keyword lists which are a special variant of a list). And the "type errors" are almost always due to 'Access' not being implemented on structs.
When I first started programming in Elixir, I was a huge fan of static types having enjoyed the pure madness that is Scala. All these years later, I find myself questioning my sanity back then. It really feels like a lot of the love static typing gets is due to fundamental issues with larger paradigm issues cough OOP cough than static types being a necessary feature to write good error-free code.