|
Personal Info:
Blogroll:
Disclaimer:
The content of this site are my own personal opinions and do
not represent my employer's view in anyway.
© 2009, Joe Duffy
|
|
 Sunday, December 28, 2008
As embarassing as it is, the errata for Concurrent Programming on Windows is non-empty.
I've posted an initial listing -- full of primarily simple typos like misplaced commas -- at http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html#Errata.
Sincere thanks to everybody who has reported errors thus far. If you find any additional ones, please email them to me directly: joe AT bluebytesoftware DOT com. We'll attempt to fix as many errors as possible in subsequent printings of the 1st edition and, if that fails, they'll make the 2nd edition.
I've spent the past few months (from September onward) travelling approximately 75% of the time. As a result, I may be slow responding to email concerning the book. I've also not finished putting together the code samples up for download; my current ETA for that is mid-January 2009. I already know there are a few more errata entries lurking within, due to some last minute typographical updates made late in the editing process. If only word processing software came complete with built-in compilers... (excuses, excuses)
In any case, I'd love to receive feedback on the book. Even if it's not about an error. Things you like, things you'd like to see improved, things you wish I'd not written about, requests for clarifications, etc. Just drop me an email. Cheers.
 Saturday, November 29, 2008
I've had an obsession with programming languages for some time now. This probably began the first time I learned of LISP. Most people I know have had a similar "Ah-Hah!" moment associated with LISP, but it was when I first truly realized the deep extent to which a programming language shapes thought -- sometimes in negative ways. LISP put it all into perspective.
Since then, the obsession has only become worse through my employment at Microsoft, where I've had the privilege to work alongside and interact with some of the greatest minds in programming languages. This is an absolute honor. I worked on a few compilers and did some language design, particularly when on the Common Language Runtime team, and my favorite project today is my work on type-system support for static enforcment of concurrency safety and guaranteed isolation. I have found great joy in applying underlying concepts in more niche (and extreme) languages like Haskell to more mainstream languages like C#. My favorite pasttime is tracing back the lineage of languages to their earliest ideas, especially when this leads to the unearthing of a subtle commonality among them. I have been designing one of my own and, while it is undoubtedly a 5-year project that may never see the light of day, I do it for the love of languages.
This book has been stewing inside me for a while now. And after seeing Guy L. Steele and Richard P. Gabriel's infinitely beautiful "50 in 50" presentation at JAOO this year, I decided it was time for it to escape.
Notation and Thought: Behind Computer Science's Most Influential Programming Languages
“That language is an instrument of human reason, and not merely a medium for the expression of thought, is a truth generally admitted.” --- George Boole, Laws of Thought
Programming languages are not only a notation for expression, but also a medium of thought, akin to the duality between natural written and spoken languages. If you can think it, you can create it. The reverse is also true: if a language poses impediments to your thought process, certain solutions to problems are simply unfathomable. Languages are therefore not just what you see “on paper”--each is a unique tool that can substantially limit, or expand, the creative freedom of the programmer in whose hands it sits. Good languages get out of the way, and great ones do a whole lot more.
In the early days, there was of course nothing that resembled modern day languages. Computers had to be told what to do in excruciating detail. One only has to look at modern day assembly language to see that programming a computer in this manner constrains creativity and slows progress. Alan Turing didn’t even have that when he wrote his classic On Computable Numbers with an Application to the Entscheidungsproblem paper, but he at least managed to solve some simple problems: by moving a tape reader and reading and writing symbols, he was able to create the modern day equivalent to subroutines and even add up a number or two. But our industry would have never seen radical advances in enabling technologies, and widespread computer use, that we enjoy today without significant advances in higher-order abstractions.
Plankalkül, or the plan calculus, is widely recognized as the first real programming language. It was designed by a German computer engineer, Konrad Zuse, and first written down in an unpublished manuscript in 1943. The language offered composite (albeit simplistic) data structures, arrays, named variables, subroutines, and moderately sophisticated control flow and looping constructs. Although it was never used in practice, Plankalkül was surprisingly ahead of its time. It was a big step towards more abstract problem solving.
It should be no surprise that subsequent programming languages are as varied in their design as the humans that created them. This fact can be seen by examining the ensuing decade of computing post-Plankalkül. The 1950s saw the invention of four new major languages that fundamentally shaped the future of language design. FORTRAN, or the FORmula TRANslation language, specialized in describing transformations on data and numerics, and was the first non- assembly language to reach widespread use in performance sensitive situations. LISP, or the LISt Processing language, was developed for symbolic processing and, eventually, found a home in artificial intelligence, pioneering many techniques that are still in use today such as first class functions as data, a recursive style of programming, and garbage collection. Its principles were derived from the mathematical logics of Alonzo Church and Haskell B. Curry, notably Church’s lambda calculus from the 1930s. ALGOL, or the ALGOrightmic Language, focused on describing algorithms elegantly, kick-started the imperative family of languages (of which many popular industry programming languages like C++ and Java are members), and later set the de facto standard style for Computer Science education curricula. Its method of encoding algorithms with assignments was far closer to the von Neumann architecture than was LISP, making the resulting programs behave predictably and efficiently. Lastly, COBOL, or the COmmon Business-Oriented Language, became the first domain-specific language (DSL) that targeted non-programming business and finance experts, broadening the general accessibility of computers. Each of the four has had a crucially important role to play in the history of programming languages.
There has been no shortage of language diversity after the birth of the initial four. In fact, hundreds of languages have since come and gone, some enjoying brief or extended periods of popular use. All that have since come have been deeply influenced by the pioneers, but have also contributed a handful of innovative new ideas that help programmers more clearly think about and express solutions to real-world problems. The lineage of languages has branched off into separately named family trees--such as imperative, functional, logic, declarative and domain-specific--only to reunite intimately with each other down the line. Indeed, it really is just one big happy family.
This book traces this lineage through the most influential languages--those that have deeply impacted the way that programmers think and write--and provides insight into the motivation behind them, their major influences, and the important features that each language contributed. Throughout, it is my hope to develop within the reader a new appreciation of the art of programming computers, an understanding of the impact that language has on our thinking, and an excitement about the future of language design that lies ahead.
Joe Duffy November, 2008
 Tuesday, November 04, 2008
Type classes, kinds, and higher-order polymorphism represent some of Haskell’s most unique and important contributions to the world of programming languages. They are all related, and began life as type classes in Wadler and Blott’s 1988 paper, How to make ad-hoc polymorphism less ad hoc. Eventually, Jones introduced the (then separate) concept of constructor classes, in his 1993 paper, A system of constructor classes: overloading and implicit higher-order polymorphism. Eventually these two ideas were unified into a beautiful single set of features (namely, type constructors and kinds) in Haskell.
In this short essay, I’ll explain what these things are and why I’m sad that we don’t have them in C#.
To take the simplest motivating example, say we want to define a generic square function:
square x = x * x
Given a Hindley-Milney type system (with type inference), how should the compiler type this function? The challenge that immediately arises is that, to know the type of x and the function’s return value, we must know something about the function * being called within the body of square. But to know something about that function, we’d need to know the type of x. We’ve entered into a cycle, and have hit a wall. Clearly the type will be something generic, but polymorphic on what?
Imagine that we could infer the type of the * function as follows:
(*) :: a -> a -> b
In other words, * is a function that takes two values, both of type a, and produces some value of another type b. We know its two arguments must be of the same type because in square we pass the same value x to it twice. Given this typing for *, we could then type square similarly as:
square :: a -> b
In other words, square takes a single value of type a and produces a value of type b. The constraint on the type a here is, of course, that some function * is available that is typed as taking an a as input. There’s no obvious way to capture this in the type system, though we might conceive of something like:
square :: (* :: a -> a -> b) => a -> b
In other words, given a type a for which some function * is defined, which takes two a’s and returns a single b, the type of square thus takes an a and produces a b. You can’t say that in Haskell, although we’ll see a bit later that type classes allow similar constraints (with “=>”) to be written.
While this hypothetical typing is extremely general purpose, it would produce considerable challenges in its implementation. Standard ML throws up its hands and infers all mathematical operators (like *) as working with floats, meaning that all of the types above (both a and b) will be inferred under the type of float. (*) is of type float -> float -> float, and square is of type float -> float. Similarly, F# assumes you’re working with ints. Both Standard ML and F# have amazingly rich type inference systems, but this begins to run right up against the limits of what they can do. We’ll see some harder examples shortly.
You can probably guess that Haskell’s solution to this conundrum is to use higher order polymorphism with a feature of its type system called type classes. They allow us to classify types much in the same way types ordinarily classify objects. We can classify the set of numeric types as follows, for instance:
class Num a where
(*) :: a -> a -> a
… other numeric operations …
And then we can go ahead and provide concrete mappings for integers and floating point numbers:
instance Num Int where
(*) = addInt
…
instance Num Float where
(*) = addFloat
Each instance of the type class (in this case, Num) is a bit like a dictionary mapping the named functions (in this case, just *) to other functions that are defined for the concrete type (in this case, supplied in a’s stead). With this information defined, the Haskell compiler can now infer the type of square as:
square :: Num a => a -> a
This inference really just says that the function square is defined for all types a that are in the type class Num. The “Num a =>” part is a bit like a C# generic type constraint, in that it restricts what kinds of a’s can be supplied. Given what has been stated thus far, that’s just Int and Float. So we can only call the square function with types on which multiplication is properly defined, which is exactly what we want.
At this point, we might want to try defining a similar thing in C# using generics. (And for this simplistic example, and others like Haskell’s Eq a type class, we will succeed.) There are two basic ways we could achieve this. The first is to define an INum<T> interface (or abstract class—pick your poison), and give it an instance method to multiply the target with another number:
interface INum<T> {
T Mult(T x);
}
We would then have the basic numeric data types like Int32 and Float implement INum<T>:
struct Int32 : INum<Int32> {
public Int32 Mult(Int32 x) { return value * x; }
…
}
struct Float : INum<Float> {
public Float Mult(Float x) { return value * x; }
…
}
Given these definitions, it would be a breeze to write a Square method that only operates on INum<T>s:
T Square<T>(T x) where T : INum<T> { return x.Mult(x); }
Thankfully, we can recursively reference the T from within the generic type constraint.
Now, of course, there’s no way the C# compiler would infer the necessary INum<T> constraint. But given that we don’t have rich type inference (aside from for local variables) in C#, this doesn’t pose any new problems. Another slight annoyance is that you need to modify the source type to declare support for INum<T>, when a perfectly reasonable implementation could have been provided “from the outside,” but you’ll find that this will only occasionally get under your skin.
The second way we might go about this is to take an approach similar to .NET’s EqualityComparer<T> class, where we have an abstract base class that represents the ability to do something with instances of Ts. And then we only provide implementations on concrete Ts for which that ability makes sense. For example, we could have a Multiplier<T> that looks a lot like INum<T>:
abstract class Multiplier<T> {
public abstract T Mult(T x, T y);
}
Multiplier<T> on its own isn’t usable. But we can provide implementations for Int32 and Float:
class Int32Multiplier : Multiplier<Int32> {
public override Int32 Mult(Int32 x, Int32 y) { return x * y; }
}
class FloatMultiplier : Multiplier<Float> {
public override Float Mult(Float x, Float y) { return x * y; }
}
// And so on …
Now we can write a slightly different Square method that takes a Multiplier<T> as an extra argument:
T Square<T>(T x, Multiplier<T> m) { return m.Mult(x, x); }
Now there isn’t any kind of generic type constraint on Square’s T, but of course we can only call it if we have a concrete instance of Multiplier<T> in hand. And by definition that means there is a Mult method defined that we can call. (This isn’t wholeheartedly true. You can of course call Square<U> for any U, passing in null as the second argument. But presumably the method would check for null and throw. This is a real limitation, however, which would likely push us back in the direction of the original interface solution. If we had non-null types, we could get closer to a fully statically verifiable solution.)
Aside from a lot more typing, and the lack of rich type inference, we seem to have reached parity. The simple examples provided in the literature and Haskell’s Standard Prelude can be implemented in such a fashion. But we are kidding ourselves if we think these are the same thing.
The main problem is that C# doesn’t support higher-kinded type parameters. We haven’t yet seen a type class in Haskell that fully exploits this capability, but there are several. The simplest one I know about in the Haskell Standard Prelude is the Functor type. (Monad is also a great example, but is a bit more complicated (and sufficiently frightening) that this will be a topic for another day.) Functor’s definition is:
class Functor f where
fmap :: (a -> b) -> f a -> f b
The Functor type class offers a single function, fmap. It takes two things—a function that transforms a value of type a into a value of type b and some functor value of type f a—and returns some new functor value of type f b. This looks like an ordinary type class, except for one funny (and subtle) aspect. Functor abstracts over type f, but notice that we’re using f in fmap’s second argument and return type by actually constructing it with two other types a and b! In case you’re having a hard time thinking in Haskell, it’s as though we tried to write this in C# using our interface trick from earlier:
interface IFunctor<T> {
T<B> FMap<A, B>(Func<A, B> f, T<A> a);
}
This won’t compile. We can’t refer to T in the typing of FMap as T<B> and T<A>: it’s not expressible in C# and .NET’s type system. Let’s pretend for a moment, however, that we could. What is an example of class that might implement this? How about something that deals in terms of Nullable<T> instances?
class NullableFunctor<T> : IFunctor<Nullable<>> {
Nullable<B> FMap<A, B>(Func<A, B> f, Nullable<A> a) {
return new Nullable<B>(f(a.Value));
}
}
All you need to do is take a close look at a 1997 paper by Simon Peyton Jones, Mark Jones, and Erik Meijer, entitled Type classes: an exploration of the design space, and you will find a plethora of even more complicated (and useful) examples that use an innocent-sounding aspect of Haskell’s type system called multi-parameter type classes. All of the types are higher-order and are merely moved around and manipulated like abstract (higher-order) symbols. The type system gracefully gets out of the way and allows you to drop abstract type parameters into any holes they fit in, without mandating that you say too much. The secret sauce—as noted earlier—is kinds.
Kinds are used in the implementation of Haskell’s type system, and you won’t mention a whole lot about them anywhere. They basically categorize what kind of types can appear anywhere a type is expected. A great overview (with plenty of context) can be found in Mark P. Jones’s Functional Programming with Overloading and Higher-Order Polymorphism paper and, of course, the Haskell 98 Report.
Here’s a quick rundown. Kinds appear in one of two forms:
- the symbol * represents a concrete type (a.k.a. a monotype), and,
- if k1 and k2 are kinds, then k1 -> k2 is the kind of types that take a type of kind k1 and return a type of kind k2.
Kinds are formed in many ways: the primitive types (such as Char, Int, Float, Double, etc.) are an example of the former, and are of kind *. They “bottom out.” Type constructors, however, like Functor are an example of the latter, and are of kind * -> *. That is, they take a kind k1 (the first *) and produce another kind k2 (the second *). By giving some concrete type T (*) to Functor, we get back a Functor T (also *). The latter is therefore a bit like a function mapping one kind to another. Functions have a kind of * -> * -> *, because a function has two types: the type of arguments (the first *) and the type of its return value (the second *). These compose, so that you might have (* -> *) -> * -> *. And so on. Thinking about kinds can take a bit of getting used to.
But the really useful thing here is that kinds allow you to write higher order type constructors like those we have begun to explore above, like Functors and Monads. I.e., given a type t1 of kind k1 -> k2, and a type t2 of kind k1, then t1 t2 is a type expression of kind k2. This can be applied to the occurrences of f a and f b in Functor’s fmap function. In the type Functor f they are of kind * -> * -> *. When a concrete Functor instance is specified, e.g., by substituting T for f, this turns fmap’s T a and T b arguments to kind * -> *. That is, they still both expect another kind before bottoming out. And therefore we can substitute some concrete U and V types for a and b, to reduce them from kind * -> * to kind *.
Now we’re done. And, as if by magic, it all works.
 Sunday, November 02, 2008
A few months back, while writing my new book, I whipped together a tool to dump information about your processor layout using the GetLogicalProcessorInformation function from C#. You can find the code snippet in Chapter 5, Advanced Threads, of my book. (A developer on the Windows Core OS team, Adam Glass, had also written a similar tool in C++.) I will be posting code to the companion site for my book in the coming weeks, at which point you can easily get your hands on it.
Anyway, I sent the code to Mark Russinovich suggesting it might make a useful SysInternals tool, and he agreed. Now it's up on microsoft.com for download, under the name of Coreinfo: http://technet.microsoft.com/en-us/sysinternals/cc835722.aspx. When run, Coreinfo pretty prints information about the mapping from cores to sockets, cores to NUMA nodes, and what kinds of caches are shared on the machine. Particularly for somebody like me who is always running code on different kinds of machines -- and given that parallel code performance heavily depends on memory hierarchy -- I've found this tool to be invaluable and very helpful. Enjoy.
 Friday, October 31, 2008
Dan Grossman invited me to deliver a talk as part of the University of Washington's Computer Science and Engineering Colloquia series. It was recorded and will eventually air on UWTV, but has also been posted online:
Microsoft's Parallel Computing Platform: Applied Research in a Product Setting
The goal of Microsoft's Parallel Computing Platform (PCP) team is to enable the shift to modern, multi- and manycore hardware, by providing a runtime, programming models, libraries, and tools that make it easy for developers to construct correct, efficient, maintainable, and scalable programs through the use of parallelism. In doing so, tens of years of industry research has been combined and applied in a myriad of ways. This talk examines PCP's current progress, explicitly relating it to specific research of the past and present, in addition to surveying future efforts and possible research opportunities.
http://norfolk.cs.washington.edu/htbin-post/unrestricted/colloq/details.cgi?id=768
<WMV - streaming, WMV - download, ...>
If you're not aware of the work we're doing in Visual Studio 2010 -- both in .NET 4.0 and C++ -- this talk gives a pretty good overview of all of it. It has a researchy feel to it, with plenty of pointers to interesting prior research that has influenced our work along the way.
 Thursday, October 30, 2008
I sat down last week to record a handful of interviews with some folks from Pearson Education.
They are now live on the InformIT website:
Apologies for the Quicktime-only format.
 Thursday, October 02, 2008
The word “architect” means different things to different people in the context of software engineering. And it varies wildly depending on the kind of organization you’re in. An architect at a medium sized IT shop might focus on connecting disparate business systems together at a high level, but without diving down into code. An architect at a startup may be more like a tech lead, checking in code like mad, but also keeping the rest of the team in check. And a software architect at Microsoft can play an even varied number of roles because the company is so large and diversity of projects so great.
A colleague and mentor of mine who I respect greatly says that an architect is the guy (or gal) who is in charge of making those decisions which, if made incorrectly, could sink the project.
There is a lot to be said for this. These decisions are those with the broadest, deepest, and longest lasting impact. The decisions themselves are often made by team members initially, but the architect is responsible for providing constant and rigorous technical oversight. Architects set the high level technical agenda, look ahead several releases, and keep the team on course. They are ultimately to blame if the technical foundation is unsound and/or final solution fails to meet expectations. Their butt is on the line.
On one hand, an architect is the lead engineer with most at stake in the project. On the other hand, an architect is more like a member on the project’s board of directors, providing high level guidance and meddling as little as possible (but as much as is necessary) in the day-to-day details.
An architect’s success is measured by what he or she ships to customers, and not by the amazing ideas that were ultimately never realized. This necessarily means an architect’s success is deeply rooted in the team’s culture, work ethic, and ability. He or she needs to work through others to get things done.
There have been some great architects throughout the course of computer science, but who may not have been labeled as such. Linus Torvalds is the architect of Linux, and David Cutler the architect of Windows NT. John Backus was arguably the architect of FORTRAN, Niklaus Wirth the architect of Pascal, Bjarne Stroustrup the architect of C++, James Gosling the architect of Java, and Anders Hejlsberg the architect of C#. Bill Gates was the architect of Microsoft BASIC, and Charles Simonyi the architect of the initial versions of Microsoft Office (Word and Excel). In each case, you can see that the end result is very reflective of one person’s value system and ideas, but took a lot more than just that person to be successful. Each of these people learned to let go of their project just enough that it could achieve the scale that it was meant to achieve, but not so much that the project veered off course. Some projects have multiple architects, but the successful ones usually have one who is really in charge.
Already you can see some subjective opinion being thrown into the mix, and some of it is apt to be controversial. Although not comprehensive, I’ve put together seven guiding principles that I personally aspire to. I’ve certainly not mastered them all, but have always looked up those people around me who seem to have. Why seven? No reason, really. Over the past few years, I’ve tried to spend as much time as possible learning from successful architects, and these stand out in my mind as being the key common attributes that appear to be common among them.
0. Inspire and empower people to do their best work.
Architects ultimately succeed or fail based on the quality of people on their team. Knowing how to inspire and empower these people, so that they can do their best work, is therefore one of the most important skills an architect needs in order to be successful.
You can’t do it all yourself. This can be frustrating at times, and at times you might think that you can (particularly in times of frustration). I’ve personally hacked together 1,000s of lines of code that I’m incredibly proud of in a weekend, and that would have taken weeks or months to get done if I had to instead explain the idea to somebody else and wait for them to write those same 1,000s of lines of code. And the 1,000s of lines they write of course wouldn’t end up being the same as the ones you’d have written. And they may decide that they don’t like the design after all, start discussing it with colleagues, stage a mutiny, and ultimately overthrow what once seemed like a great idea. This is a tough pill to swallow. But it’s a sad fact of life that you need to learn to be comfortable with.
The same thing would have happened if you were the one to implement the idea, of course; the difference is that somebody else needs to be empowered to take the kernel of an idea, and run with it. That entails reshaping it as necessary to make it realistic and successful.
I’m not suggesting architects don’t write code (quite the opposite: see #3 below), but you can’t write it all (except for very small projects). If you buy the argument that an architect is just the leading senior engineer on the project, then by definition the architect is probably qualified to write quality code quickly. But what about the code they don’t write? Other people on the team need to write it, and the architect needs to have enough time (where he or she isn’t hacking code) to inspire those people to write the right code. This takes energy and effort. You need to paint a compelling picture of the future, but with enough open-endedness such that the team can flex their creative muscles and fill in the details.
This is the only way to scale. And architects need to scale to achieve broad impact.
Architects should also welcome all ideas with open arms. You want to foster an open and energetic environment on your team, where intellectual debate is the norm. All ideas are fair game.
That’s not to say all ideas are good ones, and ultimately the bad ones need to die a quick and painless death before going too far, but an architect who won’t even entertain new ideas from the team (typically because of NIH syndrome (i.e., Not Invented Here)) often drive away the best engineers. Great engineers hate to be told what to do. They don’t want to feel like they are walking in the shadows of somebody else. They want to use the skills that make them so great, which involves inventing bigger, badder, and more impactful designs. And you want them to use these skills too, because that’s why you hired them: these skills are crucial to the success of your project. Part of your role as the team’s architect is to recognize who on the team has the most potential, and to arrange for them to have as much leeway and creative freedom as possible. You don’t want to end up with a bunch of lackeys whose job is to “just implement” your ideas, because you’ll get what you paid for.
It’s a true sign of success when the culture you impart unto your team allows them to invent things in the spirit of your own design principles, but without you needing to do it yourself. Jim Gray, for example, inspired countless people to do great things. Does he get credit for each of those ideas? Of course not. But was he indirectly responsible for them to some degree, and do they all have a little Jim Gray in them? Absolutely. Being an architect on a team is similar; not every idea has to be your own. In fact, it’s far more powerful if few of them are.
1. Oversight, but not dictatorship.
That brings me to technical oversight. Because an architect is typically not a manager for his or her project (although in some cases he or she may be), arms-length influence needs to be used to get things done. In fact, the architect may have very little to say over specific project management, scheduling, and budget decisions, but is typically on the senior leadership team for the project. So when I talk about “leeway” above, I’m talking about the degree to which an architect monitors and attempts to meddle with the progress of the team. While it’s tempting for an architect to set the ship sailing to sea, and then turn around to work on the next big thing, this almost never works. The initial vision and idea is far from a shipping solution, and software engineering only gets interesting once you actually try to build something. Ideas are cheap. The architect needs to help the team work through the ramifications of certain technical decisions that were made up front, and help with the continual course correction.
Because an architect’s butt is ultimately on the line, he or she needs to work as fast as possible to correct problems when something goes wrong. This implies the architect is involved enough to notice when something goes wrong, hopefully well in advance of anybody else seeing it. I’ve seen many models that work, ranging from the architect being the approver for all major design decisions, to the architect simply reviewing all major design decisions after-the-fact, to the architect delegating this responsibility to trusted advisers. For example, Linus Torvalds for the longest time required that all checkins to the Linux code base be reviewed by him. Anders Hejlsberg still effectively approves each C# language design change. In my opinion, the closer to each major decision the architect can afford to be, the better.
Left to its own devices, the team would veer off course in no time. That’s not because of malicious intent, but rather because of the sheer diversity of software engineers. This diversity is present on many levels: in skill level, taste (which is hard to measure: more on that in #2 below), motivation, work ethic, interpretation of the vision, personal beliefs and experience, and so on. An architect acts as a low-pass signal filter, smoothing out any irregularities that deviate too far from the core design principles.
In Tony Hoare’s ACM Turing Award paper of 1981, The Emperor’s Old Clothes, he explains the risk of not providing this kind of architectural oversight:
“’You know what went wrong?’ he shouted - he always shouted – ‘You let your programmers do things which you yourself do not understand.’ I stared in astonishment. He was obviously out of touch with present day realities. How could one person ever understand the whole of a modern software product like the Elliott 503 Mark II software system? I realized later that he was absolutely right; he had diagnosed the true cause of the problem and he had planted the seed of its later solution.”
Sadly, this responsibility often entails being “the bad guy”. Sometimes you need to mercilessly kill an idea because it would put certain parts of the project at risk. Other times you need to let somewhat bad (but not too impactful) ideas go. There’s a tradeoff here, because each time you kill an idea you’re going to leave somebody feeling burned. And you may waste peoples’ time, depending on how much time has already been invested in that idea. Some battles are best left unfought. There is an art to be learned here: if you can get those with the idea to firmly believe that there has to be a better way, you can avoid being seen as the bad guy. “Sit back and wait” can work in some cases, but it can backfire too.
The deep involvement in the technical design details unfortunately means that the architect can become the bottleneck if he or she is not careful. This can slow the team down. Some slowdown can admittedly be a good thing, because it has the effect of forcing more thoughtfulness in each and every decision. But as the team grows, the granularity of decision oversight necessarily has to change to ensure the team is empowered to make progress. In order for this to work, you need to have trusted individuals who are involved at a finer granularity and will use the same principles and values. This takes trust and time.
2. Taste is a hard thing to measure, but is invaluable.
Software engineers like to measure. Many people try to make design decisions based on quantitative data, even though they know that engineering is more of an art than a science. But there is one common trait that, as far as I can tell, is impossible to measure, and yet common to all of the great software architects I know: good taste. And because it’s impossible to measure, those who lack it have a hard time understanding the difference between a design with good taste and one with bad taste.
There is a certain elegance and beauty to the designs created by architects with good taste. When you see it from a distance, you know it, but when viewed under a microscope—the kind of microscope used when debating the finer points with other engineers on the team—it is much harder to detect. Often it’s incredibly difficult to articulate why some particular design has good taste, which makes it even harder to justify. Eventually people are willing to trust your judgment because they begin to see it too.
In fact, good taste is perhaps one of the most important skills an architect needs to have. Bad taste leads to clunky designs that nobody likes to use. Steve Jobs knows this. And yet taste is probably the most difficult skill for an architect to develop, and one of the subtler ones that few people recognize as being necessary. Many managers think that throwing more engineers at a design problem will solve it, when in reality often all that is necessary is one person with very good taste and an eye for detail.
I’m not certain where taste comes from: an innate skill? Perhaps, but not exclusively. In my best estimation, good taste can be learned from paying close attention to the right things, taking a step back and viewing designs from afar often enough, being learned in what kinds of software has been built and was successful in the past, and having a true love of the code. That last part sounds cheesy, but is true enough to reemphasize: if you don’t feel a certain passion for your code and project, it’s a lot easier to let bad taste run rampant, because your care level isn’t as intense as it needs to be.
3. Write code and get your hands dirty.
The best architects realize that code is king. It rules all else. At the end of the day, Visio diagrams, high level vision documents, whiteboard works of art, design documents, emails, functional specifications, and so on, are all a means to an end, not the end itself. The code is your product, and if you don’t understand the code, you don’t understand t |