https://github.com/wende/elchemy
It allows you to write elm-like code that transpiles into easy to read and idiomatic (for the most part) elixir code.
I love the philosophy and robustness of the BEAM VM, but I can't go back to the stone ages of a language with a poor (or non-existent) static type system.
What you probably want is a strong type system. Functional programming languages (I'm thinking Julia as an archetype of strongly typed systems) especially are really good at dispatching over types, and can make robust optimizing inferences using a well-designed type system.
BEAM languages are strongly typed, but there are far fewer types (and no true custom types) like julia. However, gating based on functional guards is simply amazing and more than makes up for a limited type system in terms of code clarity. As for compile-time vs runtime error trapping - part of the point of BEAM languages is "who cares?" An error doesn't bring down your VM. It gets logged, you can see it, and apply a hot patch and fix it if you need to.
It won't bring down your VM, but it will impact any end-users of your service. Not all errors can be solved by restarting a process or retrying a request (but I recognize the argument that you should aim to catch those types of errors in tests because they are part of the core business logic).
Also just wanted to note that hot-patching has serious costs. Code upgrades/downgrades can be difficult to write and the upgrades themselves also need testing (and this often doesn't get the attention it needs).
There are no silver bullets, basically.
Expressive, algebraic types allow me to tell my compiler, IDE, and code heirs what I'm trying to do so they know just by reading my code if I failed. That's indispensible.
When you connect to a cluster to send to something on another node, it’s no different than making an API call to a service that you don’t control. The compiler can’t make any guarantees there without contracts in hand, which can change at a whim on a deploy or when new nodes are added.
For message passing across a cluster, setting up Dialyzer is probably the most realistic approach. Function calls on the BEAM are more like request routing I think.
Node -> Module -> Function -> Arity -> Pattern
As these things change, the contracts have to be exchanged with every member of the cluster which would not only add significant communication overhead, it would also bypass those compile time guarantees.
That's before even considering things like hot deploys where the code is updated live, while running, mid-execution.
The contract enforcement becomes akin to the goals of WSDL, which added a ton of overhead with minimal benefit aside from making consuming APIs easier for statically typed languages because of generators.
Is the overhead worth the difference from...
Client (this message will fail on this server) -> don't send
to...
Client -> (this message failed)
Especially when talking about a cluster which you own...that benefit is questionable at best.
It doesn't cover every case, but it covers enough of them to provide a very watchful eye without getting in your way.
https://speakerdeck.com/jvoegele/elixirconf-2016-dialyzer-op...
A static verification framework for message passing in Go using behavioural types, Lange et al., ICSE 18
http://blog.acolyer.org/2018/01/25/a-static-verification-fra...