6 ms·
I'm bullish on Elixir and I agree that ExUnit deserves to be singled out. Whether you consider Elixir fast or slow probably depends on what you're coming from
by latch 8y ago
I'm bullish on Elixir and I agree that ExUnit deserves to be singled out.
Whether you consider Elixir fast or slow probably depends on what you're coming from and what you're doing. Personally, I wish the performance story would significantly improve.
But, the reason I think Elixir should be your goto language comes down to process isolation and the ecosystem around it (the synergy of the runtime, libraries and language). If you're doing a pure web app, you might not fully leverage it. But, for anything that involves long-lived in-process data (like a workflow/pipeline, or persistent connections), the way to approach this in Elixir (with supervisors, processes, message passing and pattern matching) tends to result in highly cohesive and maintainable code. Message passing is a pretty effective antidote to coupling.
I just finished writing code that had to take a stream of unordered time-based data and dedupe it over a small window. I'm not overly pleased with the final result, but it's completely isolated from the much larger app it sits in, easy to refactor/rewrite, with no risk of someone taking a shortcut and just accessing some internal detail, and testable.
I feel like I wrote a microservice, but without any of the challenges of a separate app.
- hderms 8y agoI can see the logic behind the statement 'message passing is a pretty effective antidote to coupling' but I personally subscribe to the idea that actors don't compose. In light of that I think use of actors and messages implies some level of coupling which may not be obvious. Actors can't be arbitrarily composed together without knowing which actors they will be interacting with, making their addresses available and understanding their protocols so they can communicate together. Functions have no such requirements so I don't totally understand how message passing implies less coupling than conventional function-based architectures
- dnautics 8y agoMessage passing is less coupled than functions or "objects" that share bits of state.
- presscast 8y agoI think the pain-point being designated by the OP is that the cure is worse than the disease: Problem = tight coupling Solution = x, where x => {indirection, implicit_coupling} I'm not sure I agree the final judgement call, but the analysis is spot-on. Message-passing interfaces are generally difficult to discover and to debug, and require a lot of careful documentation, logging & instrumentation.
- dnautics 8y ago> Message-passing interfaces are generally difficult to discover and to debug, and require a lot of careful documentation, logging & instrumentation. You just use the "most common structures". For example, Elixir has the Agent module, which encapsulates state, but lets whatever you're using interact with the state via lambdas (or passed functions). Another common structure is the state machine. I was somewhat displeased with the complexity of gen_statem, so I wrote my own! It's about 50 lines of code, and can have plugins to do facilities like logging and debugging via a plugin-able interface. Ultimately the real win with message-passing is not having to write spinlocks, semaphores, or mutexes.
- lostcolony 8y agoSo you create a function around the message. It's not uncommon at all to have something like (pseudocode) myActorModule.increment(ref, amount) -> ref ! {increment, amount} myActorModule.syncIncrement(ref, amount) -> ref ! {increment, amount, self()} receive -> ok timeout 1000. You can build out the message passing interface to an actor -as- functions, and even supply sync ones if you want. The actor implementation can be completely opaque to the caller; just the exported interface functions are defined, exported, and supported.
- jerf 8y agoYou can use functions without knowing their inputs and outputs, and your functions don't need environments that somehow allow them to lookup the other functions they're going to call? Messages and function calls aren't quite isomorphic, but they're pretty close. (They get really close if you include coroutines and coroutine-like things, to cover the non-1-to-1 relationship between messages and replies that messages can have.) I'm not sure the difference can sustain the implication you're trying to draw. Especially if you include RPC in the function world.
- lostcolony 8y agoI think you're assuming side-effect free functions (and apparently ones whose interfaces are automatically known, since the interface to a function is just like the message handling in an actor; in fact, Smalltalk, the original OO language, didn't have 'methods', it had 'message passing'). But, you can still write libs and share them in Elixir, to benefit from any argument you'd care to make re: functions. Actors are all about managing state. It's when state starts leaking around or being modified unexpectedly ('wait, why is that being called there?!') that a more typical language (Java, say) starts to get tightly coupled, and sharing that state is the norm, and expected. And I agree, if you have pure functions, that issue is largely controlled for too. Just, that's a really hard paradigm to stay inside of and still be productive. I think that's why microservices are even a thing; it forces you to decouple state from different 'functional' parts of your system; by creating separate systems you have to be very intentional about what state you share, and how you share it (database or redis or an API call or similar depending on need). Actors do that automatically for you; the state is contained within them; there's no chance(1) it will accidentally be changed by some other process/code, and the 'controlling' process gets to decide what messages affect changes, and how those changes occur. Or, in other words, all state changes relating to a context happen in one place, rather than state being passed around to be changed anywhere willy nilly across contexts. 1 Technically there are system level things that allow you to munge state inside of an actor. This is more frowned upon than using reflection for arbitrary reasons in Java, however, so the only reason you'll ever do it is because you're debugging what's going on in something, or you have decided you absolutely -hate- your toes and need to footgun them off.
- latch 8y agoYou can certainly achieve the same low coupling with anything. But .... Messages tend to be lightweight. Often a symbol/atom "command", and a few scalar values. Like command line arguments. If the process needs more data, it sends its own message to the owner of said data. With conventional functions and OO, you're often dealing with complex objects (with references to references ...) which often leads to breaking the Law of Demeter. Message passing also gets your asynchronicity. You're also decoupled from specific runtime implementation (the receiving can be on a different machine, the semantics are the same). You can achieve the same without message passing, but it's certainly more baked-into the runtime/tooling/language. Joe Armstrong's popular quote on this: "The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle"
- PopeDotNinja 8y ago> I feel like I wrote a microservice, but without any of the challenges of an separate app You feel that way because you did :)
- moreoutput 8y agoI keep wanting to move to Elixir but always go back to Erlang. Any advice?
- fcbrooklyn 8y agoSpeaking as someone more into Elixir than Erlang, I'm wondering why you want to move to Elixir at all? If you're happier in Erlang, then why not stick with that? For me, the best thing about Elixir is the BEAM, and you've obviously already got that. If you don't want or need metaprogramming, or the various Elixir conveniences feel less than convenient to you, it seems like you may as stick with what you find comfortable.
- derefr 8y agoThe advantages of Elixir over Erlang, for me, mostly come down to the modules in its stdlib and package ecosystem. If you don't enjoy writing Elixir, you could always just install Elixir, and then write Erlang that calls into Elixir-stdlib modules. I believe you can even install Elixir packages into an Erlang rebar project, if you like. No need for Mix or anything.
- ams6110 8y agoWhy do you go back?
- derefr 8y ago> Personally, I wish the performance story would significantly improve. Before asking for performance improvements, have you tried: 1. tuning the BEAM? It has a lot of knobs. For example, there's "initial heap size", which can effectively turn any subset of short-lived processes that would otherwise need to do GC into simple "allocate an arena, do work, deallocate the arena" workloads. 2. tuning your OS? WhatsApp had to tune their (FreeBSD) kernel quite a lot to get it delivering packets to the Erlang runtime efficiently. 3. disabling tracing support? Part of the reason ERTS focuses on being a bytecode interpreter, rather than an AOT compiler, even in production, is that this allows ops engineers to connect to production nodes and trace/debug/profile code that's not doing what they'd expect, watching what happens as real user data flows through it. Because of this, the interpreter is "instrumented" even when a "production" (embedded, etc.) profile is used to generate a release. If you aren't going to use this tracing support, though, you can tell the runtime to globally disable it—for big wins in interpreter speed. (I separate this from #1, because this is basically the last-resort BEAM knob to turn. You're throwing away a lot of cool stuff by doing this.) 3. using HiPE? Specifically, sticking `@compile {:erl_opts, [native: :o3]}` into a subgraph of your modules (like a parser, a lexer, and the code that calls them both) can yield major improvements. 4. really using HiPE? I.e. recompiling both OTP and Elixir, and your project, using `ERL_COMPILER_OPTIONS="[native,{hipe, [o3]}]"`. This makes some things slower, so it's not the default, but it can beat #3 for modules that make a lot of stdlib calls (e.g. anything that loops using calls to Enum/:lists.) 4. using NIFs? (I don't recommend going this route until you've found that all of the above fail to get you enough performance, because the above all let you continue to just write plain Erlang/Elixir, while this step requires worrying about FFI. But it can be a big win, where and when it makes sense, just like in any other interpreted language.) If your Erlang code is still slow after all of those, then I'll be surprised and would like to hear your specific story.
- nivertech 8y agoI think most people would do 4 before 3 and even 2. Most erlangers would not use HiPE in production.
- latch 8y agoThanks. This is really helpful. I tried to think of some specific example. I think most of what I'd generally come up with is tied to the available datastructures. We recently tested some code to essentially do a group-by in code, with various aggregation functions, and it was considerably slower in Elixir vs D and C. Very simplified, something like: Enum.reduce(rows, %{}, fn [user_id, hits], agg -> Map.update(agg, user_id, hits, &(&1 + hits)) end) I didn't apply any of your suggestions. If I had another performance-like related pain point, it's throwing multiple cores at some data. I think it's somewhat common to have a large piece of data that you want to concurrently access. For example, keeping a large suffix tree in memory for an autocomplete endpoint, and letting each request concurrently access it. This isn't really a specific performance issue, but it's a performance sensitive pattern that is very antithetical to everything Erlang. ETS has limited structures, requires copying of values, and has no atomic replace (so you can't build a new version in a temp table and just rename it).
- ilovecaching 8y agoSo, just to speak about the BEAM, which is the part of Elixir I am familiar with, I don't think any BEAM language will ever be a good goto language. The BEAM isn't good at general purpose computing. It's not particularly efficient at performing calculations or using memory. It's good at reliably performing as you increase memory pressure on your system (via message passing). You can scale a business to a decent size on a smaller number of fatter nodes (high RAM machines). Connections will keep pouring in and you'll get a consistent ms. That said, you'll run into issues once you hit a certain level of scale. I've had to make patches to the BEAM just to keep us scaling, and I've never seen someone get away with stock dist. You pretty much have to rewrite pieces of OTP. Now is Elixir ok for a CRUD app serving 10G of traffic? Of course... but this is also the age in which every language has a solution to the C10K(, C100K, and more in many cases) problem, and there are compelling reasons to write your CRUD app in Go and get better library support, static types up front (trust me, you want a static type system), and lower latency. Personally, I'm bullish about Rust. With async/await landing, it stands a good chance of providing the same focus on safety and reliability while being infinity more flexible at what it can effectively. It's a better general purpose language. Also, the actor model (and CSP as well) aren't the end all be all of concurrency patterns. Rust only provides the building block (futures) so it's much easier to choose the concurrent pattern that fits best for your problem. You can sting a future using the actor model, so it's still an option, but you aren't limited to it.
- emerongi 8y agoOk... yet 99% of development is trying to write bug-free and readable code and do it pragmatically. Elixir gives you that plus a lot of goodies that are really hard to get in other languages. The creators of Erlang were really clever and in my opinion, so are the creators of Elixir. If I ever were "bullish" on a language, Elixir would be it. Elixir is a very pragmatic language. It doesn't get in your way, but still forces decent habits and design on your application.
- ilovecaching 8y ago> bug-free and readable code and do it pragmatically Erlang has historically been a bad choice for this, and there's been a lot of effort (dialyzer, records, maps etc) to make it better. Dynamically typed languages are, in my experience, not worth the cost of maintenance and readability. In Elixir and Erlang it's possible to create function signatures that tell you almost nothing about the shape of the data you expect to pass in. This makes it really hard to collaborate at scale. Various attempts to enforce dialyzer use are usually met with 'we need to move faster' or 'we'll add them later'. So if you want to write bug-free readable code, I think Rust is a much better choice. It's statically typed, and the addition of traits and lifetimes tell you so much about how a function or type will behave, and there's no need to sync specs or docs. The code doesn't lie.
- qaq 8y agoonce Pony matures might be a good option if you care about performance
- mercer 8y ago> I'm not overly pleased with the final result, but it's completely isolated from the much larger app it sits in, easy to refactor/rewrite, with no risk of someone taking a shortcut and just accessing some internal detail, and testable. The 'let it crash' is really something I've not encountered in any other programming language/environment. I've got a constantly growing Elixir project that I use for scratching various itches. Quite often I'll prototype some new feature in some of my free time, and quite often what I come up with is pretty haphazard. In some cases the stuff I have running crashes for the silliest of reasons, but the rest of the app is unaffected, so it doesn't matter. A look at my log often makes it clear that I forgot something as stupid as checking for a timeout to a HTTP request. I fix the issue, push it to the server, recompile in the IEx console (or Application.start()/stop()), and keep an eye on what happens next. When I have a bit more time, I might actually take a more careful look at the code to fix some obvious issues that somehow haven't caused problems, or I might add some typespecs when things seem to have solidified, where Dialyzer/Dialyxir, via its 'success typing' (which I guess is basically gradual typing like TypeScript?) will probably help me avoid stupid mistakes down the road. What I like is how organic the whole process feels. The godawful, badly thought out parts of my project don't cause the whole thing to crash, and the more solid parts can be augmented by a type system. It's an approach that I was previously entirely unfamiliar with.