Beyond 10,000 Lines: Lessons Learned from a Large Phoenix Project
shift.infinite.red
shift.infinite.red
http://learnyousomeerlang.com/dialyzer
The way success typing works it is a bit like static typing but when it cannot deduce the types it assumes success. However when it finds a discrepancy it is always right. So the more type annotations you add, and the more precise they are the more benefits you get from it.
It has also been there for many years. I think Python only very recently has started getting the same kind of things via MyPy. Of course, they had to call it something differently (Optional Static Typing).
Elixir has a wrapper around it, it seems: https://github.com/jeremyjh/dialyxir but never used it (I user Erlang mostly).
The wonderful thing about it is that you don't have to get all type annotations since the begining, you can add them over the time. This allows to use "unspecified" types for quick prototyping, and make them more specific when the project evolves, which is a major complain of traditional type systems such as Java's.
Another tool that I find useful is Credo[1] which "is a static code analysis tool for the Elixir language with a focus on teaching and code consistency".
The Ecto[2] project uses a tool called Ebert[3] that automatically runs Credo for each pull-request and comments with the issues found. Here you can see an example of Ebert's bot commenting on a PR[4]
[1] https://github.com/rrrene/credo [2] https://github.com/elixir-ecto/ecto [3] https://ebertapp.io/ [4] https://github.com/elixir-ecto/ecto/pull/1785
Erlang is not just about distributed computing (in fact, it never was; any affinity for distributed computing was more of a side-effect of Erlang's design). Rather, it's about fault-tolerance. Supervision trees and "let it crash" are the cornerstone of Erlang programming, and therefore by extension the cornerstone of Elixir programming.
Meanwhile, OTP applications build on this in a way that permits composability. It's kind of like microservices behind the scenes, bit they feel like a monolith; you build up your system from lots of different OTP applications that work together to provide a unified whole.
Elixir and Erlang web frameworks (Phoenix, Sugar, Chicago Boss (IIRC), etc.) already do a lot of this for you by kicking off various OTP dependencies; for example, your average Phoenix or Sugar application will in turn start Plug, Ecto, and various other OTP apps, and these will in turn spin up their own dependencies (like Cowboy and Postgrex, respectively).
Basically, it's not quite right to equate OTP to just distributed computing. OTP is at the heart and soul of the vast majority of software written for BEAM.
I'll ask around in the company for more information.
I've worked briefly on wrapping an existing erlang app in a Elixir (Nerves) container and I did have a few issues around dependency management which required small changes here and there, but nothing major.
One area where I usually spend more time than I'd like is to get type specifications in good shape so that dialyzer doesn't report too many warnings, but that gets better with every Erlang/Elixir release.
I wasn't personally working on this, so I apologize for being fuzzy on the details, but I understand that getting rebar3 to fetch all the deps that would normally be managed by hex was not possible or at least non-trivial. There was talk of having to manually install each dep that you knew the Elixir lib would be requiring.
I think it was sorted into something workable, but if anyone has better understanding I would love to be pointed to some resources!
Not a seamless setup, but it does work. As the sibling comment suggests, perhaps using mix for everything would help.
If you are just starting out and have to make a choice between elixir and erlang, go with elixir. I can't see any reason to start with erlang instead of elixir. You can use all of the erlang codebase in elixir. As a matter of fact most elixir projects call into erlang as erlang has a huge standard library. Also, elixir has nice macros which makes your code a lot DRYer.
Every "large" project gets to a point where some routes aren't totally RESTful. It happens. But it's not great advice to say that non-RESTful routes are always a code smell.
Create: POST /resources (or PUT /resources/42)
Read: GET /resources/42
Update: PUT /resources/42 (or PATCH /resources/42)
Delete: DELETE /resources/42
List/Search: GET /resourcesHTTP verbs are the operations you wish to perform. GET on a "/" resource is effectively a listing, GET on "/identifier" retrieves a specific item. PUT updates, POST creates new, DELETE is obvious. SOAP was a bad idea because it stuffed an extra RPC layer on top of HTTP's existing layer, requiring you to parse the body to find the content. Thus almost anything interacting with SOAP had to understand both HTTP and SOAP, plain HTTP tools were useless.
HTTP provides out-of-band signaling and extensions with headers. It also has a built-in mechanism to negotiate wire and content formats with Accept/Content-type. All of this means it automatically supports graceful degradation and backwards-compatibility. HTTP is stateless so unless you go out of your way to break that property it scales really well.
URIs identify the resource you want to perform the operation on. Items that are children are located "under" their parents: "/parent/42/childtype/child_id". Again - why introduce some extra system for describing these relationships and identifying resources when URIs already do a fine job of that?
If you want to take it even further you can use URIs in your data types. For example if a child needs to indicate its parent you can provide the parent URI (just the path/query/fragment portion) rather than a parent identifier. Why should the client care how you generate identifiers? It also means you can change them in the future (e.g. Int to String). Hide implementation details of the server from clients when possible.
Use Cmd/Qrys from CQRS, route to a single endpoint in a restful fashion, and spend more time doing instead of writing boilerplate.
I started phoenix before fully understanding Elixir and had a hard time. Then I got that book and when I was halfway through I loved Elixir already.
Now I use phoenix for almost everything web related.
https://github.com/thechangelog/changelog.com
Also take a look at the code powering hex.pm:
Quoting the relevant fragment in full:
"We focused our automated tests on our controller actions and plugs rather than going for 100% test coverage. Since these are the main ways that the Phoenix application interfaces with the outside world, they’re the critical points of failure.
Controller tests also exercise a lot of the code paths in your application, making it less necessary to unit test every single module. As a result, you end up with fewer tests, which makes it easier to do refactoring, provided your changes preserve the behavior of the controllers and plugs."
e.g. if a method is private, it's not tested.
that's the usual way when full reliability is not worth the dev time, or when devs think functional tests only are enough but still want a tap on the back for having unit tests and coverage numbers.
in their case it's the later as you can see for: "Controller tests also exercise a lot of the code paths in your application"
classical functional tests being called unit tests excuse. not that functional is better or worse, but correct names are better no matter what.
Their HTTP API is their interface for the application. It makes a great deal of sense to focus on that level for their testing (note: they didn't say they didn't test internals, they said they focused on the external interfaces).
Plug [0] on the other hand is something like "rack" in the ruby world, a "A specification for composable modules between web applications". Phoenix is built on plug.
I think very simple you could say that plugs are pipes or similar to middlewares - functions that take the connection, transform it or do some checks based on something, then pass it to the next plug. Like a pipe full of functions.
Because phoenix is built on it, it is very easy to specify custom plugs and add them to your request / transform pipeline.
Someone correct me if I explained that wrong.
More like "large" projects.
I write something like 25kLoC/year (of shipping code, generally very complex stuff) and I don't even program full-time. The two projects I am working on now are 35kloc (the smaller one) and 250kloc (the medium-sized one).
If someone thinks 10kloc is big, I have a hard time thinking of that person as a professional programmer.
(Numbers listed here exclude blank lines and comments.)
But I do in general agree with your take on it :)
Every time I have heard this kind of claim (with modern languages), it turned out not to be true except for trivial code or straw-man bad code in the 'bigger' language. So if you have real-world examples that have real-world effort put in, I'd like to see them! (I would be happy to be wrong.)
5x-10x productivity increase would be huge if it actually existed; it would be so unstoppable that everyone would switch to the new really-great language immediately. That hasn't happened, which should be a clue that maybe the increase is not there.
Even a 20% decrease in cost of engineering would be so large as to be unignorable.
When writing software, how fast you can type the code is rarely the limiting factor for speed of development – the architecting and consideration of interplay between components takes the bulk of the time. The grandparent claimed code reduction (which has intrinsic maintainability benefits) but made no statements about general cost of engineering.
Which is supposed to be what is simplified as LOC goes down.
So if a supposed 5x-10x code reduction (which I've never seen real evidence of) doesn't lead to 5x-10x productivity increase, how much increase is there supposed to be? Surely more than zero?
I don't think so. If you can express the same concepts with the same interfaces and functionality in 1kloc vs 10kloc, most of your time has probably still gone into figuring out the interfaces and connections.
> So if a supposed 5x-10x code reduction (which I've never seen real evidence of) doesn't lead to 5x-10x productivity increase, how much increase is there supposed to be? Surely more than zero?
Oh, certainly more than zero! Sometimes much more. But there's simply not a one-size-fits-all formula for the relationship between lines of code written and productivity.
Anyways, not really sure what you're getting at. Your original comment was that 10kloc isn't "big"; the rebuttal is that lines of code is a naive way of looking at system complexity, which is presumably what you mean by "big".
Pattern matching makes it easy to bind variables and validate their values in one line (so no needs for if statement).
In Elixir, you usually don't catch exceptions, you let it crash. So all the code to handle failures/errors doesn't exist, it is handled by OTP.
No need to write any communication layer since it's built-in OTP that implements location transparency.
These are kind of the low hanging fruits on top of my head. I'll point out that fewer lines of code doesn't automatically translate to increased productivity. Also, transitioning to a brand new stack is not always justifiable/possible even with the promise of significant increased in productivity, it's really not that simple.
For your 5x-10x case, what is true is that the gain is genuinely possible, but it is just as likely to come from libraries than it is language constructs. Because of that it's often 5x-10x in a limited area.
Obviously we're throwing around random values like 5x and 10x "productivity" but it's more nuanced than that. There's more than LoC that can be measured as "productivity": how about bug count and severity per LoC written, refactoring cost, performance, robustness, library support, setup time, etc. And many more metrics.
Metrics are valued differently depending on the programmer and the problem. Eg, who cares if my CRUD web app has memory leaks and crashes randomly, it's stateless! There's not one really great language because every programmer their own productivity priorities.
Often, though, LoC is used as a poor proxy for this multi-dimensional "productivity" value.
1. Thinking about the problem and modeling it properly is still hard, and language independent
2. The language still has its own quirks/failings that you have to work around
PS. My comment has nothing to do with Scala per se - I'm using it as an example.
Lack of functional composition
Mandatory types (for every trivial parameter object or "lambda")
Excessive Object-Whatever-Mapping
Coping with mutable state in large amounts of effectively global data.
Excessive partitioning due to the size impacts of above hardships
Then we can start to talk about the lengths of the lines...
1) elixir is relatively young so there aren't many projects that have had a chance to grow to be huge. so yea, take everything you read about it with that grain of salt. people haven't grown to hate it yet...maybe everyone will always love it, but I wouldn't bet on it.
2) when used in certain contexts, elixir applications may end up being quite a bit smaller than comparable java/scala/python/whatever apps. A lot of what we end up writing with microservices is just basic RPC and fault tolerance boilerplate stuff, which OTP takes care of out of the box. Example, pinterest re-writing a 10kloc java app in 1kloc of elixir, running it on fewer servers: https://engineering.pinterest.com/blog/introducing-new-open-...
(OK, I made those numbers up, but reading Java code is bloody tiring due to the silly walks and typical paste-o-graphic work styles)
1.4 million lines of Erlang
309 thousand lines of C
120 thousand lines of Elixir
... And lots of other code and things I've excluded. These are lines of code, not counting blank or comment-lines.Now we could argue that we could unwrap this count at any layer? Why stop at Erlang/OTP? Why not include the C runtime and the Kernel? The real effect here is that a lot of this code implement things that are directly used by Phoenix apps, so I think it's fair to say, it's concision comes from being very good at leveraging other code.
(I don't write Elixir code but I've seen some good results from teams who have. None of it is magic though.)
I have a project that is 5k lines of code and roughly 11k of tests and specs(cucumber). It is a rewrite of a project that was 50k lines of code with 1.2k lines of tests and had less functionality and features than what it does now.
Lines of code are meaningless when it comes to how much value they provide. I personally prefer when a codebase is smaller because it means some thought was put into it and most likely has less bugs as a result.
I personally think copy & paste is an anti-pattern (instead, use features such as generics), but many think it is a godsend. Using code reuse rather than copying code can make a massive difference in size and maintainability, less is definitely more!
Part of that is a political problem, of course, but was not an issue earlier.
C++ is the tragedy that keeps smiting the industry with its children. (e.g. - Java, and its silly names)
"But we need long names, and need to compress them since they contain so many words". No, you need to better partition things so there is enough context around what the name is attached to, so that the name can be simple.
connection |> endpoint |> router |> pipeline |> controller
Not sure what it is but it must be a combination of "|> seems pretty awesome" and "well yeah it obviously makes sense to get a request as a struct and chain functions in this way". It just clicked and aligned so perfectly with my mental model :)
Idea: make users with high karma capable of using benign, unassuming and non-common parts of speech to ping the admins?
...I think I've been thinking about security too much
Maybe just busy with real work?