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.
Why, consider: mailbox → pattern matching → fully statically typed code.
It's not unlike the standard HTTP-based API → routing and parsing → fully statically typed code.
Maybe you’re implying that message passing makes compile-time validation of messages difficult? The types themselves are a solved problem, as long as you allow actors to fail when they receive a message they can’t handle.
If you use Phoenix, using types at the data model level using changesets and then trickling them down all the way to the UI is a very good compromise. As changesets provide type validations out of the box too.
Yeah, one of the worst practices. I've been working with Elixir professionally for 6 years now and I still see this sh*t everywhere. Bad APIs, bad UIs because someone coupled themselves to the database structure and can't escape. List of memberships? Keep them as a list with the same fields as the junction table. Top-level APIs taking maps with string keys as "params" so they can very easily be cast for a changeset.
This was the only out of box solution when Elixir didn't support types. So, if you really did Elixir professionally for 6 years, you'd know that by now.
> Bad APIs, bad UIs because someone coupled themselves to the database structure and can't escape.
If you don't commit yourself to the database structures you defined at the time of application creation, then it just reflects poor planning and architecture overall as that is one of the very first things you do.
What you describe is an approach a lot of NoSQL fans use - use whatever works then, worry about datatypes later on. That's how you shoot yourself in the foot.
> List of memberships? Keep them as a list with the same fields
Again, using embeds_many or has_many works well too, using changesets - which is my point exactly. Not sure where the disagreement is here.
Your account is full of just ragebait comments at a quick glance, so I'm just going to leave it here.
> If you don't commit yourself to the database structures you defined at the time of application creation, then it just reflects poor planning
No it reflects the reality that requirements and applications evolve over time. You sound like someone who's never supported an application for more than 5 minutes.
> You sound like someone who's never supported an application for more than 5 minutes.
If your application requirements change every 5 minutes, then you prove my point - you suck at architecting and should honestly just give your job away to someone more competent.
Sometimes types are worse than the alternative.
I obviously don't know your specific use case, but in my experience having the database schema reflect throughout a project means its either very small or the design is going to run into problems.
It also sounds like a potential security nightmare. We have a policy of never sending domain objects across the wire so nothing accidentally gets sent. APIs must strictly whitelist data structures.
The way this can work in something like an Elixir or Clojure: you have gradual types in most of the core code, but you translate it just before you hit the view layer (e.g. templates).
The great thing about dynamically typed languages is you don't have to declare a new type for each view. You just select out the data you need and expose it for the view. In Clojure this is as simple as a select-keys.
The disagreement is on Ecto schemas used to represent databases tables from the persistence layer to the UI. Of course, use changesets to normalise user input but using the same schemas everywhere is a sign of immaturity as a developer. You really sound like someone who only does CRUD services. Real world is often more complex.
> Real world is often more complex.
Which is why you architect before-hand with a paradigm of your choice, like DDD (Domain Driven Design) using proper contexts (which Phoenix supports) beforehand. That is the sign of a mature developer, not the other way around.
If your datatype for a column evolves over time to completely different types, it's just an excuse for poor planning and architecture. Eg. A string turning into an integer. That just sounds like someone junior would do with MongoDb.
> You really sound like someone who only does CRUD services.
You throw this like an insult, but in reality most applications can be simplified to just CRUD services. Chat interfaces? CRUD. Social Media? CRUD. Banking? CRUD.
I haven't used Elixer but tt's generally a good idea for the UI to have a different data model than the database (even if it means you initially type almost the same thing twice and have to write a tedious translation layer).
This lets you evolve each part independently and use the "native" types frontend vs backend, which happens surprisingly frequently as the app grows
> but tt's generally a good idea for the UI to have a different data model than the database
You're not wrong and most other comments are responding this from some sort of UI library perspective, like React / Svelte. However, if you're using even the barebones scaffolded UI using LiveViews from Phoenix, you don't have to do any of these. Phoenix will wire up the form to the changesets by default. Which is what I'm referring to.
Phoenix does have that. ViewModels. I don't think its required to use though, but we always do.
Do changesets incur a runtime cost?
Not sure what you mean here. Changesets are used to validate user input before interaction with business logic or your database; of course data validation has a runtime cost, in any language.
Please don't use changesets to enforce some kind of type system between system components. In case you do not trust your own code, Elixir is strongly typed (though not static typed), there are test cases, there's dialyxir and if still you cannot stop yourself from passing a number where a string will do, the process will crash, log a message for you to fix the bug, and get restarted by a supervisor.
I get why people are obsessed with static typing on "normal" languages, where bugs cause system downtime, but the Erlang platform gives you so many guarantees that even if you somehow make a mistake, it is never catastrophic. Gradual typing in Elixir is a nice cherry on top of the runtime, not the cornerstone to robust OTP software.
Ecto Changesets[0] are runtime constructs, yes. They're similar to libaries like Pydantic, if you're familiar with Python.
[0] - https://ecto.hexdocs.pm/Ecto.Changeset.html
Yes, this is exactly what I was wondering, thanks. Another version of this that I love is Effect Schema in TypeScript land.
The runtime costs aren’t trivial, especially on large datasets, but I’ve come to love this pattern a lot.
yes, they do. its minimal though
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.
Sounds very similar to my experience with Clojure. I think Elixir and Clojure are alike in that regard.
You might find Gleam[0] a better fit.
[0] https://gleam.run/
I may be wrong, but last time I checked there was not a statically typed OTP implementation which is kindof a bummer. I think Gleam is the ideal implementation on top of the BEAM but it does just seem pretty immature.
If you're only willing to use languages with the same features, what's the point? Learning how a different paradigm manages without types can be more insightful.
Yeah I agree learning new paradigms can give you new insights.
There's also a balance between learning new languages for fun and for the insights they give, and wanting to ship.
As an example: Prolog was mind-bending for me when I tried it and I had a lot of fun with it, but I can't imagine using it to build a product (I'm sure other people have though).
Perhaps my first comment sounded more critical than intended. I'm really excited to see where this initiative with set-theoretic types goes, and if it leads to a fully statically typed language then that will be a bonus. If that doesn't happen, then I'm still perfectly happy with the language as it is.
Elixir taught me that I don't need static types as much as I thought.
I finally found uses for Prolog haha. For years I would have been able to write exactly your comment.
One use is a spellcheck. Though some bits are in Rust cause backtracking would be too slow.
Another is a game I'm making, the server is in Elixir, and I use erlog to basically program the NPCs in prolog. The game generates events and they are processed into facts if they are perceived by the character.
And with that I can have the system generate goals based on stuff like "I havent seen X at the market for 3 days whilst beforehand I saw X every day. Let me go check on X."
I didn't know Erlang started as a Prolog program basically, but it shows cause they fit together like a match made in heaven.
I'll also make the argument that type systems in languages are purely additive rather than orthogonal.
What I mean by that is, I used to write JS. Transitioning to TypeScript didn't alter my mental model of the language.
Likewise for Python with type annotations.
The only time I've had that happen is with Scala 3's dependent types/type lambdas, but thats LITERALLY called "type-level programming", so it makes sense.
I wonder if it should read "Elixir taught me that I don't need static types as much as my professor taught"?
Because the BEAM has much more to it than a terrible dynamic type system?