It’s a breeze to make broad changes knowing the compiler will prevent me from missing a code path, etc.
It’s a breeze to make broad changes knowing the compiler will prevent me from missing a code path, etc.
Code coverage reports are a thing. You should be writing your tests async (including tests that leave the VM) in elixir, that will give you a chance to catch and raise asserts on race conditions which will stress ouct timings in your vm, etc.
In node for example, I'm refactoring constantly (or, at least, should be...) because I keep backing myself into cognitive and developmental corners by using a kind of brick-by-brick approach that is perhaps encouraged not only by the language but by the eco-system.
I'm not finding this with Elixir, possibly because many of the more important architectural decisions have already been made for me at the language level, and apparently been made very well.
The hurdle becomes more understanding how to best do something, rather than figuring out how to do something at all.
A big part of this also may be Elixir's REPL. It has help for everything built-in. Whereas in node for example I find I'm constantly searching online and piecing together things from many different parts, in Elixir I almost never am, it's all just right there.
I think the key “architectural decision” here is that code and execution (i.e. the process) are bundled together by design.
Take a basic factorial function in Elixir:
defmodule Math do
def factorial(0), do: 1
def factorial(n), do: n * factorial(n - 1)
end
There is almost nothing there that isn't directly representative of the base math equation itself (which, by convention, treats the result of factorial(0) as equal to 1). The function order is important in this case, the first is a "guard" that prevents the second from being executed when the firsts case is met. At this point the module exits out of the second function by multiplying the (silently) accumulated result by 1 and returning it.Versus, in JS:
function factorial(n) {
if (n == 0) {
return 1;
} else {
return (n * factorial(n - 1));
}
}
It's not too bad, but the if-else, comparison, multiple return statements and nested brackets at the second return are all done away with in the Elixir version.Further, recursion is not something many programmers reach for first when working in many other languages, perhaps out of habit, or maybe of the concern that extending such implementations later can become difficult. As such, most programmers might implement the above as more something like:
function factorial(n) {
if (n === 0 || n === 1) return 1;
for (var i = (n - 1); i >= 1; i--) {
n *= i;
}
return n;
}
Compared to the Elixir code, many steps are required to read and understand this. When this type of laboured patterning is expanded out into a larger project, with many interlocking parts, it may quickly become difficult to work with, and can become necessary, and necessarily difficult, to refactor into something simpler, which then may require rethinking the entire process.Pattern matching is really helpful, though. It turns complex nested logic into simple truth tables and acts like Eiffel's design by contract, just pattern match on valid inputs, then handle the errors.
The same functional patterns repeat at different levels in the stack. For example, in the Phoenix web framework, processing a HTTP request can be considered as pattern matching on the expected inputs (validating them), then a series of transformations (making a db request, taking the result and rendering it into HTML via a template), then returning the result.
From a concurrency perspective, the interesting part is that each HTTP request runs in its own separate blocking process (green thread). So when you are doing the programming, you don't need to think about concurrency and async stuff. You just do your thing. This makes it easy to think about and debug.
let factorial = n => n === 0
? 1
: factorial(n - 1) * n;
But I do wish JavaScript had some more powerful pattern-matching syntax. For example, both Rust and C# both have nice switch/match expressions that can really simplify code like this.https://elixir-lang.org/getting-started/typespecs-and-behavi...