> Everything seems so organically put together and integrated!
If you're talking about ERTS, sure. If you're talking about Elixir, I disagree. I love the language, but consider:
# Opening a file
f = File.open("foo")
# Reading four bytes from said file descriptor
IO.binread(f, 4)
# Seeking four bytes ahead from said file descriptor
:file.position(f, {:cur, 4})
Elixir
requires you to use Erlang to do certain things, rather than wrapping those things to re-expose the Erlang stdlib as Elixir—even when other, very similar things
are wrapped. (The specific policy, as far as I understand it, is that only functions that have different semantics from their Erlang equivalents get wrapped. So, if the Elixir version should throw an exception for error-cases, or is an implementation of a protocol, or any other Elixir-ism, there's a wrapper; but otherwise, there's not.)
It'd be like if core use-cases in Clojure required you to use the Java call syntax. Kind of strange. Still a joy, but strange.
> does the lack of types (both Elixir and Erlang are dynamically typed as you know) affect long term productivity on the platform
I wouldn't say so.
The "idiomatic use-case" of Erlang/Elixir is IO code. It's wire-protocol parsing and wire-protocol generation. (Take "wire protocol" here to also include on-disk formats.) Essentially, it's the code that decides what the types of untyped binary streams should be, by doing heuristic things to it and passing around weakly-tagged intermediate representations in the mean time; and it's the code that generates those untyped binary streams.
When Erlang/Elixir are being used for these idiomatic use-cases, static typing, or even reified dynamic typing, doesn't really make sense. You don't know yet what the system-internal types are. Erlang's type system, such as it is (one product type, called "term", which can be an integer, a binary buffer, a boolean, an array/tuple of terms, a linked-list of terms, a map from terms to terms, etc.) is the type system of a parser/generator. In fact, to write parser code in e.g. Java, you must first reimplement exactly that type system, and then express your logic in terms of it. You're Greenspunning Erlang!
(Other high-level languages, while requiring this greenspunning to write custom parsers, make using certain syntax-level marshalling features for particular, limited kinds of marshalling really easy. See, for example, Go's generic "encoder" lib, that works with many individual serialization format libraries, and relies on struct field annotations to do its job. If encoder-style structs fit your use-case, this code is amazingly succinct! If, on the other hand, you're e.g. deep-inspecting network packets like a firewall would do, and you don't want to decode the whole network packet to decide what to do with it because that's way too much overhead, then you've got to give up on encoder and write your own parser code. And it's ugly. Erlang/Elixir shines here.)
All that being said, you can write non-IO code (business logic requiring typed inputs/outputs) in Erlang/Elixir. It's less idiomatic, especially in Erlang, but it's not like it's clumsy. And it's certainly a better experience in Elixir (with struct types, for example.) But Erlang and Elixir still always kind of assume they're either being used as a "control plane" and that the data is flowing in some other way; or that they're the "boundary" between the (raw binary) outside world and some sort of business engine process or library written in native code.
The most idiomatic Erlang (or Elixir) you can write, takes data from the wire, figures out what it means, spawns a process that hands it off to a port program or NIF, receives a response, and then encodes that response and sends it back up the wire. When Erlang/Elixir is also doing the job of that inner business engine as well as being the glue, it's less streamlined, and you start having to think about things like using HiPE and function-inlining annotations to make your BEAM modules run faster (which doesn't really matter when those modules are just there to do IO and never compute anything intensive.)
> a lot of crashes of processes are due to fairly prosaic reasons like unmatched patterns
Fun consideration: crashing on unmatched patterns is how Erlang "gracefully" handles the case where you have a distributed system composed of multiple ERTS nodes running your application, and you're doing a rolling update so that some of them are on different versions of your application, and they're actively RPCing with one-another, but with different application-layer messages without guaranteed mutual understanding.
Try to figure out how you'd handle that scenario in a language where you're sending around marshalled typed messages that must be decoded by a matching local marshaller, rather than simple tagged tuples. Especially try to figure out how you'd handle partially parsing a message where you understand the outer struct's shape, but don't understand one of the optional inner structs.
In any other language, the only option there is to just keep around the old version of the decoder, such that the new release of the app can decode both the old and new data formats, and ensures that it won't generate new-style messages for old-style peers. Then you roll your update over the whole network, and then create a release which rips out all the graceful-upgrade code you just wrote. It's a whole production.
Erlang's distribution protocol, and its external term format, are designed to obviate these challenges, such that your code (which, remember, is idiomatically IO-boundary-layer code trying to decide what to do with packets) can still make decisions in the face of data you don't have 100% of the relevant schema mappings for. The let-it-crash philosophy, and the supervision tree, are how you idiomatically handle these messages that have successfully been delivered from your peers, and which are "speaking your language, but not your dialect." Used together, you never have to do the graceful-upgrade protocol-version-juggling foofaraw.