Elixir and Rust is a good mix
fly.ioI made the ‘risky’ decision to learn Elixir & Phoenix for a 6 week University project that I delivered earlier this week. It could have turned out to be a terrible decision but honestly once I got my head around it, it’s probably some of the most productive coding sessions I’ve ever had. Deploying to Fly.io was also amazing, I think it was 3-5 commands from signup to deployed with a database. Very happy with it all.
terraform init && terraform apply is 2 commands!
slightly /s
kubectl apply is 1 command!
ansible-playbook is 1 command!
sh ./do-the-thing.sh
you get it
I used to use Elixir, but the lack of static types got to me (especially since I prefer the type-driven development methodology). Using Rust afterwards was great, plus it was faster than the BEAM. I guess, why not use Rust entirely instead of as a FFI into Elixir or other backend language? I've been using Axum and it works pretty well. The only time I had to do FFI with Rust was with Flutter via flutter_rust_bridge, for running a CRDT library (automerge) that was implemented in Rust, for offline app functionality.
> Using Rust afterwards was great, plus it was faster than the BEAM. I guess, why not use Rust entirely instead of as a FFI into Elixir or other backend language?
Sure, you just need to reimplement light-weight threading with preemptive scheduling prioritizing latency over throughput, extremely robust fault tolerance with a supervision hierarchy, and runtime introspection with code hotloading capabilities. Maybe you could add frictionless distributed system support as well.
No big deal, why not right?
This list isn't long enough. OTP and Elixir are wonderfully unique. Rust is, however, as well. I don't think that anyone who truly understands the two could comment about using one as a default.
Is any of this needed though? For some use cases yes, but for many no. It's possible the parent has no practical use for these capabilities.
If you're building a project that actually benefits from Elixir/Erlang's features, you can have a constructive discussion about whether those features justify sacrificing static typing. If you don't actually benefit from those features, "I tried to use a language that's unfit for my purposes and found it unfit for my purposes" is a comment of very limited value.
I'm not a Rust programmer, but:
> light-weight threading with preemptive scheduling prioritizing latency over throughput
Rust has Tokio for light-weight threading which might well be sufficient for the majority of use-cases.
> extremely robust fault tolerance with a supervision hierarchy
One could argue that Rusts compile-time guarantees together with something like the Result-type make it so that such a supervision hierarchy isn't quite necessary and a few "manually implemented" error-boundaries are sufficient. This is also true for errors like network-hickups.
> runtime introspection with code hotloading capabilities. Maybe you could add frictionless distributed system support as well
Fair enough points.
I don't think your attitude against the OP is justified though.
Come on, Rust has a good type system, but it’s nothing special - Haskell has the same guarantees since decades and it’s not like Haskell programs are suddenly without fault. There is only so much static types can catch without dependent typing, as per Rice’s theorem, types are a ‘trivial property’. Most of the interesting stuff happens at runtime and those are much harder to reason about.
Just because you have a Result type doesn’t mean you actually properly/meaningfully handle the error at all, it may just happen that “restart” is the correct solution. Also, Rust is not safe from dead/live locks and many other concurrency issues, only data race free.
This is not against Rust, but against the very biased hype for it.
> it’s not like Haskell programs are suddenly without fault
I never said that. But would you claim that Haskell programs are generally more faulty / less stable than Erlang programs?
> Just because you have a Result type doesn’t mean you actually properly/meaningfully handle the error at all
Okay, but the same is true for Erlang and the BEAM.
> it may just happen that “restart” is the correct solution
Yeah, but is very easy to do with Rust and Haskell or even just on the infrastructure level (i.e. restart the failed container/instance).
> Also, Rust is not safe from dead/live locks and many other concurrency issues, only data race free.
How is Erlang safe from those things in a way that cannot or only with a lot of effort be replicated when using Rust?
> > Also, Rust is not safe from dead/live locks and many other concurrency issues, only data race free. > How is Erlang safe from those things in a way that cannot or only with a lot of effort be replicated when using Rust?
Each BEAM process (other runtimes would call them preemptable green threads) has its own heap, communicates with other processes by messages, and only works on immutable data without the involvement of Native Interface Functions (NIFs). There’s no shared memory at all. It is natively massively concurrent without most of the risks involved. Sure, it’s possible to write code that results in mailbox deadlocks, but it also means stepping outside of normal program design for the BEAM, and it means stepping outside of OTP (which provides structured programming constructs like gen_server).
Rust makes it harder to program without memory safety. Erlang (and other BEAM languages) make it harder to program without concurrency safety.
Edited to add:
Joe Armstrong’s thesis on Erlang is available for download and is written in very accessible language[1].
If you want more, you can also read Programming Erlang (2nd Edition)[2] also by Joe. (I would love to see a 3rd edition tackled by someone to address the newest stuff added in the 9 years since the last publication. I can understand why no one would want to approach this, since it was Joe’s.)
[1] http://erlang.org/download/armstrong_thesis_2003.pdf [2] https://pragprog.com/titles/jaerlang2/programming-erlang-2nd...
> Each BEAM process (other runtimes would call them preemptable green threads) has its own heap, communicates with other processes by messages, and only works on immutable data without the involvement of Native Interface Functions (NIFs). There’s no shared memory at all.
Is this related to my question?
I can see that this helps to prevent (or ease) out-of-memory errors. Other than that, what's the difference to using Rust's green threads, given that the developer knows what they are doing (but are still human and can make mistakes of course)?
Maybe a concrete example would help me (and others) to understand the difference.
> Other than that, what's the difference to using Rust's green threads, given that the developer knows what they are doing (but are still human and can make mistakes of course)?
An analogy: The BEAM process model is to "developers know what they are doing" as the Rust borrow checker is to "C developers know how to write memory safe code."
In other words, your program will have bugs. Your program will not correctly handle every failure mode. Your program will fail. The BEAM process model makes it so that a failure in one process won't take down all processes. And furthermore, after a process fails, there's always a deterministic way to recover from that failure without you the programmer having to think about it too hard.
The system is so robust that having processes fail on error conditions is encouraged. Once you really internalize this "let it crash" way of thinking and writing code, you only program for the happy path and let the process system handle the rest. The code ends up being much shorter and easier to understand. It's the complete opposite of writing code in something like Go, or I imagine Rust, where you explicitly handle or punt every error you can think of at every step.
And interestingly enough, despite not handling errors at every step, the share-nothing process model ends up being much more resilient in the face of errors.
> The BEAM process model makes it so that a failure in one process won't take down all processes
Yeah. But the deveveloper still decides on how many and what processes there are. They have to understand the concept of a process and spawn them accordingly.
The same is true for Rust Tokio (and similar solutions) as well - you have to create tasks and manage their lifecycle.
For example, if you were to implement an http server, you'd have to use one erlang process per request so that if something goes wrong it only impacts this request and doesn't kill the server. In Rust, you would create a Task (green thread) per request as well, which then (if it fails) will not impact that Task that is "supervising" and creating those per-request-tasks, no matter if the request fails for a "valid" reason or because of a bug like an endless loop.
And even if there is memory and CPU resources (even OS threads) shared between those tasks, they are logically separated and for the developer it only matters in very rare cases (such as OOM errors).
I'm not saying that you get exactly the same level of fault tolerance or convenience with Rust here but I also don't see the fundamental difference. Hence, I feel your analogy would only make sense, if the developer has to work without a Task/Greenthread library.
It is completely the answer to your question.
Note that Rust does not have green threads (RFC 230: https://github.com/rust-lang/rfcs/blob/master/text/0230-remo...), so without using the coroutine crate (which most developers don’t know how to use; the truth is that most people don’t know how to use threads).
The features that I talked about have nothing to do with preventing out-of-memory errors—they don’t really help with that. For non-external resources, the features described prevent memory contention (no shared memory).
Much like it’s hard to understand the Rust borrow checker quickly, the "example" you’re asking for is not possible in a comment on HN. I recommend looking over Joe Armstrong’s thesis, for which I provided you a link.
> Note that Rust does not have green threads
Rust has Tokio, which does have green threads.
From https://docs.rs/tokio/latest/tokio/task
> A task is a light weight, non-blocking unit of execution. A task is similar to an OS thread, but rather than being managed by the OS scheduler, they are managed by the Tokio runtime. Another name for this general pattern is green threads.
Hence I don't see a fundamental difference here.
Tokio seems to be a valid replacement for lightweight threading in elixir.
Fault tolerance and supervision hierarchy might be unnecessary as mentioned.
Hotloading capabilities are unnecessary, most shops go with blue-green deployments, so the hotcode loading is usually unused (and for a good reason, so much complexity!). Distributed computing also goes unnecessary as most applications are deployed with containers with some form of autoscaling, so the industry went a different direction than elixir.
That leaves us with runtime introspection, which is pretty cool indeed. But that has to compete with Rust performance.
Pretty tough.
I love elixir, but when Go got premptive scheduler for goroutine, the need for elixir dropped dramatically. Which is sad because i loved the language and phoenix.
I'm hoping it makes a comeback, though!
> Fault tolerance and supervision hierarchy might be unnecessary as mentioned.
It's not like there is suddenly no fault tolerance though. Erlang has a certain way of handling/dealing with errors/faults and so does Rust and other languages. I would not by default assume that Erlang's errorhandling is superior.
> That leaves us with runtime introspection, which is pretty cool indeed. But that has to compete with Rust performance.
I would much rather say that runtime introspection has to comete with a static type system.
As I said many times, I really like the BEAM (saying that after having worked with Akka quite a bit) but Erlang/Elixir... those languages are really not great. There are many languages that way better. I also know that there is a new one for the BEAM (forgot the name) but so far we are mostly stuck with E&E.
The reason why I haven't brought it up is because they are not the same thing, so they can't compare. Of course rust type system is great, but at the same time the memory management is not something necessary to most software.
Elixir could teach some really good practices to people writing code, which is why I'm somewhat sad that other languages can supersede it (go, rust).
> Elixir could teach some really good practices to people writing code, which is why I'm somewhat sad that other languages can supersede it (go, rust).
My problem with this statement is, that it emphasizes the language (Elixir) but what you really mean is the paradigm of using the BEAM no? As in, using Erlang or Elixir doesn't really matter for those good practices, even if Elixir is nicer in some regards.
In that case, it should be written like that, otherwise it will confuse people and prompt them to disagree, like I did.
No, I don't think so. Sure the language is structured to work with beam, so the line is somewhat blurry, but in my mind I was referring to practices that the language itself promotes:
Just things you figure out as you write elixir code- no state, unless when you really need it - avoid mocks unless you really need - pipeline-style approach where writes happen only at the end (io at the edge of the system) - prefer integration tests
Implementing all of that, without using a significant amount of memory for every step of the way to boot. It's impressive how little memory an Erlang process needs.
> light-weight threading with preemptive scheduling prioritizing latency over throughput
Tasks for backend systems are usually pretty homogeneous. I'm not sure how in such cases the overhead of preemption is in any way better than cooperative multitasking.
Erlang processes indeed do cooperative multitasking under the hood, something like yielding control to the scheduler roughly every 1000 function calls.
Rust doesn't have a lot of good runtime introspection tools (or they're very not obvious). If you're running a system with a lot of concurrency, it's nice to be able to attach a debugger and find out exactly what's going on with each of your tasks.
I haven't seen hot loading for Rust (but a quick search shows there's some out there), and I'm not sure how amenable Rust is to dlopen and friends to force the issue.
Erlang (and Elixir) have a constrained language that allows for BEAM to be effectively premptive in a way that a Rust concurrent runtime can't be. At every function call, BEAM checks if the process should be preempted, and because the only way to loop is recursion, a process must call a function in a finite amount of time. A Rust runtime cannot preempt, if you need preemption, you've got to use OS threads, which limits capacity, or you need to accept cooperative task switching.
Also, some of us are as anti-typing as you are pro-typing. :)
>Also, some of us are as anti-typing as you are pro-typing.
Assuming ample experience with both, how does one reach this conclusion?
I have yet to see a project of any size that needs to be worked on by multiple teams and is written in an untyped language not descend into dumpster fire.
I work on a lot of 'glue' issues, often with languages like Perl, PHP, and Erlang (and a bit of Javascript here and there). Specifying types all over the place in languages like C, C++, Java, and Rust feels like it gets in the way and limits more than it helps. (feelings more than data here, of course)
Sure, at boundaries between teams, you need to specify the data in some way. That could be a type, but for me, often the other team is using a different language than me, so it needs to be a language agnostic type, and it can't include unsigned numbers because Java can't cope, and it can't include large integers because Javascript can't cope, etc. Protobufs are popular, json is too.
I have a lot of unpopular opinions though, and that's fine. It's just tiresome that everyone wants to come in and add types to things that don't need them. Also, I agree with dllthomas, most developers and teams are capable of creating dumpster fires in all sorts of environments, with all sorts of tooling. :)
You can absolutely create a dumpster fire in any language.
Putting the fire out in an untyped language is a Herculean effort.
In my experience it is even harder in a typed one because now you have to deal with the type system nightmare they built. So the compiler fight your refactoring.
You never actually get away from types, they are a core requirement of using any data beyond raw bytes. The guarantees that a strong type system provide mean you can be certain about certain things before your program even runs. If that's a problem for you, you're likely just leaving bugs on the table to be discovered at runtime.
I’d modify your statement to say "strong, static type system". Strong and weak typing are orthogonal to dynamic and static typing. JavaScript has weak dynamic typing; TypeScript has strong-ish static typing sitting on weak dynamic typing. Ruby and Erlang/Elixir have strong dynamic typing. Rust and Go strong static typing (Go’s is weakened by interface{}, IMO, but it’s a valid choice).
With the way that Erlang and Elixir pattern matching can be used in function heads, I can have much the same feeling of certainty that people express from Haskell and Rust. (Erlang typespecs help here, but are not checked by the compiler itself, only by additional tools like dialyzer or gradualizer.)
I'll admit, I do not know Elixir very well. As long as it's checked at compile-time or checked across an entire application at startup/import/init, I think it serves the purpose well. The problems I want to avoid is discovery at (production) runtime the shape of data doesn't match what my functions expected.
Nobody code without a type system. The distinction is build time vs runtime type checking. At build time you catch bugs that would appear later at runtime. Which is more costly to fix later.
> Which is more costly to fix later.
This assumption is changed, IMHO, by Erlang. Hot loading makes the cost to make small changes very low. So the question becomes, do you pay the definite cost of build time type checking (usually includes coding time type annotation), or do you accept the possible future cost to making small fixes.
Of course, if you work in an organization where even a small fix requires months to release, then do all the things you can to prevent making small mistakes.
The cost at runtime also includes loss of data, inferior user experience, direct financial loss or even loss of human life for some systems.
It really depends on the domain but it definitely is more than pushing an update.
The cost is also in debugging. It’s much harder to figure out a problem after the fact because you’ve forgotten how the code in question works. If you catch a mistake while you’re coding something up, you can fix it immediately and not give it a second thought. But if you need to track a bug down weeks or months after writing the code, it can take a lot of work to figure out what the code does (and why), and why it is behaving incorrectly.
I’ve lost weeks to a memory leak once in javascript that was a 2 line change to fix. If I realised the problem when I wrote the code, I would have saved myself a lot of trouble.
Essentially every statically typed language contains runtime errors that can be costly too.
Erlang was designed to achieve 9 nines of uptime. It achieved this without static typing across very large applications. The fact that this is a regular occurrence with Erlang disproves the idea that a lack of types is fundamentally unsafe.
Static types are most useful with monolithic application design. They counter your massive ball of code growing too complex and the complete lack of introspection at runtime. They attempt to handle the problem that any error crashes your entire system.
Erlang uses a different approach.
First, it uses safe datatypes. You aren’t going to crash because you chose a 32-bit integer and rolled it over (something a type system won’t actually help with). This is maximally likely to corrupt user data without even raising any flags. Integer rollover has actually killed people (famous in the Therac-25).
Second, it uses all immutable data, so data sharing is safe. Also something types don’t help with. This is also a maximal risk of data corruption. Incorrectly mutating data has also killed people (also in Therac-25).
Third, it is functional. Toys reduces passing around giant balls of mud. Those balls are unusable disasters unless you add some types. Functional programming with immutable records means that a program can’t accidentally change the types of incoming data. Because the pattern encourages separating data and mutable state, the most common typing accidents are simply avoided.
Erlang is designed with concurrency first. This helps to keep those balls of code even smaller and further reducing the chances of typing errors. And of course, combined with immutable data, we eliminate another set of errors typing does nothing about and that have caused massive damage and probably deaths (a deadlock causing a NYC blackout leaps to mind).
Finally (I probably missed some points), Erlang is designed expecting crashes to happen. Few runtimes are capable of anything close to the elegant Crash handling of BEAM. Instead of fearing crashes, you understand they’re inevitable and embrace them. This means that you are prepared for not just an occasional type error, but will also elegantly handle null exceptions that plague most of the most common statically typed languages.
> Nobody code without a type system.
A small number do. Assembly languages are generally untyped. The Forth language is also untyped.
> At build time you catch bugs that would appear later at runtime. Which is more costly to fix later.
Generally agree. Programmers proficient in Haskell or Ada tend to consider types to be integral to their development process. The real question is whether this is a good tradeoff against development velocity, for your given project. Neither language markets itself for rapid application development, instead they tend to emphasise that the language aids with correctness and the ability to reason about code's behaviour.
Well no since unit tests run faster than build time.
Conway's Law means you have to fix the organization before you can fix the software. That's the real Herculean effort.
Test coverage is, in my experience, much more important factor than typing. A codebase with great testing is much easier to aggressively refactor/change whether typed or not.
That said, a dumpster fire usually has no or little tests, so maybe we're arguing non-existent hypotheticals :|
I’ve gone back and forth over the years on whether tests are a good enough replacement for types. A few thoughts:
- Types and tests find different bugs. I’ve found new bugs by converting a project from javascript to typescript. The project in question had a 2:1 test:code ratio but as soon as the typescript compiler could read it, it spotted a couple obvious errors.
- Large test suites often make refactoring harder, not easier. If you have a clear, fixed API boundary and your tests test that boundary, then testing helps. But most refactoring also involves changing up those APIs as well - since bad APIs are often the reason you want to refactor in the first place. When you do that, you have to also rewrite all your tests. Good type systems help refactoring. Writing rust in Intellij, I can globally rename functions and types in my project, promote tuples to structs, reorder function arguments, and all sorts of other handy refactorings. My tests get updated too. And the compiler tells me immediately if I missed anything, without needing to rerun my tests.
- Reading the types is my favourite way to get up to speed on a project, or get back up to speed on something I wrote myself that I’ve forgotten. "Show me your flowchart (code) and conceal your tables (type definitions), and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." -- Fred Brooks, The Mythical Man Month (1975)
- I find I need far fewer tests to write reliable software when I’m using a language with a good type system. Most rust code I write works correctly once it compiles. Javascript is easier to write than typescript, but it’s harder to test and debug.
So with all that, I’m personally in camp type these days for most software. I think it’s usually the right choice.
> If you have a clear, fixed API boundary and your tests test that boundary, then testing helps.
A clear, fixed API boundary is exactly what Phoenix tries to encourage with contexts. Unfortunately, a lot of developers find them hard to understand. They're simple if you read up on DDD but again, a whole host of developers won't, or don't, do that either. LiveView in particular has a really a really great testing library [0] where you can write what are essentially end-to-ends that never touch even a headless browser. Since I'm always writing LiveViews, I pretty much only write LiveView tests and contexts tests which gives me large coverage (also some unit tests for the odd utility function). Otherwise, it's really important when writing non-typed functions to make it really obvious what is coming in and out, which is arguably a nice forcing factor.
The number one thing people bring up when shilling types is large codebases (it's been brought up in these comments). My opinion there I have found is quite unpopular and that is that pair programming should be far more prevalent than it is. I think the whole notion of "just stick a junior on that" is broken and I don't understand how types make that situation _that_ much better.
All said and done, I'm not actually anti-type. I mostly just find them to be incredibly noisy compared to a well-written function. I really like Ocaml where it's statically typed without needing to actually specify them.
> I mostly just find them to be incredibly noisy compared to a well-written function. I really like Ocaml where it's statically typed without needing to actually specify them.
Yeah; I haven't worked with ocaml but I've done some haskell (where you think about types so much more). Personally I don't mind rust / typescript's approach of needing types at the function boundary (function input & output types must be specified) while doing inference wherever possible inside each method. As an example, here's a very complex function in a project I'm working on chosen vaguely randomly[1]. The function diffs a run-length encoded DAG using a breadth-first search.
Visually scanning for types, there's a couple at the top of the function - both in the function definition and the BinaryHeap:
But I think thats about it. Maybe there's more manually specified types in "normal" rust because most functions are smaller than that. But, it doesn't feel so bad. In this case I could probably even remove the explicit type annotation for that queue definition if I wanted to, but it makes the compiler's errors better leaving it in.let mut queue: BinaryHeap<(LV, DiffFlag)> = BinaryHeap::new();[1] https://github.com/josephg/diamond-types/blob/66025b99dbe390...
> I've done some haskell (where you think about types so much more)
You definitely still think about types in Ocaml, you just don't need to annotate due to the language design. A big part of what makes it possible is that there are no overloaded operators, eg, you can't add an int and a float without casting as the mathematical operators are different: `1 + 1` v. `1.0 +. 1.0`. While I've dabbled in both, I'm no expert in either Ocaml or Haskell, though.
Really for me it's just that I've never felt the pain as I haven't worked in a big enough project, I guess. There is something that just kind of annoys me about (pseudocode): `(name : string) :: string -> "Hi, #{name}"` because, like, no shit it takes a string and returns a string! It's a death by a thousand cuts thing where I don't want to read that stuff and the compiler doesn't need to be explicitly told that in order to do static analysis.
Anyway, again it's really not the end of the world as I'm not anti-type. I just don't yearn for them in Elixir or anything. If it had a solid typing system I even might use it, but I don't yearn for them or anything.
You have some really interesting projects on your github, though! I mostly build glorified CRUD web apps! I do always get a sense that a lot of the type-talk is centred around organization disorganization.
have you tried doing it in elixir? It's not that bad.
I think that glue-issues are especially well captured by languages with good static type-system. Moreso than in languages without static types, because when it comes to glueing, there are many things, especially errors, to consider that can easily be forgotten without the help of a compiler.
However, a language with an insufficient type-system indeed makes things harder than they are without it. I would count all the languages you listed into this category.
As another poster mentioned, typescript is fairly expressive. There are other (production) languages too, such as Scala or maybe D. And there are lots of academic/very-niche languages.
> It's just tiresome that everyone wants to come in and add types to things that don't need them
Well, types are there, if you like them or not. There's a reason that you have e.g. typeof in javascript, gettype in PHP. The question is rather if you explicitly annotate them or not. But yeah, sometimes it's not helpful to annotate types, especially if the language is incapable of expressing the correct type anyways, which is true for most programming languages.
IMO, TypeScript strikes a great balance here. I loved the way I could cast something to `any` when hacking something out, then add proper type annotations once it's ready to be productized. It also a did a good job with type inference.
Disclaimer: I work at Microsoft, but not in the Developer Division
I always wanted to ask someone whose “native tongue” is untyped languages — when you reason about code, what do you think of an object, is it of specific type? Nominal, or more like structural typing that you know that it has to have this and that method?
I have started programming in untyped languages, but simply can’t remember back at all, and now I can’t really imagine dealing with objects in my mental medal as not having some type.
Note: this is not a rebuttal for/against dynamic typing, I do think that types are really important at boundaries, but they may not be the silver bullet - contracts may be better at some things, for example. This may be an open question.
I generally think about "types" in terms of capabilities more than shapes. When I write Ruby, I don’t generally think "this parameter must be an array". Instead, I think "this parameter must be an Enumerable". Or I think "incoming objects must behave like strings" (that is, they implement #to_str or are Strings)…although most of the time, I would really think "incoming objects must have useful string representations".
In Elixir, I do think about shapes more than capabilities (because Elixir is not OO), but with pattern matching, I can either specify "this must be a MyApp.Account struct" (which is just a fancy map) or I can specify "we will handle any map that has the keys X and Y, and Y must be a map itself".
I replicate this more formally when writing TypeScript, usually by building up type definitions and specifying those.
I’ll first say that my favorite languages are StandardML (very strongly typed), Common Lisp (not very typed), and JS (even less typed).
Look at Erlang. It has bigints, floats, Booleans, but-strings (sequence of bits —added because it is so common in telecom), string (not technically a primitive data type), functions, atoms, list, tuple, and map.
None of these look the same or act the same. People dream about seeing `123 == myMap`, but it simply isn’t a common thing because it doesn’t make sense.
The common rebuttal becomes: but how do I know if property X is a string or number when I’m using it?
If that’s your question, you are already messing up. What you really want to know is what X actually represents. Otherwise, you’re just shooting in the dark which is at least as dangerous as getting the type wrong and probably more so because a wrong type will become obvious quickly while mangling that number or string may not be caught until a much later time after serious damage has propagated throughout the data.
Let’s say you have something called `login`. Is it a number or string? If it’s a string, is it an ISO date, UTC date, or something else? Is it when they logged in or when their login expires? If it’s a number, it could be a Unix string. It could also be a calculated value for how long the user has been logged in and could be days, hours, minutes, seconds, milliseconds, or something less common.
How do you know which thing is correct?
In a good codebase, you read the docstring comment on the data constructor that describes what it does. If it says “milliseconds since last login” vs “token expiration using ISO datetime format” do you have any question at all about whether it’s a number or string?
If there isn’t a docstring, you’ll be digging through that code or playing around with the responses and will see the data type anyway.
The result is that you’re forced to better understand what you’re doing which isn’t a bad thing in my opinion. There may still be mistakes, but that leads to the next point.
Dynamic languages generally make it easy to dynamically check the types of incoming data and tend to be more flexible with mistyping (especially JS). I can’t count the number of major errors from common typed languages because they make introspection hard, so programmers don’t do it and crash on malformed data or when an API suddenly changes.
It’s also worth talking about null exceptions. Many dynamic languages expect type weirdness and handle it well. This usually includes null. Most statically typed code out there has LOADS of null exceptions lurking about which only trigger in obscure cases during runtime. In this regard, you could argue that the worse type issues also happen in typed languages, but are more dangerous in those languages too.
Untyped languages also tend to use safe numbers everywhere. Infinity is mostly useless, but not usually dangerous. Bigints everywhere are slightly slower in some cases, but completely eliminate overflow errors. Most typed languages use risky numeric types, so they must also force users to think about those things.
Finally, no common typed language offers good runtime introspection like smalltalk, Common Lisp, Erlang, or even JS. I feel static types are just a crutch to make up for this deficiency.
That brings us to good static languages. Typescript offers all the benefits of normal, unsound static typing combined with all the robustness of JS’s dynamic environment. The same can be said for Coalton and Common Lisp.
StandardML offers a language that feels like a dynamic language, but still offers static checks that are actually sound and completely eliminates null exceptions. Rust (an ML in spirit if not in syntax) does the same things in environments where garbage collection and other such amenities aren’t possible.
I like both kinds of languages and good examples from each word around the problems of each approach to make them (in my experience) about equal in productivity for equally skilled and experienced developers.
I tried Haskell for a while and switched to Common Lisp (although I still follow Haskell from a distance). My experience just doesn't match up with the claim that a project of any size written in an untyped inevitably descends into a dumpster fire. I've worked on largish systems in several dynamically-typed languages and several statically-typed and I personally haven't noticed any major difference in overall productivity suggesting that static types are better: they just have different friction points and different ways of working work better in each paradigm.
This is exactly my experience as well. Code architecture, metaphors, tests, etc have had larger impacts on both initial development as well as long term maintenance and malleability.
Some times, some type systems actually make people jump through hoops to accommodate their design and then it can actually have a negative effect. Other times, the typing helps.
It’s kind of like really good grammar and punctuation. They can make a story you write better and clearer. But they far from guarantee it. You can write a very good story with subpar grammar/punctuation. And you can write a really lame story that is grammar perfect.
One thing that I haven’t seen much in the discussion, is any discussion about Elixir’s matching abilities. Does Rust have that as well? I love what Elixir matching does for my code.
(Edited spelling)
The issue isn't productivity. As far as just slamming out code untyped languages are undeniably faster.
The issue is working on projects once they've reached a certain size where you have no idea what the intent of the original author was and you maybe need to refactor, add-in major pieces, or change anything with the expectation that it continues to work.
I’m including maintenance costs in “productivity”. I’ve worked on large dynamically typed codebases and never experienced what you’re talking about.
I think it’s a question of understanding how to work and think without explicit types rather than something that makes statically typed codebases easier to maintain.
Untyped code bases with microservices are the best code bases out there by far.
They are exceptionally easy to refactor, add-in new parts, etc.
The keyword is microservices, you need to know how to do proper microservices if you are using untyped code.
I dunno. Something like spec, dialyzer, or "assertive" typing (in the case of Elixir) on the boundary works just fine for me.
Microservices just push the problem out of sight — now you need interoperable types between them, there are race conditions, IDEs don’t see inside the black box of other services so refactoring is harder, etc.
They can be the correct solution sometimes, but blindly applying them everywhere is just dumb.
While I'm pretty solidly in the "pro-typing" camp, it seems worth acknowledging that such projects often turn into dumpster fires in typed languages as well, even expressively typed languages.
Maybe, maybe not - but in my experience, a dumpster fire with static types is easier to read and understand and also easier to refactor.
That matches my experience as well. I just think we need to be resistant to reading too much into "I've seen bad code that X", because that can easily be true of almost any X.
You think that statically typed languages don’t often turn into dumpster fires? Not my experience! Even though there are real advantages.
Sure, but they don't turn into dumpster fires because of the types, as untyped projects tend to do
I don't know that that's true. When the types in a program are designed in a way that's sufficiently out of sync with what a program needs to do, you can get a lot of mess working around them.
Can we say that's misuse of the tools? Sure. Is it less likely than things becoming a mess without types? Probably? Even more so as the tools improve and as the people involved know better how to use them.
I have yet to see a project of any size that needs to be worked on by multiple teams not descend into dumpster fire. Typing can definitely help, but it's just a small tool. Java is a pretty typed language and I'm seen some real doozy code bases in Java.
Though that might just as well mean that typed language projects, like those in Java have actually survived long enough to have turned into that.
It is easy to be maintainable at iteration 1 when the requirements haven’t changed 20 times yet.
>I have yet to see a project of any size that needs to be worked on by multiple teams and is written in an untyped language not descend into dumpster fire.
Github? Dropbox? I mean they both eventually went to type hints in their respective languages or migrated to a typed language but for a long time I'm certain it wasn't.
It's worth noting of course that ~Github~ Dropbox was the driving force behind mypy
yes, that is exactly what I meant by "they both eventually went to type hints", but doesn't github use ruby? I think you mean sorbet? Dropbox was the driving force behind mypy (IIRC).
bleh, I meant to say Dropbox.
And I can pretty much guarantee you that some form of dumpster, fire or other was the driving factor behind those moves.
Untyped languages work fine if you use them with microservices.
The only thing you can't do is have both untyped and monolithic at the same time.
for some reason people are able to use huge undocumented common lisp monolithic projects from the 90s without much effort and without setting their computer on fire. why do you think that is? i mean, given your world view, why would people even think about doing this for a codebase that doesnt have static types?
Maybe because the language is not untyped? It has both dynamic typing and optional static typing.
sure but not in a sense that rust is. my point is that it is entirely possible to build good software with substantial code bases in dynamically typed languges and i used common lisp as an example. in fact i dont know of one common lisp code base that turned to a dumpster fire because of typing problems. instead i find the opposite true: old forgotten code can often be resurrected because the language promotes clear coding and interactive introspection
This interactive introspection is usually facilitated by dynamic types and actual types in the software. For example large parts of the Lisp software in the 80s were already written in an object-oriented way, where the code was structured around explicit classes. Means: one sees the classes/methods both in the source code and in the running code. The source is far from 'untyped'.
For example the Filesystem Browser (FSEdit) from 1980 was written in Flavors. It actually uses an explicit OOP system with hierarchical classes.
https://tumbleweed.nu/r/attic/sys78/file?name=lmfs/fsedit.li...
Lisp is not Python or JS.
People use Python and JS, not Lisp.
Dynamic typing in Python and JS + medium-sized project = dumpster fire.
there are plenty of disaster code bases in both dynamic and static typed languages. my cause for mentioning common lisp was use of an example i personally know of where a dynamic language (albeit one with strong and static typing support available) produces some really high quality code that can stand the test of time
I'm just trying to point you in the right direction.
Dynamically typed microservices is where it is at.
thanks, but maybe not
it's certainly possible to write a nif that cooperates with the VM so that its async yield points match up with the VM's expectation of yield points. Definitely tricky to do correctly in C (given that C doesn't have a yield statement, lol, you have to structure it as an awkward tail call where you pickle/unpickle whatever state you want to keep around or unmarshal it from a passed struct). I'm not sure if that's so easy to do in rust.
Sure, you can do that for the code you write, but you can't for the code you call. Any Erlang you write or call will have this property, and most of the ERTS provided C code will as well; either it's trivially finite, it is designed to yield during the work, or it's neither but it hasn't triggered anyone to fix it, with occasional deviations of things that become known issues (like -- was for some time; although it got fixed twice and is now fairly ok).
While Rust doesn't offer this kind of tooling, the JVM and CLR ecosystem certainly do.
As it usually goes in programming, "it depends on your objectives", there are things that are easier accomplished with the ErlangVM than Rust. Also, if you want a language that uses the ErlangVM and has static types, maybe you should take a look at Glean[1].
In my case I prefer to work with Elixir because of the community, as I find easier to work professionally with Elixir than some other mainstream languages, as mostly projects follows the same good practices, use the same tools and have good documentation.
[1] - https://gleam.run/
I think the most attractive part of the Elixir / Erlang ecosystem is BEAM and its ability to manage code, processes and resources in an extremely parallel environment. Rust operates at a lower level and it feels deceptive to just compare the languages.
That said - if you don't benefit from what BEAM has to offer - I agree Rust is a really attractive alternative.
I'm in the same boat with Elixir. I love many aspects of the language, but it borrows the fast-and-loose type ecosystem of ruby.
nil is an especially big problem. Any value could be nil, and this will absolutely bite you over and over. nil even allows you to use square brackets for some reason (some_nil_value[:some_key]) which is a great way to disguise the actual issue.
There is optional type checking with Dialyzer, which is good but has some problems. The warning output can be really hard to read, and unless you're diligent in using it across most of your project, it's not very useful, because you'll end up with 'any' values all over.
I disagree. Any value could be nil, but any value could be 5, too. In fact, nil is just like any other atom. Elixir shines when you lean on pattern matching as much as possible. Your functions should unpack as much as possible within the function definition, frankly the "if" macro is completely superfluous to case and in rare cases cond. You'll find that you match the data you want, and thus reject anything else.
Square bracket dictionary accesses are a code smell, because you should be using %{^key = val} = dict or Map.fetch(map, key) or rarely Map.fetch!(map, key).
If you do that, managing typing in Elixir just boils down to defining structs to differentiate cases where dictionary A and dictionary B contain similar keys but strictly are not interchangeable.
Any errors these techniques could catch would have to happen during runtime, not at compile time, and this is a huge and key difference. In my experience having the type errors being caught at compile time is an incredible boost to productivity. I just write code then hit Ctrl+S and the compiler tells me what I got wrong, instead of all the hassle of having to write functions a line at a time, flip to IEx, run, inspect some intermediary value to make sure it works as intended, while trying to keep so many things in my head at once, which at times can be just overwhelming and highly frustrating.
Elixir is incredibly consistent with how it uses nil. It does take a bit of work -- you have to really pay attention to the convention of fetch! vs fetch vs get -- and often times library maintainers are not... always the best at that... but for all the problems that dynamic languages has, nil in elixir is pretty low on the list. I think I get bitten by maybe one a year, and it causes the sporadic report on sentry, and then I go and fix it, and that's that.
That access acts on nil is unfortunate, but it's necessary for things like get_in.
> That access acts on nil is unfortunate, but it's necessary for things like get_in.
Ah, that explains it at least. Other atoms don't implement the access behavior.
Concurrency: if you used Elixir, then you should understand how different its concurrency offering is from that of std and tokio. Standardized, structured concurrency makes building complex, highly concurrent, stable, low-latency systems achievable by mere mortals. Rust has no such offering, yet, but it may 5-10 years from now.
Runtime instrospection: the ability to log into a running application to inspect its state, start and stop processes
Compile times: elixir compiles very quickly and has tight feedback loops where as rust compiles slowly and has long feedback loops
Elixir also doesn't get in the way that the borrow checker does, allowing a programmer to just get on with work and not become saddled with related debugging.
Ecosystem: data pipeline processing via GenStage, Broadway, Flow -- wow. Rust developers should take note of what can be achieved in Elixir. However, Rayon and crossbeam are fantastic. Elixir cannot compete with Rust on performance in the category of pipeline processing, but it has very high marks in other very important categories that need to be considered for professional development.
I don't think that fault isolation is as compelling an advantage over Rust as it is other languages. Rust makes defense programming a regular part of development, unwrap used sparingly. Faults hardly happen because the program isn't designed to crash. In the rare event that a well-designed Rust program crashes, it's probably managed by an orchestrator that will restart. Both well-designed Elixir and well-designed Rust applications can enjoy very long uptimes if that is a goal.
This is just a starting point of discussion.
> why not use Rust entirely instead of as a FFI into Elixir
Because many times you value fault-tolerance and distribution more than performance.
There's a couple of Rust libs and frameworks inspired on Erlang/OTP in 'best of both worlds' attempts, such as https://lunatic.solutions
I found others like Lunatic before, but cannot remember right now.
I guarantee your life will be simpler with Erlang.
Everyone have different needs. For us, OTP on top of pre-emptive lightweight threads and a REPL in production is a huge winner for troubleshooting, finding performance bottlenecks, and reliability.
> I used to use Elixir, but the lack of static types got to me (especially since I prefer the type-driven development methodology).
You might be interested in Gleam[1].
I used to use some Gleam back before the syntax change, when it looked more like Haskell. I found that rather than using a language with a comparatively smaller community, I'd rather just use something that's well supported, so I settled on Rust instead.
Rust is faster, but that's like saying writing to memory is faster than writing to a database - sure, it is - but the BEAM is designed with something completely different in mind and is still 'quick enough' for handling those cases. That said dealing with elixir syntax is like rubbing sandpaper in your eyes for 8 hours a day.
I pretty much agree with this. As much as I liked the core language, I absolutely hated using anything else in the ecosystem, and in my opinion a lot of that was due to lack of static types. Using Ecto as the main way of interacting with the database was a miserable experience. Part of that was due to skill issue I'm sure.
> As much as I liked the core language, I absolutely hated using anything else in the ecosystem, and in my opinion a lot of that was due to lack of static types.
You might be interested in Gleam[1].
> Using Ecto as the main way of interacting with the database was a miserable experience.
Ecto is prolly one of the best ways to interact with db. Genuinely curious, what other ORMs have you used?
I usually just write the SQL, honestly. For the last 6ish years of my career I've mainly been in the Golang world, so the closest I've gotten to an ORM is a utility to scan rows into structs.
Interesting, I usually just write the SQL too in most projects, but in Elixir I love Ecto because it's so close to the SQL but has an elegant and powerful way to handle interpolated values.
Have you tried sqlc? Different to any other SQL library I've used and suits Go really well
Yes, I like it very much. I've run into some edge cases with it, and I wish there was a setting to change the names of some of the autogenerated code, but otherwise it works very well.
Yeah, hopefully its plugin architecture will continue to develop and make changing things easier in the future.
On the flipside for me Ecto is the perfect balance between ORM and writing raw SQL queries. I've used many different ORMs like Entity Framework, ActiveRecord, Prisma, Ecto - Ecto stands alone.
I believe... technically Ecto is not an ORM but a query builder, which may be why you like it better than an ORM.
> Ecto as the main way of interacting with the database was a miserable experience
That's very common when you don't know DBs. But DB savy developers usually claim the opposite, because the syntax is more familiar.
> That's very common when you don't know DBs.
I'm not a 20 year DBA greybeard veteran, but I'm comfortable enough to write schemas and queries by hand without any issue, and the entire time I wished I could do just that instead of using Ecto.
No one stops you from writing raw SQL with Ecto.
But good luck creating composable SQL with raw string interpolation, which Ecto excels at.
PR reviewers stopped me from writing raw SQL.
Ecto queries are much closer to DB queries, when you use the macro syntax.
But if you come from an ORM perspective and expect the same experience on Ecto, yes I agree it won't please you.
I used to use Elixir. I still do, but I used to too.
There is a great talk from the creator of the language on why static types are not necessary. Of course there is nothing wrong with wanting them and feeling that they are, but I think he (José) makes really great arguments for why Elixir is the way it is. https://www.youtube.com/watch?v=Jf5Hsa1KOc8
I’m with you there. I haven’t played with Elixir but did some Erlang. I really liked Erlang. Worked a bit in Whatsapp’s codebase as well. But IMO it’s def. not as nice as Rust + using a single language is better.
Could also be nice as it means the Rust parts are smaller and so its slow compiler speed isn't as much of a bother. You get the safety and runtime speed of Rust with the fast iteration cycle of Elixir. Best of both worlds type of thing.
I often find FFI more annoying than it should be (ie something always breaks somewhere), I try to avoid it where possible.
> I guess, why not use Rust entirely instead of as a FFI into Elixir or other backend language?
Because Rust brings none of the benefits of the BEAM ecosystem to the table.
I was an early Elixir adopter, not working currently as an Elixir developer, but I have deployed one of the largest Elixir applications for a private company in my country.
I know it has limits, but the language itself is only a small part of the whole.
Take ML, Jose Valim and Sean Moriarity have studied the problem, made a plan to tackle it and started solving it piece by piece [1] in a tightly integrated manner, it feels natural, as if Elixir always had those capabilities in a way that no other language does and to put the icing on the cake the community released Livebook [2] to interactively explore code and use the new tools in the simplest way possible, something that Python notebooks only dream of being capable of, after a decade of progress
But they do not not stop there, the documentation is always of very high quality, even for stuff not coming from the core developers, and they also regularly release educational material that is worth a hundred times a gain in speed.
They've set a very high quality standard and I noticed how much it is important only when I stopped programming daily in Elixir and went back to other more hyped or establish ecosystems.
That's not to say that Elixir is superior as a language, but that the ecosystem is flourishing and the community is able to extract the 100% of the benefits from the tools and create new marvellously crafted ones, that push the limits forward every time, in such a simple manner, that it looks like magic.
Going back to Rust, you can write Rust if you need speed or for whatever reason you feel it's the right tool for the job, it's totally integrated [3][4], again in a way that many other languages can only dream of, and it's in fact the reason I've learned Rust in the first place.
I must also say that the work done by the Rust community looks refreshing as well, if you look at the way rustler works it was very well thought and made writing NIFs, something that seemed arcane and distant, only for the proverbial mad professor to try, a breeze. Kudos to them.
But the opposite IMO is not true, if you write Rust, you write Rust, and that's it. You can't take advantage of the many features the BEAM offers, OTP, hot code reloading, full inspection of running systems, distribution, scalability, fault tolerance, soft real time etc. etc. etc.
But of course if you don't see any advantage in them, it means you probably don't need them (one other option is that you still don't know you want them :] ). In that case Rust is as good as any other language, but for a backend, even though I gently despise it, Java (or Kotlin) might be a better option.
[1] https://github.com/elixir-nx/nx https://github.com/elixir-nx/axon
Have you checked out Gleam? It is statically typed running on BEAM
I admit for a long time this was my primary motivation to learn Rust, but, sadly, I haven't come across problems in years that were CPU bound/where I needed something like Rust... Rustler still looks like a great fit if needed, but, depending on the use case, if I were CPU bound and needed to write my own code/not just use a Rust library, I'd be as or more likely to look at using Zig and Zigler[0], for much faster learning curve, and from what I've read, easier tighter integration into elixir, including I think language server integration. Some discussion here[1] though I forget if I listened to this one or not.
[0]https://github.com/ityonemo/zigler [1]https://podcast.thinkingelixir.com/83
Efficient PLs are useful for latency sensitive applications too, not just CPU bound ones. Though, that doesn't widen the number of cases by much...
I haven't come across problems in years that were CPU bound/where I needed something like Rust
This is where Rust falls short of C#: scaling to the issue at hand. C# can build you a beautiful app at a high-level but also lets you dick with pointers and assembly at a low level. Rust insists on defaulting to pass-by-move and an arcane trait system that hold it back from being usable in large projects.
If Rust had gone for a traditional OOP system, the "everything must be OOP/use inheritance everywhere" crew would have messed up the ecosystem pretty quickly. The traits concept is refreshing and traits + structs encourage composition over inheritance. I think it has been a huge plus for the language and the ecosystem.
I really like the trait system, but “refreshing” might not be the correct word given that it is pretty much what Haskell had for I don’t even know how many years.
Composition is objectively superior to inheritance, and that's all traits are: https://en.m.wikipedia.org/wiki/Composition_over_inheritance
Author here. The rustler team has really made a great developer experience using Rust in Elixir and I just had to share.
Shameless plug & also a point of support for the article: I used that same stack to build https://regex.help/ (more details here https://maciej.gryka.net/building-regex-help)
Neat! You should definitely add a link to the github repo to the main page of regex.help though, because I didn't realize it was open source and I'll use it now.
Github link for others: https://github.com/maciejgryka/regex_help
If you're extending Elixir with Rust, familiarize yourself with precompiled rustler: https://dashbit.co/blog/rustler-precompiled
This is essentially what has gotten me into Rust. Knowing how easy Elixir makes it to offload a piece of my system that would benefit from it without needing to write the entire thing in Rust.
I used this in past, but there were a lot of people saying that NIFs/ports are dangerous to use. Did any of this change or are there tried and tested practices that can be applied? What I used this for wasn't critical app so I didn't mind it going down.
IIRC one threat was Rust sharing memory with BEAM which could exhaust it and cause OOM crash?
I wouldn't use the word dangerous. NIFs and (linked in) ports are a risk, yes. You're loading native code, and you don't have intrinsic protection against it doing naughty things that alter the underlying shared state in unapproved ways. Things like writing to improper addresses, closing improper filehandles, opening filehandles and leaking them, leaking memory, other improper use of syscalls, or just running for too long.
The only proper solution is to audit and understand the code you're running, but hoping it's fine often works too; maybe formal methods, but to a first approximation, nobody uses those. Did you audit all of ERTS and/or OTP? I'm guessing probably not, but it's there to review if you run into a problem.
IMHO, it's not worrying about if BEAM will crash; worry about it not crashing instead. If your Rust NIF ties up a scheduler with an infinite loop, that has the potentially to lock up the whole BEAM once another scheduler needs to do something that requires full cross scheduler coordination.
BEAM can certainly crash on OOM; although I recommend setting a ulimit to ensure it will, because when it crashes, you can recover. I've also run into situations where instead of crashing or being killed by the OS OOM killer (which is close enough to crashing), the OS gets into some tricky to debug state where your application is neither functioning nor killed. Sometimes, you even get into a state where BEAM is making progress, but very slowly; that's a fate worse than death.
If you follow the Erlang philosophy, you'll have a recovery strategy from crashes or other deaths. Heart can be used to turn completely blocked into death, although I never used it professionally. But you've still got to worry about working but not well.
Nif's are always a risk to deploy cause they run embedded in the Beam runtime. That said we all use many nif's to do all kinds of stuff and its fine.
I go into this in the article a little but but the ruslter team has made dirtycpu and dirtyio macro's to help reduce the risks.
NIFs are dangerous, ports are not. Basically a NIF runs in the same memory space as the BEAM, so misbehavior of the NIF can crash the entire application. On the other hand, they have wildly better performance than ports, which have to operate through STDI/O.
The Rust / BEAM memory sharing problem does exist, but it's not nearly as bad as in more traditional C NIFs, because almost all C programs leak memory due to bad manual memory management. Hence all the buzz about Elixir+Rust.
I personally include port drivers in with ports, which gives you the same level of shared runtime environment as NIFs, with the benefits and drawbacks. Sometimes it makes more sense to interface as a port rather than as a function. Of course, sometimes it makes more sense to interface as a C-node rather than either; then you can be on a totally isolated machine, and the C-node can even crash or otherwise disable the whole OS and your Erlang node will be fine.
What's the benefit of a port driver over a NIF?
A port driver can participate in the BEAM event loop, adding filehandles that get called back when they're ready.
It's a more appropriate choice for something that's asynchronous, although NIFs do have ways to fill the same role. A port driver would probably be a better choice for specalty networking that ERTS doesn't provide (raw packets? netgraph, etc).
ports can be dangerous! One time my server lost performance because of a lot of zombie processes.
This is amazing! The Gods have sent me the perfect excuse to finally read all the rust ebooks I’ve been collecting! I have a toy project where certain functions are CPU-bound and would make for a perfect learning project. Very timely read!
Elixir, rust and fly.io in the same post. HN bingo.
I remember MS reps coming to our offices in the 90s and "Teaching" us how to program and how it is cool to have one person writing GUI and the other "guru" writing high performance C++ to be used in critical parts.
I showed them a piece of software that was mighty fast, Internet enabled and GUI intensive. They liked the software but asked where did you get this particular screen control from. You've got to see their faces when told that the whole software was written by a single person in Delphi.
Doesn't the use of Rust have to be extremely minimal because ErlangVM has hard time limits on their preemptive scheduler and if your Rust code hasn't finished when preempted, that causes lots of problems.
EDIT: thanks for pointing out where in the article this is talked about.
I go into this in the article :) the rustler team has a made a DirtyNif that can work around that, or you can manually yield if you'd like.
Indeed, your (excellent) article addresses this, here's the gist for those following along:
> Change `#[rustler::nif]` to `#[rustler::nif(schedule = "DirtyCpu")]`
> This tells the Rustler and BEAM to automagically schedule this in a way that won't block the entire world while it works. Again amazing, this is called a DirtyNif and is way more difficult to work with when you are manually using this via C.
Essentially, regular NIFs have to be extremely fast (< 1ms) because the VM can't preempt them - they run on the same scheduler threads the BEAM itself uses. Dirty NIFs solve this by running jobs in a completely separate thread pool ("dirty schedulers"). Rustler's docs explain it succinctly (https://docs.rs/rustler/latest/rustler/attr.nif.html):
> For functions that may take some time to return - let’s say more than 1 millisecond - it is recommended to use the `schedule` flag. This tells the BEAM to allocate that NIF call to a special scheduler. These special schedulers are called “dirty” schedulers.
> We can have two types of “lengthy work” functions: those that are CPU intensive and those that are IO intensive. They should be flagged with “DirtyCpu” and “DirtyIo”, respectively.
(Somewhat OT, but since I'm here: excellent article @ peregrine! I really enjoyed the read. Elixir and Rust are such a perfect fit. Plus, some of the specifics will be helpful for certain image-related things I'm actively working on, which is always nice. :) )
This makes me think of DotCloud, the precursor to Docker Inc. The idea that you can run anything has been around for a while. I don't see how Fly.io is especially good for Elixir. It seems to be about delivering reliable resources for each service, and it works no matter the web framework, as long as you have a PaaS that's geared towards running arbitrary OCI images. These posts seem a lot like the content on the DigitalOcean site, except they're written by staffers instead of freelancers. https://www.digitalocean.com/community/pages/write-for-digit...
One of the interesting bits about Fly is we have our own servers distributed globally and connected via wireguard. Which makes it is trivial to setup Elixir/Erlang distribution globally, and most importantly, close to your customers. While its not a magic bullet it is pretty amazing to type a few commands have a globally deployed and directly distributed application.
Further Fly builds its Dashboard internally with Phoenix LiveView. We want the Phoenix and Ruby and Laravel and more communities soon, to grow because we believe if they grow, we will too.
That's just internal networking isn't it? VPC on the big clouds, DigitalOcean, and Vultr. Private Services on render.com.
I remember when Fly.io used to tout Firecracker but that is just a KVM engine, along with QEMU used on a zillion hosts.
What I'd like to see are customer success stories.
Edit: looks like you have to set up your own cross-region links on DigitalOcean and Vultr. So that interests me somewhat. :)
Elixir does distribution and concurrency really well, so Fly making multi-region deployments easy makes it a good fit.
So is Fly good for small sites or is it good for huge sites? What's a big customer? For small sites my idea of a multi-region deployment is a single-region deployment that works in multiple regions thanks to the magic of the Internet.
I see this which mostly seems to be content sites. https://www.wappalyzer.com/technologies/paas/fly-io/ Same on the first forum result: https://community.fly.io/t/customer-success-stories/4882
I'd say its good for both.
Small sites because it is low-bandwidth to figure out how to use. Time is money and if I'm just tinkering on the weekend I don't really want to learn Kubernetes or the labyrinth that is AWS; I just want to ship an app.
Big sites because your users get routed to the node closest to them and again you can do this without a lot of time investment. For Elixir I just wire up Libcluster and my nodes can talk to each other.
I really want GPUs on Fly soon though. Just take my money Kurt. (I hear they're working on it)
IO in erlang is slow, is Rustler a good candidate to improve IO intensive applications?
Where did you get this idea? Depending on your task, Erlang can be very fast with IO because it will aggressively use writev/readv instead of write. Obviously, you can do this if you "know about it" in low level langs, but it might be a pain.
Erlang is generally considered to be compute-slow (which is generally the case without dropping to nifs).
Is IO in Erlang slow? I never got that impression; iolists are a good fit for scatter/gather I/O, which reduces copying, and Erlang supports kqueue, epoll, and I believe similar on Windows. I don't follow Erlang on Linux closely, so I don't know if there's any movement towards io_uring, which does seem promising to help with throughput.
Either way, I wouldn't expect doing I/O in NIFs or ports to substantially improve throughput, unless you're also moving significant processing into that layer as well, or you're going to end up doing substantially the same level of marshaling work as ERTS does, just in a different language. Setting up a different set of kqueue/epoll descriptors sounds like a lot of work for not much gain too, IMHO; again, maybe io_uring would be useful, but I think you'd be better served to bite the bullet and integrate it with ERTS.
IO in Erlang is faster than many other managed languages, thanks to stuff like IO Lists that are dealt with very efficiently through iovec syscalls (readv, writev).
Then add efficient pattern matching for binary data, and networked servers on the BEAM are the most ergonomic than any other language.
All this stuff that the BEAM offers you out of the box can be replicated in any native language with a lot of boilerplate and ceremony.
Your "Hello, <name>" webapp in Rust will probably need two allocations and a string concatenation, while on the BEAM, if constructed as an iolist, it's a single writev syscall, using a static "Hello, " string and a shared "<name>" reference from the parsed HTTP data.
I/O in beam languages is considered pretty fast, are you sure you aren't mistaken?