DHH says that explicit code favors novices and those with 'fresh eyes' on a problem while burdening experienced veteran programmers with bloat of boilerplate to read and to write.
I may as well come with fresh eyes for any code I wrote over a year ago. I'm reading _my own code_ with an eye to figure out how it was put together. I should be courteous to my future self and to anyone else who wants to read my code.
I pick up a new framework every year or two. I've been a full time software professional for over 16 years and I'm picking up new tools all the time. I prefer tools that tell me things explicitly so that I don't have the steep learning cliff of grokking the entire system in order to be able to work with one little part.
And isn't this hypocritical? So many people (probably DHH too?) gush about how easy Rails is to get started with, but here DHH argues that it's so much butter for the veteran who knows what all the implied magic is. Doesn't all that implied magic also leave the beginner kinda helpless? There's so much Rails tutorial with the attitude "here, do this, trust me, it'll work". And I've found that trust to be misplaced. The underlying implied design isn't actually what I wanted, and it's really hard to get it to do what I want because the convention is so baked in.
On the other hand, Go is a bit tediously boilerplate heavy. I'm still looking for the right balance.
I totally agree with everything you've said. I came here to post more or less this.
> On the other hand, Go is a bit tediously boilerplate heavy. I'm still looking for the right balance.
I've been working with Django (in Python) for a while now, and find it strikes a really nice balance. Almost nothing happens by magic, but there's also surprisingly little boilerplate. It used to be a little rough, but it's gotten a lot better in recent years especially (I think likely due in no small part to influence/competition from rails), so if you haven't checked it out recently, you should!
Elixir might be up your alley. The language prefers explicit, and gives you macros to make the explicit implicit when it makes sense (e.g. the ORM). Functions are preferred, and you'll get flak for macro laden code, but the best macros are the ones you don't even know are there (like the "if" statement and "def" in Elixir.Kernel).
I find myself wanting languages and frameworks that let me write the magic myself. Magic is about solving big sets of similar problems in really compact/effortless ways. Rails tries to solve a really huge class of similar problems, i.e. web development, and starts pushing back on what you can achieve without ditching the framework entirely. I don't mean that in the sense that you stop being able to write regular Ruby (which can do anything), I mean that it's smooth sailing until you want to add your own magic in 'userspace'.
Take ASP.NET, for example using method attributes for authorisation:
It's great and magical if you want to lock down a controller or a method to admins only, or people whose standard user object has an Age field > 18 or something like that. But if you want per-project permissions, with a user->permissions<-project relation? Sorry, you can't easily pass invocation parameters to your AuthorizationPolicy classes because C# Attributes don't support that. You could try having a naming scheme for your url fragments and get the project ID from there, but this means you need to have project parameters in every route, which is often redundant and would need to be checked on any object in a POST request. Any way you go, you have to write code inside your method to get the job done. You have to break out of the magic, and let go of the automatic 403 responses to do anything beyond the simplest case.
To actually build this without a clumsy if/else and manually returning 403 with JSON (to describe the error) in every controller method, it was necessary to create an exception and a custom exception handler middleware, and in the handler, produce a non-standard HTTP code and catch it again in a second middleware after the framework would otherwise gobble up 403s and return HTML instead of JSON. Exceptions inside controller methods are a perfectly adequate model for authorisation failures, but because the framework expects you to hook into their middleware for it, they made it very difficult to do it any other way.
Ok, that was a lengthy example. But it illustrates what I mean by wanting to write my own magic. Frameworks shouldn't cripple userspace magic by design.
Have you taken a look at what can be done in functional languages (perhaps F#) ? I'm just starting to get into them [0] and it's been really fascinating.
Jet.com has an interesting post on why they chose F# for similar reasons [1]
Yeah, way into FP. I've used Haskell, Clojure, and hopefully some Erlang or Elixir soon, and I pretty much use functional style wherever else I can, including C# and JavaScript.
F# seems to be getting more attention, but I mostly just see people struggling to get EF migrations going, and it seems general ecosystem support is still shoddy (compiler bugs on .NET core 1.1? 3rd party VSCode support? official VS plugins crash all the time?). I have to use C# for work, so it's just not stable enough despite F# itself being around for ages :)
THat's not my experience with F#, I've used it with EF and .net core 1.1, and VS. I've been extremely happy and had little to none of the issues you listed.
I pulled it out of the drawer again a few days ago. It was lovely, I wrote a monad for asynchronous HTTP results (basically the async monad + an Either monad + some methods to produce some HttpErrors) and encoded a bunch of API functionality (including auth filters) with early returns. The only pain was not having anonymous dynamic objects in C# new { syntax = "syntax" } and using Dapper.
I'd definitely like to give it a go for some bigger projects.
I think you may have diagnosed me correctly. I have been enjoying Clojure. But weirdly enough, the more powerful a type system you have, the more you can do lisp-like magic. Think Rust's traits that can have default implementations, which basically gives you what multiple inheritance is supposed to provide. And Into<T> that essentially sprinkles opt-in dynamic typing through your codebase. There are so many bits of magic you can express with powerful static types, and I don't really mind writing a generic implementation once.
Strong types give me the feeling of expressing some sort of universal truth in a math way. I am actually quite bad at that.
Lisp let's me understand how procedures go together to build things. In particular, it helps get to a dual phase of programs. Macros aren't my program running. Rather, they are a program that makes the program I want to run. It just so happens that being able to cycle between those phases is very useful.
the analogy I'd use is driving a car versus sitting in a passenger seat of a car. When I'm a passenger, I never remember the route we took. I retain almost no information about lefts and rights and landmarks. But if I'm driving, I really only have to drive to a place once to remember the way.
It seems like automation and magic keeps on eating away at the driving experience, most cars are sold with automatic shifting, power steering, and a host of other features that most users are blissfully unaware of, the final outcome being self driving vehicles.
> And isn't this hypocritical? So many people (probably DHH too?) gush about how easy Rails is to get started with, but here DHH argues that it's so much butter for the veteran who knows what all the implied magic is. Doesn't all that implied magic also leave the beginner kinda helpless? There's so much Rails tutorial with the attitude "here, do this, trust me, it'll work". And I've found that trust to be misplaced. The underlying implied design isn't actually what I wanted, and it's really hard to get it to do what I want because the convention is so baked in.
As someone who got 'started' (to an extent) with Rails, I both agree and disagree with this paragraph.
The main benefit of Rails, for me, was that once I dove in and try to understand how things worked, I learned a tremendous amount. Contrast that with my situation now, years later, where I'm piecing together 'best practices' for React/Electron/Cordova apps from various articles and tutorials, and I can conclude Rails really helped me along.
On the other hand I do agree that the tutorials that you mention weren't much help, if not downright harmful in my journey.
All that said, I currently lean towards the side of explicitness for actual work, and Rails goes a bit too far for my tastes. But then I'm bit of a control freak.
In the middle to late Rails period, there are some patterns that are implicit, for example, DCI, Presenter and Form object. In my view, Rails is not Rails anymore; it's going beyond buffet (as mention in his talk: https://www.youtube.com/watch?v=Cx6aGMC6MjU). It's like it provides LEGO building blocks and people use them with their own plasticine shapes. Data flow through in weird manner. IIRC, there is a place where Rails copies all in-controller's instance variables to in-view's instance variables, and tell people to pretend that they are the same objects.
Ruby itself is strange (in implicit way) enough on how things are defined and called. Maybe it's just me, I don't even understand its OOP although I came from C++ background.
I definitely don't see it as hypocritical. Humanely designed systems are thoughtful in where they deploy implicitness. Implicitness can cut down on boilerplate for experts while simultaneously cutting down on confusing minutiae for newcomers.
On baked-in conventions, the implied design of a thoughtfully designed framework likely has wisdom that you shouldn't too quickly dismiss, and should at least fully wrap your brain around (along with the complementary design decisions in the rest of the framework) before diverging. The most baked-in assumptions in a well-designed system are the ones that exist for the strongest reasons.
>DHH says that explicit code favors novices and those with 'fresh eyes' on a problem while burdening experienced veteran programmers with bloat of boilerplate to read and to write.
I may as well come with fresh eyes for any code I wrote over a year ago.
That's irrelevant to his argument though, as he doesn't speak of coming with fresh eyes to the code, but coming with fresh eyes to the framework/language.
Even if you have forgotten why you wrote some particular code, you'd still remember the ways of the framework/language from other code you've since written with them, or generally because they're more general and fundamental than your particular code (the same way you'd remember the language's syntax, etc, but might have forgotten why you wrote some algorithm a particular way).
So, maybe, one should pick a language plus tooling that allows one to round-trip between various variants on the explicit-implicit dimension.
Various strongly-typed languages have something like that, with tooling that can add and remove explicit types from variable declarations and generic method calls, make calls of extension methods explicit, add explicit arguments for functions taking default values, etc.
That can give you longer code when you don't know the language and/or the program at hand well, and terse code when you do.
> On the other hand, Go is a bit tediously boilerplate heavy. I'm still looking for the right balance.
I think the trick is to come up with the right abstractions. I think we need to strive for abstractions that are relatively easy to grok (at least as a user of the abstractions), such that it doesn't feel like magic as much as it feels like a well designed language for the problem domain.
Of course, that's easier said than done. But most things worth doing are hard :)
> On the other hand, Go is a bit tediously boilerplate heavy
Go is a half baked DSL gone too far. Just like PHP, in 15 years, people will regret that the language wasn't carefully crafted despite its good "ideas", when developers will have to maintain million of lines go projects in production.
I was interested to hear the argument up until "Ruby on Rails" showed up. If "Ruby on Rails" does what you need, that's great, but then I'd say you're barely programming anymore, just like writing HTML/CSS barely is programming.
If we just go down another level (where you would implement something like Ruby on Rails), the same question appears. Do we favor explicit logic over implicit relations? At that level, I'd say the rule of thumb that "explicit is better than implicit" pays off, because it's often not that much more code, and a lot of code builds on top.
Preferring implicit code is optimization for the first person writing the code while making it harder to understand, debug, and update.
When code is written only once and most of the cost comes afterward, that seems like an impossible choice to defend.
Why optimize for keystrokes? How much do keystrokes really cost you? Why optimize for conciseness, if it comes at the expense of readability?
I've maintained some PHP code bases with tons of implicitness (magic methods in ORMs and strings dictating object behavior, mostly) and some that had no implicitness. Guess which were more expensive for the client and painful for me?
Another example: I've recently been working on a C#/.NET code base, and I was wondering where the HTTP request is converted into a C# object (in this case, a JSON to object deserialization).
Guess where I had to go to figure it out? Google. And it wasn't easy to Google. I couldn't follow the code because of the layers of magic and configuration that controlled my code, rather than actual code. It was a huge waste of time and makes the code base completely opaque sometimes. It's a ball of yarn, and you don't know where it begins or where it ends.
Why is my code behaving strangely? Is it because I wrote it to do something a certain way? No, it's because this random config file (that I didn't know existed) is dictating behavior instead of the code itself.
I can't imagine how anyone working on large code bases with other people would want to do this. Yes, implicitness is more fun and beautiful at the beginning, but it becomes a nightmare after a short time for anyone other than the original coder.
> Preferring implicit code is optimization for the first person writing the code while making it harder to understand, debug, and update.
I feel that this argument has been repeated so often that it's almost understood axiomatically: nobody thinks about what it means anymore, it just "seems true".
I posted Aaron Turon's (of the Rust core team) article that touches on these topics elsewhere in the thread (https://news.ycombinator.com/item?id=14280908), but this topic desperately needs a better definition of "implicitness".
Virtually nobody thinks that C programming is "too implicit" because it's "optimizing for the writer over the reader," and similarly, garbage collection is an extremely implicit mechanism that is widely accepted, including by some of the strongest EIBTI proponents.
On the other hand, as a strong proponent of abstraction and implicitness in many contexts, I was extremely supportive of (and helped champion) Rust's explicit error handling through the Result type.
I like Aaron's "reasoning footprint" rubric because it gives us a way to debate this topic without "implicit" defined in the eye of the beholder. Importantly, it allows us, as a community, to broadly accept changes like garbage collection and control-flow constructs without repeatedly rehashing the same old bumper-sticker debates that mar every generation of programming.
> nobody thinks about what it means anymore, it just "seems true"
I think about what it means constantly because every single day, I spend hours digging through documentation about the .NET framework instead of just following a call stack or reading code. It's excruciating.
Do I know exactly where to draw a hard, absolute line between "too implicit" and "too explicit"? No. But I definitely feel the pain of "too implicit" when it's happening to me.
> > Preferring implicit code is optimization for the first person writing the code while making it harder to understand, debug, and update.
> I feel that this argument has been repeated so often that it's almost understood axiomatically: nobody thinks about what it means anymore, it just "seems true".
Indeed. It seems just another way to not understand "everything is hard to read until one learns to read it".
> It seems just another way to not understand "everything is hard to read until one learns to read it".
So everything is equally difficult to read and understand? There are articles that say "great software is like Shakespeare" and others that say "great software is like Hemmingway". Which one of those authors is easier to read and understand, as someone who speaks English natively?
As someone who has inherited just two code bases, I know first-hand that there can be massive differences in how easy something is to read and understand. One of my companies had a code base that non-technical people could read and even send PRs for. Another of the code bases took months of ramp-up time.
> When code is written only once and most of the cost comes afterward, that seems like an impossible choice to defend.
> I can't imagine how anyone working on large code bases with other people would want to do this. Yes, implicitness is more fun and beautiful at the beginning, but it becomes a nightmare after a short time for anyone other than the original coder.
Good points, it seems like the arguments for implicitness may have been stronger in the past, when programming languages were less developed. Think of RollerCoaster Tycoon being written almost entirely in assembly in '99 by a single programmer. You'd have plenty of incentive for implicit standards. When you have modern languages with well optimized abstractions, all that implicitness ends up losing out. But if you compare the total amount of work that went into, say, Rust, with the cost of a single dude just building an awesome game, you see that explicitness only wins when it gets to cheat and use way more resources. So yes, explicitness is always better in the limit, but when resources are more constrained, implicitness is so nimble it will just crush the competition.
I'd interpret to mean that borrow checking for example can to be explicitly demanded in rust code, whereas in assembly a lot of the safety is manually assured and often enough only implicitly in the code.
> > Think of RollerCoaster Tycoon being written almost entirely in assembly in '99 by a single programmer. You'd have plenty of incentive for implicit standards
On the other hand you could say that it's the rust compiler source that is rather explicit about the mechanics and the game code would be explicit only by extension.
Since this thread is somewhat rehashing the old arguments ("you write once, but read many times, therefore EIBTI"), it might be worth reading a more modern take on the topic by Aaron Turon of the Rust core team in his essay about the Rust language ergonomics initiative[1].
> But this, in my opinion, is a misdiagnosis of the problem, one that throws out the baby with the bathwater. The root issue is instead: how much information do you need to confidently understand what a particular line of code is doing, and how hard is that information to find? Let’s call this the reasoning footprint for a piece of code. The pitfalls above come from the reasoning footprint getting out of hand, rather than implicitness per se.
> Does readability then demand that we minimize the reasoning footprint? I don’t think so: make it too small, and code becomes hopelessly verbose, making it difficult to read by forcing too much information into view at all times. What we want is a sweet spot, where routine or easy to find details can be left out, but relevant or surprising information is kept front and center.
Thanks for linking to this example. It's a great example of getting beyond slogans and trying to suss out the actual tradeoffs that come up in practice.
Although this article never mentions the word, it's really about abstractions, not "implicitness" per se.
No one would say that if you have an almost-perfect abstraction, like a physics equation for fluid dynamics, that you should go to the effort of dropping down to the lower level (specifying what each molecule should do, in this case).
The problem is that this ORM example is an extremely leaky abstraction. Several of the inferences it is making are inferences that are not predictable if you don't already know how it will work. And they are inferences that matter.
Anyone who has ever used an ORM knows that it seems like magic, right up until you try to run some large query and it takes an eternity, and then you have to unpack it all and add all the explicit stuff to fix the problem that you "avoided" in the first place.
The real art of programming is in knowing what needs to be explicit and what can be implicit. I'd agree that if something can be made implicit without mystifying callers, it should be.
ORMs make the obvious and easy things even easier. They don't help out as much with the hard stuff. The hard things are still hard regardless if you have an ORM or not, but just because an ORM doesn't solve everything it doesn't mean you should throw the baby out with the bath water.
You rarely have to "unpack everything", you usually just need to go down a level of abstraction or two to get at the actual database driver in specific parts of code.
The trade off of using an ORM is still almost always worth it in my experience, but it certainly depends on what your app does.
I would rather not leave myself a crime scene in my code that I have to figure out before I can do my work (especially when debugging!). I bounce around way too many systems in way too many languages to keep all the rules of every ORM and framework in my head.There's always a balance, but you can do really expressive things in the explicit. In the authors example of the Person, Ecto from Elxir doesn't favor magic happening for the has_many; you have to call out the validation AND relationships yourself. I think that's a good thing
schema "people" do
field :first, :string, null: false
field :last, :string, null: false
belongs_to: :company, App.Company
has_many: :assignments, App.Assignment, foreign_key: :person_id
end
def create_changeset(attrs) do
%Person{}
|> Ecto.Changeset.cast(attrs, [:first, :last, :company_id])
|> Ecto.Changeset.validate_required([:first, :last, :company_id])
|> Ecto.Changeset.foreign_key_constraint(:company_id)
end
And in the Assignment changesets you'd do the reverse for making sure the other sides' entities exist, or in the join table for many to manies.
I think the win readability wins later FAR outweigh the few validations I have to explicitly type out. A new person or someone under a deadline can quickly open one of the Ecto schemas, see all the relationships, see the validation being done, and see why stuff fails. And by turning it into a function you must explicitly call, it makes the data the most important thing and not the ORM itself. I could have called that function "parent_changeset" or "lawyer_changeset" if those made sense for what those functions did; they're not specially named. And then when you get that, you still need to actually apply the changes to have any effect.
This whole discussion feels similar to the explicitly/implicitly typed discussion. Sometimes I get annoyed by writing more code in an explicitly typed language. But most of the time, especially when I get back to the project after it being inactive for a while, I am so happy with the more explicit version of my code.
I tend to agree, and fall into the camp of favoring static types.
It's also extremely helpful when reading and debugging others' code bases. Having to construct systems of rules in your head sucks. Being able to follow the function calls and data around while validating assumptions with the REPL makes figuring external bugs out much easier.
Implicit code is indeed beautiful. DHH has also struck a great balance of implicit code remaining remarkably expressive with Ruby on Rails. It is the best example i have seen. But i would cite 2 shortcomings of implicit code:
- Implicit code increases the barrier of entry to new coders. This is counterintuitive as there is less overall code to learn. But implicit code often relies on auto-magic. Until you know the pieces of that magic, you are left confused. Case in point, as a web coder to find the file for `render user`.
- Implicit code works great until you need something to work differently than the implicit nature intends. This is because implicit code is also opinionated code. When your feature needs to fight against those opinions, you can expect to write a lot of highly explicit, ugly code to break that mold.
The slider of explicit-implicit will be talked about as long as code exists. Implicit systems result in less LoC, but you risk sacrificing expressiveness. For growing applications supported by growing teams, some of that time gain can slowly fade as your team gets younger and less experienced.
There is a large extra shortcoming: debugging. When reasoning about implicit code, there are more assumptions you need to make. Debugging is basically verifying assumptions until you find the disconnect between your reasoning and code execution.
The implicit code requires you check all of the implicit assumptions. Explicit code makes checking such assumptions much easier.
Excellent point! So far I get the impression that 'level of abstraction' and it's benefits/downsides can be separated into a bunch different categories (readability, conciseness/concision, ease of getting back into things after being away, protection from various classes of errors, etc.). Ease of debugging will now be another one on my list.
This article is ok as far as it goes, which isn't very far. Embracing implicitness can give benefits in the way that DHH describes. It can also lead to violations of your expectations.
Now, raise your hand if you honestly think that the answer is "embrace implicitness" or "embrace explicitness" full stop. In that context, it's worth noting that the Tao of Python was written well after the language was constructed, as a summary of the choices that were made. It was not originally intended to be a design document.
What determines the cases where one side of the tradeoff is good, and the other is bad? Show me cases where Rails does better than Django or some other framework and vice versa, and tell a story about how you identify those cases and judge whether the tradeoffs are worth it. That would be an article worth studying.
See, the thing is that even in this example he wasn't 100% implicit. Could rails magic up those has_many and belongs_to in method_missing? Absolutely. Do they? Of course not since that's "too implicit." For whatever reason, rails decided that columns are boilerplate and relationships should be declared.
I got bitten by a similar pluralization issue when I tried to use Rails. I spent a good amount of time messing around to try to figure out what I was doing wrong that was preventing Rails from pluralizing ETA like it did with my other classes. Turns out that to Ruby/Rails the plural of ETA is ETA... I haven't touched Rails since then.
I don't like over-explicit code like Go forces on you, but if I was forced to choose between over-explicit and over-implicit, I wouldn't have to think twice about going for the explicit language.
As always the answer is somewhere in the middle. For a core library that your company or even better everyone using the framework as you uses everyday implicitness is great. You invest a little bit upfront and it keeps paying off everyday. For one-off code you hardly ever touch you better be explicit. Of course there trick here is knowing which one you are writing. A good rule of thumb might be you start explicit and over time add an implicit layer on top while refactoring. With DHH's Active Record example that would have worked out nicely, since from the API perspective is really just reasonable defaults that could have been added once you realize that Active Record thing is actually being used in frequently.
Implicit code is great for frameworks. The kinds of things you have to do over and over again and a lot of the things Rails is great at. Create a database table, link it to an ORM, take params from a POST request, save them to that table...all that stuff should be pretty vanilla and absolutely yes, convention over configuration is amazing for that.
Explicit code is great for business level logic. In any business, and especially startups, things are changing so frequently that there's often very little in the code that could be set up as a convention and the accompanying "magic" that has to make it happen. Sales processes change weekly. Frontend features change daily. Reporting needs can change hourly.
In those environments you have to be able to quickly look at the code and see/know exactly what it is doing now and why so that you can be better able to make changes without introducing defects. Testing helps this a lot, but succinct, readable, explicit code makes life so much easier.
> Just on the database side, it implies that there’s a “people” table backing the “Person” record.
Plural table names are already pretty stupid, but using "people" vs "persons" even more so.
Implicitly having the table name match the entity after some case transformation is fine, but it should be more direct and not require an English dictionary to handle edge cases.
It's stupid, but it's approximately as stupid as having single object variable names, type names, collections, table names etc. all with the same, indistinguishable singular noun. It's also what programmers have dreamed about since we realised you didn't have to use punchcards, and it's what I tell people programming is like so they can understand what I do.
Rails lets you write code that can be read out loud and still make sense. You can have a meeting about an ActiveRecord class and not get caught up clarifying what you're talking about every step of the way. You could put some code in a presentation to your manager if you really wanted. In three years' time when you look at your code, you can read it aloud again and find yourself saying exactly what it does. Rails lets you describe ideas at a similar level of abstraction as common English speech. That has value, even if it lacks precision. If having a dictionary embedded into the macros is what helps people write code well, then they clearly value matching up with their brain over matching up with a compiler, and good for them.
Code isn't prose and you don't read it out loud or have it read to you. You read it with your eyes so the case of the word is clearly visible. You also have the additional context of the language of the code and syntax highlighting from your editor.
If I'm read app code, a "person" is a variable and a "Person" is a class.
If I'm reading SQL a "person" is a relation (e.g. a table).
I consider losing structure and adding complexity just to be able to read something out loud a net loss.
> I consider losing structure and adding complexity just to be able to read something out loud a net loss.
So would I. But Matz wrote early on that 'Ruby is designed to make programmers happy,' and that is behind lots of language decisions in Ruby and Rails. Reading aloud isn't an isolated end goal but more a way of describing that design principle. Writing Ruby should be a pleasant interaction, and Ruby programmers should feel like they are talking to a friendly machine. There are many ways to write the same code, and you can develop a distinctive writing style if you want. Many Rubyists talk about and write code like it's poetry, and feel like they have a voice through the code they write. Can you say that about a single Java programmer?
This isn't an accident, and effects like this are not frivolous entertainment the Real Programmers can ignore. It isn't a perfect vision, and you're absolutely right that sometimes it introduces complexity, but it's a real philosophy with tangible benefits, and pluralisation is a tiny part of that vision.
This is sort of a strawman, nobody on the explicit side of the debate wants people to write more boilerplate -- it's about how discoverable the code (and relatedly how many layers of indirection).
class Person < ApplicationRecord
belongs_to :company
has_many :assignments
end
For example what are the :company and :assignments symbols referring to? Why are they symbols? If they are models it would be better if they were required explicitly and that way I know where they are defined and can go read the code.
For every explicit thing in the code the programmer must pay attention to it when reading/understanding how the code works.
For every implicit thing in the code the programmer must have a mental model of what it is doing behind the scenes.
So one taxes the attention system of the programmer, and the other taxes her memory system.
Other things are both explicit and require a complex mental representation of state (Haskell), but the type system makes many classes of error impossible.
I think most people would agree with DHH's aesthetic when it comes to abstraction. What makes ActiveRecord's approach to abstraction get ugly in many cases is its coupling to the mutable state of the underlying database, which is not guaranteed to be in sync with the logic implied by the declarations, which may also have unforeseen timing side-effects.
Imagine the more typical scenario:
class BusinessContract extends ActiveRecord::Base
belongs_to :company
belongs_to :client
belongs_to :sales_rep
acts_as_attachment
acts_as_state_machine
acts_as_versioned
before_save :notify_rep
after_save :update_salesforce_api
after_update :generate_pdf
end
This class is now doing a great deal, and the user of the code needs to understand the time-dependent aspects of it and side-effects.
Oh yes. Two years ago, I had to work on an in-house IaaS written as a Rails app, where the models were particularly fat, with Instance#after_save hooks putting jobs in the worker queue to reflect the state into the hypervisor, e.g. starting a newly-saved instance or stopping/restarting an existing one. Trying to follow that code flow along was an excruciating experience.
I enjoyed this article a lot because this is a problem I struggle with often when coding. And not just at a particular point in the lifecycle of a project, it comes up all the time.
Decisions like this are, in my case, heavily influenced by how much I trust the rest of the codebase. Having "magic" code on top of a rock solid abstraction feels nice and safe. When I debug / troubleshoot I don't feel the need to step into the function to look at its implementation one more time.
I am much more likely to be explicit, suck it up and write the boilerplate code once again in a messy codebase where the abstractions change all the time (whether I'm the one who wrote the sucky code or not). In the end, I feel like it comes down to how well your objects / concerns are designed. When the concerns are properly separated and your objects are solid, magic code is indeed beautiful and extremely expressive.
I find this is the best way to achieve the balance people are looking for. I try to spread the explicitness across the abstractions, so that it doesn't feel too heavy at any one level. For example, I get a lot of mileage from succinct, least-surprising / 90% case methods that in turn call fully parameterized versions of themselves.
I wonder if maybe DHH is saying he prefers declarative code over imperative code.
I think it is often the case where one can argue that imperative programming is explicit, but in reality declarative syntax patterns are often easier to approach as a beginner and an expert.
As long as the implicit does the job it is fine. However when corner cases suddenly require the implicit to do something slightly different one is forced to dig into all the code that made the implicit work.
> It’s the same story with product values. If you say your product is “simple” or “easy”, you’re rarely saying much.
The descriptions for an incredible number of projects on GitHub lead off with something along the lines of "$PROJECT is a simple ...". My theory is that preemptively framing one's projects as "simple" might be a subconscious defence against accusations of triviality.
I agree with the author of this article. In general this is more about implicit functionality at the language level. Why is that much different from implicit functionality we get at the library level. In Java, when I use JPA, I am depending on massively loaded meaning behind a set of annotations... @OneToOne @ManyToOne, etc... at runtime you best know what all of those annotations are. I haven't really seen anyone that LOVES JDBC complain about tools like Hibernate/JPA. Yet it gives us lots of implicit meaning when we write this code that someone NOT familiar with JPA would be going crazy about - until they learn it.
To go a bit further and just state that suddenly it's NOT OK at the language level is kinda argumentless in my humble opinion.
And yes, there are degrees of acceptability. If JPA had an annotation like @OMGWTFBBQ and ActiveRecord supported something similar - it would be rejected. JPA and ActiveRecord do things like this, but much of the rest of these tools features outweigh their faults. I know for sure there are many Java frameworks that actually love it when you write your own annotation.
For example I use a @NoAuthentication annotation in Jax-RS. There is no standard annotation for that, yet in our company we all use it and know what it means. We mark this REST service as a service that anonymous users or authenticated users can call alike. This is because we favor a model of placing our authentication code inside the stack (in our case CXF) rather than adding code at EACH rest method. It's a very VERY implicit style, we can't look at and adjust our authentication model, but just the same, we SHOULDN'T be trusted to change our authentication model in each method.
Annotations are not exactly the same as implicits in other languages, but it's the closest example of something in Java that makes our lives much easier and hides a LOT of explicit info.
I think it's better to view this in terms of analyzing abstractions and their pros and cons, versus some vague notion of explicit vs implicit.
The more I learn, the more it seems an immense portion of high caliber software engineering is knowing how to approach and analyze very general questions related to abstractions, such as:
1. Given the current and expected future constraints, is this the best abstraction we can come up with?
2. Is the abstraction worth its cost? How heavily should we invest in using it, given how confident (or not) we are currently that it solves our problem well?
I think of abstractions as being everything from breaking an if condition into several local variables to splitting a large monolithic app into independent services. We could go further of course, but I think that gets the idea across.
There's a subjective balance to be struck between implicitness and explicitness. The example DHH gives are basically default parameters, which is a pretty reasonable amount of implicitness for me.
On the other end of the spectrum are things like Scala's implicit parameters, which I abhor.
You can still make good abstractions with explicit code, and remove most of boilerplates. But please at least be explicit about the fact you are using that abstraction.
One problem with Rails is that these abstractions are applied implicitly and burried deep in the framework. For DHH it's easy to know what it's doing behind the scene. Not for people who did not create these implicitly applied abstractions.
So.. Either you will have to learn ALL the Rails conventions, or you can create your own abstractions and use them explicitly. Also, it will be easy to put a breakpoint on the call site to your abstraction. Try that with Rails, you don't know what/where to put that breakpoint.
I don't think his example is great. Here is the equivalent in Django (in Python, a language that exposes 'explicit is better than implicit' as a core ideology):
class Person(models.Model):
company = models.ForeignKey('company', related_name='people')
How is this worse than the implicit version on the post? Basing it purely on a LOC argument is flawed as implicit doesn't always mean less code, and explicit doesn't always mean more code. In this example there is actually less code, you don't need a `has_many` at all (unless it's a m2m, then it's the same LOC).
In my experience the Ruby community is all about LOC arguments. So many blog articles I read about Ruby stuff is just "look what I can do in one line", and I'm like "maybe you shouldn't?". For example: https://twitter.com/stefanmajewsky/status/643380134037323776
I'm not familiar with RoR, but that explicit version is hard to read. However, I think the problem with implicitness is that when it becomes wide-spread, nobody can read the code with ease (i.e. everyone made their own implicit code, and it comes together poorly). I see nothing wrong with a little bit of magic, especially if it is something you use often.
Side note: the author has a short paragraph about "simple/easy" and "complicated/hard" products, which doesn't ring true with me. I would think most people would say their software product is "complicated, but easy to use".
When I develop in rails I tend to read the rails code a lot to understand what kind of implicit knowledge I was supposed to know. I suspect that veteran rails developers have done that a lot and also keep-up with framework upgrades when the assumptions are changing.
This is the tax we pay for being part of the DHH cult following (some against our will) :p
DHH never fails to impress. As usual, he cuts to the core of his beliefs, and delivers sound argument for them.
Program with Ruby long enough and it really starts to feel like a religious experience. You feel like you can literally do anything. Things you can't do with it just don't seem important anymore.
Puts it on steroids you say? Seems I'm in good company.
Bad joke aside, and I may be a naïve newbie, but what is stopping someone from writing tons of implicit code during their most productive work hours and when they're tired go back and make comments that make the code more explicit. Seems that this would get the best of both worlds in that you'd be able to be hyper productive when you've got the mental resources and then taking that extra time to make sure whoever stumbles upon your code later doesn't have to deal with a huge learning curve.
The problem with comments is that they are too often lies. As the code changes, the comments may not necessarily be updated, and in my experience, they often aren't. Automated documentation and validation are superior to static comments in general. Using a compiled language can help with some of this, and automated tests even more.
True, I know the JetBrains IDEs have automated refactoring, and I'm sure other IDEs do this as well, I'm not sure how much of additional overhead it would be to implement an auto comment feature/plugin that handles the job of generating/maintaining comments like #Person refers to columnn people and other things a more explicit language would simply state. Seems like something that could be fairly easily automated.
I may as well come with fresh eyes for any code I wrote over a year ago. I'm reading _my own code_ with an eye to figure out how it was put together. I should be courteous to my future self and to anyone else who wants to read my code.
I pick up a new framework every year or two. I've been a full time software professional for over 16 years and I'm picking up new tools all the time. I prefer tools that tell me things explicitly so that I don't have the steep learning cliff of grokking the entire system in order to be able to work with one little part.
And isn't this hypocritical? So many people (probably DHH too?) gush about how easy Rails is to get started with, but here DHH argues that it's so much butter for the veteran who knows what all the implied magic is. Doesn't all that implied magic also leave the beginner kinda helpless? There's so much Rails tutorial with the attitude "here, do this, trust me, it'll work". And I've found that trust to be misplaced. The underlying implied design isn't actually what I wanted, and it's really hard to get it to do what I want because the convention is so baked in.
On the other hand, Go is a bit tediously boilerplate heavy. I'm still looking for the right balance.