TypeScript, but for Elixir?

At my last company we migrated a large codebase from JavaScript to TypeScript – basically it just amounted to adding a couple of lines to the file, changing the extension to .ts instead of .js and then going through and making sure all your code now adhered to the slightly stricter standard. We also had a separate directory where we had some type-specification files that we had to create for some of the more complex types that we used.

Is there a similar layer that it is possible to add on top of Elixir?

Maybe, @spec and dialyzer. See also: How do typespecs / dialyzer compare to Typescript?

Thanks - that was an interesting thread. There seem to be mixed reviews on Dialyzer.

I’d also be curious to hear someone spell out what the upsides are to having Elixir be a dynamically typed language rather than a statically typed language.

All I hear about are the downsides of dynamic typing, but surely there must also be some upsides (other than merely slightly shorter lines of code)?

Upsides to dynamic typing:

Consider the case of launching Task.async within a GenServer and catching the result using the info/2 callback. How does a static typesystem correctly type this situation?

4 Likes

There are few things, which need dynamic typing: modules defined at runtime, hot code updates, ducktyping (if if quacks it’s a duck), …

But the most important one is (distributed) message passing.

You could also look at gleam and what it can’t statically type.

6 Likes

At this point it’s pretty much known that BEAM code cannot be 100% statically type checked. But I do wonder what’s the problem in having static checking in 95% of the places and just use more defensive dynamic dispatch code for those 5% that can’t be typed (speaking about internal implementation of course, not about manual coding in Erlang/Elixir/etc.)

3 Likes

Isn’t that essentially what gleam does or at least tries to do?

From the (not much) of what I’ve read, yes. Still curious if they deal with macros or metaprogramming in general?

People have been trying to add types to the BEAM since at least the 2005 timeframe, and it always seems to fail on the same few corner cases. (Dynamic Upgrades, Message passing etc)

I believe the main developer of Gleam posted on here not too long ago that they do not handle macros yet in Gleam. They have an open issue on Github for discussing a macro system:

1 Like

For a start, dynamically-typed Elixir… exists.

There are statically-typed alternatives on the horizon, but they aren’t done yet: Gleam looks to be moving forward at a good speed in public (first release of the actor stuff dropped back in October), and there’s something over at Facebook working in private.

On the flipside: what would “full” static typing bring that Dialyzer doesn’t? Even Gleam has a Dynamic type for when it can’t decide / can’t verify the type (for instance, in the raw OTP plumbing).

1 Like

Exhaustive pattern matching. I spent several months in Rust land and now I get anxiety working with Elixir code because it doesn’t have it.

2 Likes

(Note, the following is in jest but I still mean it :smiley: )

You shouldn’t need to. That is why you are on the beam. It is meant to crash on patterns you don’t care about. It makes the code cleaner to only handle happy path and not litter the code with code that should not happen. :smiley:

What do you do when you don’t handle one of 132 error codes from the socket module? Probably a catch all which swallows and logs the error or returns it to someone who has even less knowledge how to handle it. Instead the right thing to do is to let it crash and let the expert supervisors handle it. After all. It is their only job.

If it turns out that clause is part of your domain, then you add it and handle it accordingly.

(Obviously, this is something I would only recommend in beam languages and not to be tried elsewhere (until they have an equally good story on failure handling))

8 Likes

Ah, I don’t disagree. It’s just that languages that don’t have the guarantees of the BEAM / OTP are better off being more conservative because f.ex. for a Rust program such an error condition means an irreversible crash – and even if many big-ish projects use Kubernetes where crashed pods gets restarted, I’ve seen those scenarios unfold in practice and it’s not pretty: the app’s availability is severely reduced and logs are filled with the same errors until somebody actually does something about it.

So yet again it’s tradeoffs, right? But I know I wouldn’t mind still having proper sum / algebraic types with exhaustive pattern matching one day. It definitely improves readability of the code even if the benefits of having them in the BEAM are not big otherwise.

It’s just trading failing at compile time for failing at run time. It should be possible to handle the bad path in Rust in such a way that is efficient at runtime. I love almost everything about Elixir so far, but I do miss the type system of Rust and even Elm.

1 Like

I’ve never actually worked in a statically typed language before. At this point, it’s become one of those things where I’m afraid that if I do, I will feel the pain @dimitarvp does which will be annoying because I really like Elixir :upside_down_face: I am someone who thinks a lot about types and contracts and I make heavy use of the type-hinting features in Elixir. Would it be nice if my editor could immediately tell me I’m going to have a compile error? Yep! But …well, ok, I don’t actually have a retort to that…

If you want to read some good, level-headed championing of dynamic languages (that doesn’t do so by dumping on statically-typed languages), read Sandi Metz’s Practical Object Oriented Design in Ruby. Ya, it’s an OO book about Ruby, but Sandi Metz is awesome and some of it can apply to functional (in fact a lot of her advice equates to “be more functional”). She deep-dives into duck typing (mentioned earlier by @LostKobrakai) which is really nice. There is also a really good chapter on testing. Otherwise, the chapters on inheritance, mixins, dependency injection, factory pattern, etc… got me thinking about how ridiculous OO can be—but that’s beside the point.

1 Like

I worked in Java for years, so I suppose I’ve had some exposure to a statically typed system.

One downside is that you end up with a lot more boilerplate code and your code is less concise and therefore less fluent from both a reading and a writing point of view. That was certainly the case in Java. I haven’t worked in Java for over ten years now, but back in the day you couldn’t even write a hello world program without five lines of boilerplate public static void main... etc. I really came to hate Java and to dislike programming as a result.

Once I found Python, though, I decided to give programming another job, and that ended up working out well. Hello world in python is just print "hello world" which I almost couldn’t believe when I first saw it, given that I was coming from a Java world. Python frankly made a lot of sense to me.

Pretty soon, though, I had to actually figure out JavaScript. I can’t say that I really understood programming before I really took on a hearty JavaScript project.

Now I’m on to Elixir and honestly, this pure functional approach feels to me like the best yet. I came to really appreciate the extra security that TypeScript provided for a complex JavaScript project, but the more I think about it the more I think that the concurrency environment of the BEAM and the immutable-data feature of functional programming perhaps more than make up for the any downsides to dynamic typing in Elixir. I did try doing multi-threaded programming during my Java days and it was one of the biggest headaches of my life. Just utterly painstaking tedious debugging problems that lasted for days. Somehow Java’s static types didn’t save me from that.

Back to JavaScript, though – I’m wondering now whether JavaScript may be a special case – although it is the most widely used language in the world, I gather that it is not often praised for it’s design. Perhaps JavaScript is unique in the degree to which it benefits from a typing layer being added on top.

Incidentally, object-oriented programming made sense to me when it was explained to me in school and it seems to make sense when it is explained in toy situations – “a dog and a cat are both mammals! So we can inherit…” etc – but in practice when I worked in Java all the types I used seemed like they didn’t relate to anything much in the world and instead had to do with various parts of the Java system – and I came to rely heavily on javadocs integrated into a heavyweight IDE. Now, however, I’m a convert to the functional perspective. This essay by Steve Yegge really puts a finger on how much I despise Java and how much better I think a functional programming approach is: http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html

With Elixir, I’m honestly surprised that it hasn’t caught on more – the entire system seems like it anticipated the needs of the internet of the 2020s back during the 1990s when it was being applied to telecom switches. So it seems to me that Elixir+OTP+BEAM ought to be the preferred tech stack from probably 50% or more of tech startups right now and I’m just not sure why it isn’t.

1 Like

I think the way OO is taught in school is a mistake. The “a dog and a cat are both animals so we can inherit” makes people think that “the thing” about OO is inheritance. Really, thing about OO is encapsulation of state through the combining of behaviour and data. I haven’t used inheritance in my OO work in many years. In my professional life I’ve worked on ecom, procurement, warehouse management, and scheduling systems and none of those have ever lent well to inheritance. I think it makes sense for something like a video game where you have a lot of characters who all have common characteristics—like health, mana, a walk cycle, etc. But in an ecom system, I’m not going to have Milk, Bread, Egg that all inherit from Product, I’m just going to have a Product with a name. As I’ve heard mentioned several times: “Functional excels when you have more behaviour than you have things, and you are mostly adding more behaviour than things—OO excels when you have more things than you have behaviour, and you can mostly adding new things.”

There is a lot to respond to in what you wrote, but my dog is barking for attention :dog:

4 Likes

Main downside for working with Rust for me was the reduced speed of iteration. With Elixir I could literally just go to iex and sketch my idea in a few lines of code. With Rust you have to create a new project, edit the main.rs file and introduce logging library or just directly print to console – or write your functions and then write tests for them. Rust has come a long way and its tooling is absolutely excellent, mind you, so the whole thing still happens very fast.

Rust is encouraging very good practices, no doubt, and it’s making you think twice about stuff – which is very valuable. But sometimes you just want to quickly check if an idea works well or not. In these cases I found Elixir to be a better experience than Rust. But to be fair to both sides, Rust is catching up with frightening speed in every single area – except OTP guarantees.

Comparing the TS+JS with Elixir can be a bit misleading. The main reason TS provides such a benefit in the JS world is two things: the poor tools for on offer structuring code (in Elixir we have modules, processes, apps-as-deps, etc.) and the fact that it is weakly typed. Static typing helps with the weak typing as it helps provides static analysis to do what the runtime really ought to be doing. It’s a (very) imperfect solution, but TS goes a long ways to addressing the weak-typing issues of JS.

Elixir (or, rather, the BEAM) is strongly typed. This is what enables guards such as is_integer and why we get runtime errors when using the wrong sort of data (e.g. passing an integer rather than a string to Integer.parse/2). Static typing allows one to write code that is far more predictable and can defend where needed against odd / bad input.

On the other side, static typing typically means more boilerplate / verbosity and less generic code. But it can also allow for more performant code generation by giving further hints to the compiler, catch bugs that would otherwise slip through … the exhaustive pattern matching of Rust has already been mentioned, and that can be quite useful in some cases.

I think it is instructive, however, that many statically+strongly typed languages are moving more and more to allowing for type inference and generic programming that makes the language look and feel a lot more like a dynamically typed language (C++ an Java both being examples there).

We do have dialyzer and it is pretty decent, particularly for a (mostly) functional language. The function is a good unit of typing for Elixir … buuuut … it is slow (I suspect this is more an issue of implementation rather than due to any inherent weakness in function annotation), its error reporting can be pretty obtuse at times (though that’s an attribute of many static type systems, particularly older ones), and it doesn’t actually enforce any contracts in code (it is documentary-only … change the function? you need to also update the @spec, not that the code will care and often enough dialyzer won’t tell you about it either …). So it’s an imperfect system.

I don’t think a whole new language on the BEAM to introduce static typing makes any sense whatsoever, though. Static typing is not nearly a good enough reason to pick a language. It is at best a tie-breaker between ‘equally useful’ options. The amount of highly reliable code being written using Elixir demonstrates that the margins for improvement are not huge.

Personally, I’d love to see a more integrated version of @spec that has better performance characteristics. The type inference dialyzer has is already pretty decent, it’s more about QoL and integration.

Just my 0.02 … :slight_smile:

6 Likes