5 ms·
I've been programming heavily in Elixir for the past 2+ years and I've become increasingly happier with the language, its growing ecosystem, and the Erlang/OTP.
by plainOldText 8y ago
I've been programming heavily in Elixir for the past 2+ years and I've become increasingly happier with the language, its growing ecosystem, and the Erlang/OTP.
I first came across Erlang a few years back, while I was researching more exotic programming languages, but for some reason Erlang never felt like home. I know plenty of people like it, but personally, I found it difficult to get accustomed to its syntax, especially after having spent most of my time in Python.
But as soon as I discovered Elixir, I got hooked. All the advantages (and disadvantages) of using the Erlang/OTP programming model, while experiencing a nicer, developer-friendlier, and more familiar interface.
Our work is seldom, if ever, not dependent upon the sweat and labor of other people.
So, thank you Elixir team! Elixir is truly a joy.
- sidkshatriya 8y agoElixir Erlang/OTP are indeed a joy. Everything seems so organically put together and integrated! I love the OS-like feel of the platform. However, I'm curious: does the lack of types (both Elixir and Erlang are dynamically typed as you know) affect long term productivity on the platform? The lack of proper static typing does make large codebases very difficult to build and maintain, don't you think? People have mentioned dialyzer a lot in reference to Elixir/Erlang but how good/practical is it? Sometimes I wonder: how fault tolerant can the OTP platform be anyways if your process can crash due to an unmatched pattern? A type checker should tell you at compile time that you've not tested all possible combinations and this should not happen at runtime. For a process to crash on an unmatched message and have a replacement process restarted by the supervisor to take its place ("let it crash") is definitely cool but seems like an extreme solution. What could have been fixed forever by a type checker is being deferred to the runtime. Thats really my main gripe. There are probably many scenarios where the whole OTP philosophy of crash recovery is definitely going to help with the robustness of the platform but I suspect a lot of crashes of processes are due to fairly prosaic reasons like unmatched patterns, wrong arguments to functions etc.
- dnautics 8y agoElixir certainly has types, and they are far stronger than python and Ruby. In the end a lot of your crashes are going to be "other people's software, like, say, "an unexpected race condition in nfs". Or "some other library crashes when parsing a really big json". In the end these are not really solved by a static type system. The idea that static types will save you is flawed by giving you a false sense of security and tying up developer time outside of the 80-20 Pareto law... In my short so far experience as an elixir developer I've delivered all of my products on time. Running it I know there are errors since they floated by the console in the background... I didn't care about the 1% error since it did not significantly affect my performance. Not worrying about every last problem has been great for my sanity. Finally, there is a possibily antipattern Dev cycle, I've settled in to. Unit test the happy path, write happy path, conjure errors, unit test them, solve them with an eye for recovery, spec out the type system, sweep away last lingering errors with dialyzer. Occasionally totally refactor. I can usually demo any time after step 2.
- andy_ppp 8y agoThis is great, I'm not sure why this has been down voted. Due to the way Elixir works you simply don't have the same concerns about the wrong thing being passed as in other languages - pattern matching and guards don't replace a type system but provide some good protections when used correctly. Add in dialyzer and you have something that gives you the flexibility loose typing and a fair amount of the guarantees of a more formal type system. And with Elixir you have a way of starting processes again should you still crash with an error. Never mind that microservices and cross node messaging are built in as standard, as well as confidence in being able to scale your system with things like Flow or OTP. I'm surprised not to see more traction (especially in startups) and I love a project this complex only has 16 bugs on github!
- dnautics 8y ago> why this has been down voted static typing is somewhat of a programming religious wars. I mean sure, extremely weak typing is IMO horrible (python, ruby, js, I'm looking at you) but being dynamic does mean that quickly testing out an alternative code path. Just today I realized I needed to forward some information to a somewhat polymorphic data structure, a process which I hadn't planned for, I found a map structure that is encapsulated state in a gen_server in exactly the right place, so I just shoved it into the map under a new key. That is effectively breaking what could have been a static type system. And it was two lines of elegant code, no refactoring necessary, and probably a line of documentation saying that in this corner case the structure will contain this extra info. IMO the right approach is compromise. You could do this in a statically typed system by just making everything a map... which is effectively what the BEAM languages do, but at least there's a type analysis tool that can introspect deep into the built in "flexible" types.
- passer-by-123 8y agoA type system will allow you to catch a certain category of errors. There are still whole categories of logical errors and runtime errors, such as the ones that arises with communication with other systems and processes, that you can’t foresee and making sure that your system will go back to a working state in face of those errors is essential. From this perpective, a type system is a nice to have and a recovery mechanism is a must have. However, a type system is much more than preventing a certain class of errors. It is documentation, it is a design tool, it can improve performance, and others. Plus, if you can catch errors earlier, why not? At the end of the day, I would love a type system in Elixir, but I am certainly happier that the recovery system is there right now.
- dnautics 8y agoare you aware of dialyzer?
- passer-by-123 8y agoWhile Dialyzer can be handy, it is not a type system. Elixir/Erlang do have typespecs, which helps on the documentation aspect, but a type system would give much more on the performance and guarantees departments.
- bjz_ 8y agoDialyzer works on success typing, which only tells you when it's certain there's a bug, but will stay silent if it's unsure. Even at the highest settings I couldn't get the same lovely iteration loop that you get in an ML-style language (like Haskell, Elm, Rust, etc) that I've come to expect, where you feel like you are having a conversation with the tool. At the time I was attempting to use it, it was also super slow, and libraries were not keeping their typespecs up-to-date. Also a big one: no parametric polymorphism results in `any`s galore. :/
- _asummers 8y agoIt basically starts with a notion that your program is correct, and then runs through all the facts it can collects about dependencies and your code, and then if it finds some contradiction, it warns. It does weird things though for optimization, such as replacing long lists of literals with any() after a certain point, or merging structs into bizarre-o franeknstructs like %{:__struct__ => A | B, :a => int(), :b => int(), :shared => int()} if A and B have that :shared field in common and your typespec is A.t() | B.t().
- plainOldText 8y agoIndeed, an advanced static compiler could provide assurance against certain types of errors. Generally, I think compilers should do as much work as possible, because people are slow and prone to error. Having said that, I know of no language that offers concurrency via the actor model – which is easy to reason about – fault tolerance, advanced static typing and distribution out of the box, all at the same time. Thus, I'm wiling to forgo the static typing, in favor of all other features.; no to mention other cool Elixir/Erlang features such as live runtime introspection and tracing, hot code reloading, the OTP, excellent multicore support, and soft-realtime capabilities due to preemptive scheduling and per process garbage collection. And btw, people have written large systems in languages less stricter than Elixir – e.g. Python, Ruby, etc - thus I think large codebases in Elixir should be fine.
- Zalastax 8y agoMy Master's Thesis is about this topic! The related works section mention some languages that get fairly close to being actors with types (e.g. Pony), but you're right that we're not there yet. The tricky thing is that we need types that evolve as messages are being sent - an actor might change behaviour when it receives a message. Academica is still figuring out the kinks of how to do that. My work assigns a static type per actor and my conclusion is that that's decent - but quite annoying to program with and without the guarantee of deadlock freedom. https://www.dropbox.com/s/lczrcqu2m9p6osv/Master_Thesis.pdf?dl=0 https://www.dropbox.com/s/lczrcqu2m9p6osv/Master_Thesis.pdf?...
- di4na 8y agoThis is now my goto link to explain the problem to people that ask about it. Thanks for the links :)
- Zalastax 8y agoThat warms my heart!
- bjz_ 8y agoOh lovely, will have to give this a read at some stage! I'd also be really interested in versioning, and upgrading a cluster over time, without bringing the whole thing down (like with Cloud Haskell). Would be super handy to be able to verify that your migration works first, before actually going ahead with it. I'd also love it if the types could be used in some way to make this kind of thing easier to reason about. Are you also aware of the work advancing in Akka Typed? After a number of iterations they made some modifications that made things simpler - not sure how it compares with your work though: https://doc.akka.io/docs/akka/2.5/typed/actors.html#relation-to-akka-untyped-actors https://doc.akka.io/docs/akka/2.5/typed/actors.html#relation...
- nathan_long 8y ago> There are probably many scenarios where the whole OTP philosophy of crash recovery is definitely going to help with the robustness of the platform The fundamental error that only "let it crash" can handle is hardware failure. No amount of type checking or exception catching will help if the server melts in a fire. In that case, the only solution is for a different process on a different machine to deal with the failure. This will be true for any programming stack. Erlang's approach is, since we have to delegate to a different process to handle errors in the most extreme case, let's use that approach to handle all errors, so we only have one mental model for error handling and don't have to do a lot of extra work to handle hardware failure. (Joe Armstrong refers to "errors" as times when the programmer doesn't know what to do, as opposed to exceptions, which can be rescued.) The performance story is similar: no matter your language, if you want to scale, eventually you'll need multiple processes on different machines with no shared memory passing messages to each other. For consistency, Erlang uses that model all the time. Erlang's fault tolerance decisions are what lead to its fairly good, and especially consistent, performance characteristics. I've written more about this on the DockYard blog: https://dockyard.com/blog/2018/07/18/all-for-reliability-reflections-on-the-erlang-thesis https://dockyard.com/blog/2018/07/18/all-for-reliability-ref...
- Thkejehd 8y agoI slap types on my elixir structs and even if it's not entirely comprehensive (success typing) it definitely helps with refactors and helps me catch bugs.
- derefr 8y ago> 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.