53 comments

[ 3.0 ms ] story [ 95.5 ms ] thread
Over here in the dynamic languages world (JS, Ruby, Python, etc) I often cry myself to sleep because I spend hours debugging something that turns out to be preventable with a const or a private function or something.
JavaScript tears :'(
I ain't got no tears using Typescript these days!
Python being Python, you can prevent non-malicious problems of the sort you mention (const and private functions / variables). Const via overwriting setattr / etc, private functions via stack inspection (among other things. There's a sliding scale of thoroughness versus clean code here.)

Though it won't protect you from malicious intent (though this is still the case with C / etc), and it's caught at runtime as opposed to at compile time.

> Though it won't protect you from malicious intent

It often does feel like junior developers are malicious. While I commend their ingenuity in solving the problem, code review can be a real schlep.

"There's an architecture used in video games for a long time now where rendering and other engine-level functions are decoupled from the game logic, and the two communicate via a local socket."

Is he talking about multiplayer or has anyone ever actually seen this?

Some games (even in singleplayer) host a "local server" and environment information is passed to that. If you're making a networked game it makes sense as you'd be creating the server anyway, decoupling is nice and less codebase to manage.

Steam's Source games, UT do this, if you open up the console and scroll up you can see the local server initializing.

Ditto, Minecraft.

Not sure about Supreme Commander but it wouldn't surprise me if it did.

For Source, that's probably the GoldSrc/Quake legacy. You can see this somewhat in the source on GitHub - WinQuake has both client and server mashed into one process, but there are clear divisions, with the cl_.c/h and sv_.c/h files.
I met a longtime game dev at a reasonably well-reputed game company talk about how they did something similar where they did a message passing architecture between threads in a single process, and how this also helped contain old code that people were afraid to touch while allowing newer practices and refactoring to happen in other regions.
Most games that do non-authoritative peer-to-peer multiplayer ultimately do something similar for singleplayer. Although it may not be as far down the layers as a local socket.

Also, Minecraft uses something vaguely similar, with a separate client and server, with the client and server communicating locally via shared memory (effectively a local socket, but not bothering to bounce through the OS). Though Minecraft's an odd case - they used to not do it this way, and switched to it, and in the process broke a lot of things.

> If they're not in the tutorial, examples, or reference, you don't even know they exist. If you use the header file for documentation, and internal methods are grouped together beneath the terse comment "internal methods," then why are you calling them?

Autocomplete is a thing. Even if there is a big comment explicitly saying not to use a function, if it doesn't show up in the autocomplete window someone will inevitable use the function anyway (and sometimes even if it does).

If I'm reading this correctly, you're saying that someone will use a function for which there is no documentation, no external reference, and the user hasn't even seen the code (since they didn't see the comment saying not to use it)? Just a function name and (maybe) a method signature in the autocomplete? The caller deserves whatever woes befall them.
> The caller deserves whatever woes befall them.

Why?

Yes. All the time, if the name and signature are enough to tell you what it does and how to use it, which is normally considered a good thing.
The caller does indeed deserve whatever woes befall them, but sometimes the caller is loud enough, powerful enough, or prominent enough that you have to support them... even when they're bypassing your actual API and hitting your internal methods.
Not only will they (attempt to) use them, but they will decry your half-assed documentation, and be right to do so.
In my view the purpose of const/protected/internal/static etc is not so much to prevent mistakes by myself or others, but to embed in my code a 'living documentation', enforced by the compiler.

If I mark a method as internal, that means I intend it for reuse within the library but don't expect it to be used by any external caller, which means another developer can come along and make changes to it without needing to worry about anything outside the library (short of those in C# using [InternalsVisibleTo] or similar, which hopefully is restricted heavily to obvious test projects).

By decorating my code appropriately, I don't need to write comments most of the time, which means I don't run the risk or filling my code with out-of-date or poorly understood annotations that become useless almost as soon as I finish writing them.

I agree with this. The code is not just a message to the compiler to build the binary/bytecode, it is also a message to other programmers. As such there is value in annotating things in that manner.

Even in Python, a very dynamic language, there is a convention that methods that start with _ are private. And I often also separate them into a different (bottom) section with a big header saying this is the private block of methods. So in that case there isn't a compiler rule but rather a common convention (which is kind of a step above comments if you wish).

If you want to enforce this more stringently, you can even prefix methods with __ (double underscore). Python will mangle the name:

    __mydef -> _myClass__mydef
You can, of course, still access the method, but it's very useful for keeping implementation details of a base class out of a subclass. This way, your subclass can have its own `mydef` without overriding the base class.
I disagree with the exact statement but also agree with its sentiment. The functionality of a language should be applied for a specific engineering reason, and protection features are sensible at the right scale. But at no point does it replace documentation efforts. You can surface the simplest possible interface to your users and still have questions and concerns that need supplementary text.
Strongly agreed.

> const

I don't have to worry as much about iterator invalidation with containers, because I've documented that I'm not going to change it. Bonus points: I know the documentation isn't lying like it usually is. (Caveat: Someone const_cast<>ed it away, and my trust is forever crushed ;_;)

I don't have to scan for side effects to figure out if this const variable is an alias for "the initial size of the container" or "the number of elements remaining to process, which just happens to start as the initial size of the container".

It's clearer if the vector normalization function is "mutate in place" or "return a copy". I've seen enough code that does both that I can't necessarily rely on eyeballing the return type.

I can slap it onto existing, likely side-effecting code, to help make sure I don't miss anything when refactoring to an immutable style, because e.g. I'm preparing for multithreading.

That it helps catch accidental mutation is merely a happy side benefit.

> protected/internal

These are worth it just to unclutter my intellisense, which is how I read most of my documentation on some projects. Because it contains all the documentation, or because the API is straightforward enough that it contains all the necessary documentation.

> static

I've had this both help and harm. I'm typically using C++'s anonymous namespaces instead. I have collisions frequently - localized log methods, error tables, local module allocators, etc.

This is less about "I want to prohibit external calls" and more about "This is throwaway local code that I don't want to painstakingly disambiguate from every other possible chunk of throwaway local code." - I see frequent enough collisions with local log methods (which annotate before calling global log functions), error tables, shorthand aliases to specific allocators, etc.

And now to quote from the article instead:

>> What all of these fine-grained controls have done is to put the focus on software engineering in the small.

I'll certainly agree that fetishizing these micro tools to the detriment of larger system issues is bad. That's more along the lines of "insufficiently wide paranoia" than "the wrong kind" though.

>> There's an architecture used in video games for a long time now where rendering and other engine-level functions are decoupled from the game logic, and the two communicate via a local socket.

You can overcouple via RPC over a socket just fine. Like module level protections, it's a mere speedbump against making a bad decision. There's no panacea.

Static also allows the compiler to make better optimizations, knowing that nothing outside of the current compilation unit will ever use it.

Const also allows the compiler to optimize better with the knowledge that the called function won't perform any writes to the object state.

Access modifiers give you a way to separate out functions that can be called externally vs those that shouldn't. The API doc generator won't know which is which unless you mark these correctly. You use access modifiers to show intended use and keep things clean, not to lock someone out. Same goes for internal classes.

In practice const very rarely allows any interesting compiler optimizations (passing a reference to a const local variable to a function whose body cannot be seen is pretty much the only place where it's safe to do so). The const overload of a function with const and non-const versions could in theory be faster, but copy-on-write data structures are the only common example of that actually being the case.
To be clear, static typing is the zenith of of compiler optimizations and in many ways the programming "ideal," however in the context of loosely typed languages (and their OOP abstractions) and I think the author is missing a significant use-case.

Separation of responsibility/decoupling on the service level are good principles to begin with, but in an OOP paradigm where you are defining/exposing classes and methods, these keywords are really useful for grouping functionality when you adhere to "contract-driven development."

When used correctly they help you abstract the interface for your class (ie, the public implementation-agnostic methods that provide interoperability between the service and the program as a whole) and separate it from methods only designed for internal use (within the class itself).

Often times those abstractions are essential (DRY principles) and the class is the proper place to encapsulate them, however you wish to clearly designate that "this method is self-modifying, limited in scope, and does not interact with or is required by any other class in any meaningful way," and for that it's quite useful.

A nice standard reply. But how do you verify that your usage has the impact you think it does?

When I started estimating the complexity of my code - just using rule of thumb and line counts - I found that most uses of classes were unjustified. The "right size" of a class was quite large, especially so in the top level of an application.

Well, I can't speak necessarily for the way you write or view code, but for my process I've found its impact intrinsic and clearly definable.

First I should note that I follow a few basic principles/sets of principles when programming loosely-typed imperative languages.

1) Single-responsibility for classes and methods, with each method coming in around <= 20 lines. In total the majority of my classes in enterprise level applications (supporting 100's of millions of users & responding to internal events with meta/statistical analysis) rarely grow beyond 200 lines per class. Usually whenever I hit the 2-500 line range I'll find that many methods can be logically abstracted to a few core traits in order to take advantage of multiple-inheritances without overcomplicating the central registrar or DI patterns.

2) I build an interface before building a class--the interface defines what methods will be available to the application/world context (thinking as per an API interface), as well as what type(s) should be received and what type(s) should be returned. By type hinting/checking at this stage and ensuring conformance to an interface, you can swap out implementations easily later, as well as have a general map or "spec" before you really start hammering the nails--this helps in staying organize and weeding out bad architecture decisions early.

3) I keep these public methods simple, so that I can clearly detect failure points and debug based on input/return types for the 'service' as a whole, and then I use protected methods internally similarly to data pipes in functional programming; each method is clearly named, has a specific transformation it applies, and acts on a series of (n) objects by mapping transformations vs iterative loops which precludes un-terminated conditionals and other type-juggling weirdness.

This all results in software that's very concise (IMHO), runs well, and is quite simple to test. I can inherit any class from a testclass, in order to test the protected data pipes with any sample streams. I can verify the I/O types for all interfaced methods of the class (via functional tests), and tie it all together neatly in knowing that I can pull up any file and clearly differentiate between what formats/returns/transports data, and what mutates it and/or the "state."

The process is by no means perfect and I continue to learn in my pursuits as do we all, however this structure has worked well for me consistently on the types of large projects where others have failed, and in that context I feel it's worth expounding.

I completely agree we should stop being paranoid about what's going to happen when we let other people loose on our beautiful code.

But I don't think having private methods or marking variables const is about paranoia, it's about communicating intent to the people who come behind you and using the compiler to enforce that intent. Every piece of extra context you can give to someone reading your code helps them understand why it's there. Identifiers like const and private are almost like nonverbal communication for code.

This post resonates with my experiences of the last 6 months. I'm a very good web developer, and have been working for a while now on a couple iOS projects. Coming from javascript which everyone seems to regard as dangerous and overall terrible, I expected that after some time I would get used to and appreciate working in Objective-C and Swift. Nothing could be further from the truth. While you can shoot yourself in the foot with javascript, in practice it's not really an issue. The absurd lengths that these languages (particularly Swift) go to make you less productive while preventing errors is ridiculous and unhelpful. I get as many runtime errors writing Swift code as I do javascript, but my code is riddled with as? or foo.bar!.method() cruft just to satisfy the compiler and get on with my life. Rather than thinking about how to solve a problem I'm strategizing how to work within the limitations of type systems.

As someone who learned to code with javascript, ruby, and python, I'm not sure I'll ever really appreciate these "nanny languages".

This old canard.

I am a professional Objective-C developer and have been for several years. Type safety is not an absurd length. Eliminating an entire class of errors from your program by having the compiler infer and enforce types is not ridiculous. Using a type-safe language is a very good idea.

We are human. We have stupid unchecked nil object errors come up in our code bases all the time. Swift will ensure that does not happen again. That's like the least part of what I am looking forward to.

If you are having trouble writing a program that compiles with strong typing, I don't know what to tell you. Using types is nothing more than stating what you expect the shape of the data to be in and having the compiler make sure that is so.

Is it really worth that much though? I'm a Ruby developer and I've only very rarely actually wanted a type system. I've often found that I could work around its lack by implementing class checking and raising an error whenever the wrong type gets passed. It gave me everything I wanted without having to give up dynamicism.

I think types probably work well in situations where you don't know what kind of code you're going to have to deal with in the future. It depends on the type of organization you're in, not the type of problem you're trying to solve.

I think any respectable programmer should try to avoid having to have people hook into his code at any level other than the level he defines. To interface at the level of data, not client code. If you are needing typing to solve your own inadequacies as a programmer, you should become a better programmer rather than expect your language to do that for you.

With a dynamic language, you can get all you could have wanted from a type system without having to infect your whole codebase with it. Most of the time, you just don't need it.

I agree that some type systems are infuriating. They demand highly verbose code, and offer almost no protection in exchange for your effort.

However, not all type systems are equal. Haskell (for example) almost never requires explicit type annotations. It has a type inference system that is sometimes frighteningly good. You can express a huge amount of logic through the type system and enforce very non-trivial constraints.

I've been writing Ruby for 7 years, and I've loved every moment of it. It's a wonderful language. That said, I usually have to spend quite a bit of time getting my code to work correctly. In Haskell, by the time I get to the point where the type checker approves of my code, it usually works as I intended the very first time I run it. It's a wonderful feeling.

The problem isn't the type annotations. The problem is losing the meta-programming capabilities you gain when you can make everything an object with a class. These capabilities are really useful when you don't really know what you're doing yet, which, for me, is almost all the time.

Knowing at any time I can take the class hierarchy I just built, turn each class into an instance of an object, and store those objects in a database, with a 10 minutes and a fancy bit of code, is much much more useful than having a babysitter.

I don't know much about Ruby, I will admit.

But dynamic typing is terrible for large code bases. The only place I allow it is at the very edges of our system, where we are transforming the proper types of the internal system into POD-objects to be serialized and returned to the JS/HTML front-end. I'm not willing to give up the compile-time safety of breaking the build if someone inadvertently assigns a string to a number, or an object to a primitive in code that they check in, in place of unit testing that, odds are, will be incomplete or otherwise broken and won't catch the problems that static typing catches.

> But dynamic typing is terrible for large code bases.

Yes, you can get into a lot of trouble very quickly as your codebase grows if you don't have a heavy security blanket. But the more Ruby I write, the more typing looks like Linus from Charlie Brown's blanket.

Because you don't need a monolithic code base any more. You can break it up into smaller pieces that interact with each other using POD objects.

When I start to have type problems in Ruby, I start looking around for a domain concept that I need to extract into a gem. The codebase never grows to a point to where it becomes a serious problem.

> If you are needing typing to solve your own inadequacies as a programmer, you should become a better programmer rather than expect your language to do that for you.

That is one of the most ignorant things I have ever read.

Perhaps if you are needing dynamicism to solve your own inadequacies as a programmer, you should become a better programmer rather than rely on your language like a crutch.

Perhaps if you were an adequate programmer, you wouldn't have such a hard time understanding types and getting the compiler to accept your program.

I cannot imagine a worse programmer than one who willfully rejects a tool which improves a codebase to great extent. Typed languages are strictly more powerful, and you cannot get the same code guarantees from a dynamic language.

I don't want to be "that guy" but it sounds like you're not writing Swift, you're writing Javascript in swift syntax. Optional as part of the type system instead of falling back on nulls and exceptions is IMO a very good thing. Try to embrace it with an open mind for a few years (yes, years) before declaring it a useless pain in your behind.
These protections might be overly fussy in small, one or two person projects. But for programming in the large they are essential as they represent a social contract between teams.

If you call only my public methods, you can generally expect:

   - if my public methods don't work properly, it's a bug
   - and I, as the library author, promise to care
   - if the internals of my library change, I'll keep my public
     functions working if possible.
Disregard my protections and all bets are off. If you cast away const on an object I gave you and then call a non-const method, you might be totally violating the threading model of my library. If you write and complain about this, I will ask why you thought it was ok to cast away const.

Without these annotations it would be much harder to effectively communicate and enforce the parameters within which the library is "promised" to work.

`private` is a joke in C++. Coupling happens at the header file level, not the class level. Any C++ programmer who cares about reducing dependencies should be thinking about header files, not classes.

Man, we really need a module system.

I think the problem is not that `private` exists, but that it is taught/sold to programmers as a silver bullet for making software architectures better. People think "Decoupled systems are good. `private` hides my variables from the outside world. Therefore, if I use `private` variables, my system will be decoupled." This is false. A program can use `private` extensively and still be tightly coupled.

So, I agree with the author that `private` encourages myopic engineering, but I think education is a better way to fix the problem than removing the features.

>Man, we really need a module system.

And you might be getting one: https://isocpp.org/files/papers/n4214.pdf

It's under consideration for C++17, as I recall.

EDIT: Also, CLANG allows you to use modules right now, but IIRC those are CLANG specific extensions and the final module spec. may or may not be compatible with them.

Perhaps 'private' is oversold. It can be used simply to isolate variable changes. So you don't have to grep the entire source for references. So you can make changes confidently. Yes there can be some leakage; but don't throw the baby out with the bath water.
Some of these things are simply to "nanny" us, but there's a very good reason for private / protected: The Namespace is a precious and limited resource. It's a good-thing(tm) not to pollute it with internal details, and what is unexposed need not be well-named, which is after all one of the 2 hard problems (along with cache invalidation and fencepost errors).
I usually love James Hague's posts, but this one has his head buried deep in the sand. And he's enough of a veteran to remember early Lisp and Smalltalk systems, where excessive openness in all aspects looked awesome for prototyping/hacking or academic toy programs (or what is considered toy-size in the last 20 years anyway), but was a disaster even for the mid-1990's definition of programming in the large. Attempts to discipline these languages came too late, so they got overrun by competitors with lots of "paranoid" rules like C++, Java, hell even Eiffel. (Not the single reason of course but one important reason; not even the gurus could help themselves to not abuse dark magic and write application code that would be compatible across minor updates of the language/frameworks, let alone other compilers/VMs/platforms.) If you need extra evidence with more current technology, see most "bad parts" of Javascript (successful, but in the same way that the Titanic was wildly successful for one week; people who build large JS apps have been in a mad rush to fix or replace it for the best part of the last decade, hopefully with ES6 coming as a first major win in that direction).

TLDR sorry James, we have tried the elegance of extremely simple and open language/runtime architectures, and that was always an abject failure.

> we have tried the elegance of extremely simple and open language/runtime architectures, and that was always an abject failure.

Just because it was tried in the past, doesn't mean it wasn't the right direction.

> Attempts to discipline these languages came too late,

So, as an industry, we were learning and now we know some basic principles. Discipline does a great deal for Erlang. Ironically the game sockets he describes are basically how Erlang functions at scale. To say the practice is always an abject failure is burying your head in the sand.

You didn't understand what I meant by "discipline". I didn't mean best-practices -- i.e. programmers learning to not abuse openness without any help from the language. What I meant was adding the missing controls to languages (making them more similar to the languages James criticizes) so proper discipline could be enforced: visibility, module systems, restrictions to change core runtime classes, optional static typing etc.
I wonder how much the software state of the art is advanced by software developers as opposed to software maintainers.

When you are working to refactor software written by others, things like "private methods" are a gift. If you are trying to refactor some piece of code, you only have to make sure that the callers within the said class are modified to guarantee that the codebase is not broken.

I don't think a lot of people understand that maintainers in the real world do not have the time to read and understand every line of your code. And the toughest part of doing software maintenance is figuring out how much you can safely ignore. I feel qualifiers like "private" were designed to help with this problem.

I also severely dislike the architecture proposed by the author with loosely coupled services talking over a socket. This style of code is only maintainable if you understand the entire system inside and out. For example, lets say the maintainer receives a ticket that says 'Report X has wrong data'. She will start investigation with the question : 'Why does it have wrong data ?'. She will walk back up the call tree looking for why and eventually learn that the data coming off the socket is wrong and that is where the trail ends (unless there is a document describing who is responsible for putting said data there).

I have faced this issue in real life. I can understand when this style of decoupling is necessary to improve modularity, but it does not have a positive maintenance impact.

Apple have added a fantastic thing to iOS 8 and OS X 10.10 that I am terribly interested in but have managed to find nothing about beyond this post[1]. I think you'd be interested, it addresses exactly the problem you describe by including information from other threads and processes to crash logs.

[1] http://www.objc.io/issue-19/activity-tracing.html

I actually want _more_ possible restrictions such as limiting rage of values, or allowing only printable characters in a string.

As other comments say, its a documentation to future self and others.

Most of the commenters here didn't seem to read James' whole article. He's not saying that you should throw away isolation but that isolation should be taken care of from your architecture, not your programming language. Of all the places that should be up on a microservices architecture and see the sanity of it, you would think that would be here on HN, no?
I find the assertion that compiler enforced discipline is paranoia, too hippy.

What it does is free you from a big overhead, which is keeping track of the access levels for all of your instance variables. Moreover, documenting enforcements is a very shaky way to go about it. It requires military discipline. I've seen time and time again, documentation which didn't get changed with the code.