Skip to content

Universal Type Inference is a Bad Thing

9
Apr
2008

Recently, Scala has been wowing developers with its concise syntax and powerful capabilities, but perhaps its most impressive feature is local type inference.  When the intended type for an element is obvious, the Scala compiler is able to make the inference and there is no need for any additional type annotations. 

While this is extremely useful, it falls quite a bit short of the type inference mechanisms available in other languages such as Haskell and ML.  Many people have pointed out that Scala doesn’t go as far as it could in its inference, forcing the use of type annotations when the type could easily be inferred by the compiler.  To understand this claim, it’s necessary to consider an example from a language which does have such universal type inference.  Consider the following function:

fun sum nil init = init 
 | sum (hd::tail) init = hd + (sum tail init)

For those of you not familiar with ML or its derivatives, this is a simple curried function which traverses a given list, adding the values together with an initial value given by init.  Obviously, it’s a contrived example since we could easily accomplish the above using a fold, but bear with me. 

The function has the following type signature: int list -> int -> int.  That is to say, the ML compiler is able to infer the only possible type which satisfies this function.  At no point do we actually annotate any specific nominal type.  Clarity aside, this seems like a pretty nifty language feature.  The ML compiler actually considers the whole function when inferring type, so its inferences can be that much more comprehensive.  Scala of course has type inference, but far less universal.  Consider the equivalent function to the above:

def sum(list:List[Int])(init:Int):Int = list match {
    case hd::tail => hd + sum(tail)(init)
    case Nil => init
  }

Obviously, this is a lot more verbose than the ML example.  Scala’s type inference mechanism is local only, which means that it considers the inference on an expression-by-expression basis.  Thus, the compiler has no way of inferring the type of the list parameter since it doesn’t consider the full context of the method.  Note that the compiler really couldn’t say much about this method even if it did have universal type inference because (unlike ML) Scala allows overloaded methods and operators.

Because of the relative verbosity of the Scala example when compared to ML, it’s tempting to claim that ML’s type inference is superior.  But while ML’s type inference may be more powerful than Scala’s, I think it is simultaneously more dangerous and less useful.  Let’s assume that I was trying to write the sum function from above, but I accidentally swapped the + operator for a :: on the second line:

fun sum nil init = init 
 | sum (hd::tail) init = hd :: (sum tail init)

The ML compiler is perfectly happy with this function.  Such a small distinction between the two, but in the case of the second (incorrect) function, the type signature will be inferred as the following: 'a list -> 'a list -> 'a list (the 'a notation is equivalent to the mathematical ).  We’ve gone from a curried function taking an int list and an int returning an int value to a function taking two list values with arbitrary element types and returning a new list with the same type; and all we did was change two characters.

By contrast, the same mistake in Scala will lead to a compiler error:

def sum(list:List[Int])(init:Int):Int = list match {
    case hd::tail => hd :: sum(tail)(init)    // error
    case Nil => init
  }

The error given will be due to the fact that the right-associative cons operator (::) is not defined for type Int.  In short, the compiler has figured out that we screwed up somewhere and throws an error.  This is very important, especially for more complicated functions.  I can’t tell you how many times I’ve sat and stared at a relatively short function in ML for literally hours before figuring out that the problem was in a simple typo, confusing the type inference.  Of course, ML does allow type annotations just like Scala, but it’s considered to be better practice to just allow the compiler to infer things for itself.

ML’s type inference ensures consistency, while Scala’s type inference ensures correctness.  Obviously, “correctness” is a word which gets bandied about with horrific flippancy, but I think in this case it’s merited.  The only thing that the ML type inference will guarantee is that all of your types match.  It will look through your function and ensure that everything is internally consistent.  Since both the correct and incorrect versions of our sum function are consistent, ML is fine with the result.  Scala on the other hand is more restrictive, which leads to better assurance at compile-time that what you just did was the “right thing”.  I would argue that it’s part of the responsibility of the type checker to catch typos such as the one in the example, but languages with universal type inference just can’t do this.

Type inference can be an incredibly nice feature, and it is very tempting to just assume that “more is better” and jump whole-heartedly into languages like ML.  The problem is languages which have such universal type inference don’t provide the same safety that languages with local or no type inference can provide.  It’s just too easy to make a mistake and then not realize it until the compiler throws some (apparently) unrelated error in a completely different section of the code.  In some sense, type inference weakens the type system by no longer providing the same assurance about a block of code.  It’s important to realize where to draw the line as a language designer; to realize how far is too far, and when to step back.

Groovy’s Performance is Not Subjective

31
Mar
2008

Ah, the saga of misinterpreted micro-benchmarks!  Developers advocating one technology over another have long used micro-benchmarks and trivial examples to illustrate their point.  This is most often seen in the area of language implementation, where performance is a critical (and often emotional) consideration.  The Computer Language Benchmarks Game is a good example of this.

With the rise of the JVM as a platform for language development, we’ve been seeing a lot of benchmarks directly comparing some of these languages with each other and with Java itself.  Actually, it seems that the popular trio (as far as benchmarking goes) are Scala, JRuby and Groovy.  It’s been a bit of a favorite past-time of the blogosphere to create benchmarks for these languages and then spin up controversial posts about the results.  Back in September, Darek Young wrote a post illustrating a ray tracer benchmark which really caught my eye:

Results

ray.java
12.89s

ray.scala
11.224s

ray.groovy
2h 31m 42s

I was expecting the Groovy code to run longer than the Scala code but was shocked at the actual difference. All three versions of the code produce identical images: (fullsize here)

Wow!  I’ve seen these results dozens of times (looking back at the post), but they never cease to startle me.  How could Groovy be that much slower than everything else?  Granted it is very much a dynamic language, compared to Java and Scala which are peers in static-land.  But still, this is a ray tracer we’re talking about!  There’s no meta-programming involved to muddle the scene, so a halfway-decent optimizer should be able to at least squeeze that gradient down to maybe 5x as slow, rather than a factor of 830.

If this were an isolated incident, I would probably just blow it off as bad benchmarking, or perhaps an odd corner case that trips badness in the Groovy runtime.  Then a week later, I read this post by Pete Knego:

Test Groovy Java Java vs Groovy (times faster)
Simple counter 8.450 150 56x
Binary tree building 19.500 2.580 7.6x
Binary tree traversing 2.530 76 33x
Prime numbers 43.270 1.170 37x

All [non-decimal] times are in milliseconds.

Well, this is really disappointing. I expected Groovy to be slower but not by that much. In order to understand where does such a performance hit come from we have to peek under the hood. The culprit of all this is of course Groovy’s dynamic nature, implemented as MOP. MOP is a way for Groovy to know which class elements (fields/properties, methods, interfaces, superclasses, etc..) are defined on an object and to have a way to alter that data or invoke it. The core of MOP are two methods defined on GroovyObject: get/setProperty() and invokeMethod(). This methods are called every time you access a field or call a method on a Groovy object and quite a lot of work is done behind the scenes. The internals of the MOP are listed in MetaClass interface and implemented in six different classes.

All of this is old news, so the question is: Why am I bringing this up now?  Well, I recently saw a post on Groovy Zone by none-other-than Rick Ross, talking about this very subject.  Rick’s post was in response to two posts (here and here), discussing ways to improve Groovy code performance by obfuscating code.  Final result?

This text is being written as I was changing and trying things, I gained 20s from
minor changes of which I lost track. :-) I am currently at 1m30s (down from the
original 4m and comparing with Java’s 4s).

I’m sorry, this is acceptable performance?  This is someone who’s spent time trying to optimize Groovy, and by his own admission, Groovy is 23x slower than the equivalent Java code.  Certainly this is a far cry from the 830x slower in the ray tracer benchmark, but in this case it’s simple string manipulation, rather than a mathematically intensive test.

Coming back to Rick’s entry, he looks at the conclusion and has this to say about it:

Language performance is highly overrated

Much is often made of the theoretical “performance” of a language based on benchmarks and arcane tests. There have even been cases where vendors have built cheats into their products specifically so they would score well on benchmarks. In the end, runtime execution speed is not as important a factor as a lot of people would think it is if they only read about performance comparisons. Other factors such as maintainability, interoperability, developer productivity and tool and library support are all very significant, too.

Wait a minute, that sounds a lot like something else I’ve read recently!  Maybe something like this:

Is picking out the few performance weaknesses the right way to judge the
overall speed of Groovy?

To me the Groovy performance is absolutely sufficient because of the
easy integration with Java. If something’s too slow, I do it in Java.
And Java compared to Python is in most cases much faster.

I appreciate the efforts of the Groovy team to improve the performance,
but if they wouldn’t, this would be no real problem to me. Groovy is the
grooviest language with a development team always having the simplicity
and elegance of the language usage in mind - and that counts to me.  :-)

This is almost a mantra for the Groovy proponents: performance is irrelevant.  What’s worse, is that the few times where they’ve been pinned down on a particular performance issue that’s obviously a problem, the response seems to be along the lines of: this test doesn’t really show anything, since micro-benchmarks are useless.

I’m sorry, but that’s a cop-out.  Face up to it, Groovy’s performance is terrible.  Anyone who claims otherwise is simply not looking at the evidence.  Oh, and if you’re going to claim that this is just a function of shoe-horning a dynamic language onto the JVM, check out a direct comparison between JRuby and and Groovy.  Groovy comes out ahead in only four of the tests.

What really bothers me about the Groovy performance debates is that most “Groovyists” seem to believe that performance is in the eye of the beholder.  The thought is that it’s all just a subjective issue and so should be discounted almost completely from the language selection process.  People who say this have obviously forgotten what it means to try to write a scalable non-trivial application which performs decently under load.  When you start getting hundreds of thousands of hits an hour, you’ll be willing to sell your soul for every last millisecond.

The fact is that this is simply not the case.  Performance is important and it must be considered when choosing a language for your next project.  Now I’m not implying that it should be the sole consideration, after all, we don’t use Assembly much anymore.  But we can’t just discard performance altogether as a means of evaluation.  Developer productivity is important, but it means nothing if the end result doesn’t meet basic responsiveness standards.

It’s like the first time I tried Mingle (which is written using JRuby BTW) back when it was still in pre-beta.  The application was amazing, but it took literally minutes to render the most basic of pages.  The problem was closely related to the state of the JRuby interpreter at the time and its poor memory management with respect to Rails.  In the end, the application was completely unusable.  ThoughtWorks had produced an amazing piece of software in a remarkably short time span, but the cost was the use of a language and framework which (at the time) led to unredeemable performance.  They spared the developers at the expense of the end-users.

Both Mingle and JRuby have come a long way since those first tests.  Charles and the gang have put an intense amount of effort into optimizing the JRuby runtime and JIT compiler.  They’ve gone from an implementation which was 5-10x slower than MRI 1.8.6 to a fully compatible implementation that is usually faster than the original.  Obviously performance is achievable in a dynamic language on the JVM, so why is Groovy still so horrible?

The only answer I can think of is that the Groovy core team just doesn’t value performance.  Why else would they consistently bury their heads in the sand, ignoring the issues even when the evidence is right in front of them?  It’s as if they have repeated their own “performance is irrelevant” mantra so many times that they are actually starting to believe it.  It’s unfortunate, because Groovy really is an interesting effort.  I may not see any value for my needs, but I can understand how a lot of people would.  It fills a nice syntactic niche that other languages (such as Ruby) just miss.  But all of its benefits are for naught if it can’t deliver when it counts.

Interface "Wow" Factor

7
Mar
2008

I’ve been using Firefox 3.0 since b1 on my main development machine under Windows Vista and to be honest, I’ve been floored by all of the improvements the Firefox team has managed to squeeze into this release.  For starters, performance is improved easily 10 fold over 2.0, especially when dealing with scads of tabs.  That alone makes the upgrade worthwhile in my book. 

Even beyond that though, the entire application just has a more polished “feel” about it.   Beta 3 saw the introduction of an improved theme on all platforms.  This new theme was most noticeable on Mac, where it made Firefox suddenly jump from a MacPorts-style outsider to running right alongside the “iApps” in terms of style.  The improvement on Vista was a little more subtle, but none-the-less impressive:

 image

It looks a little weird when you first examine it, but after using this UI for a while I’ve come to really appreciate it’s simple devotion to usability patterns.  I’ve followed with passing interest the Firefox development cycle since v0.5, so I knew that they were spending a lot of time polishing the UI.  I was actually one of those aware of new “keyhole” look long before beta1, so when it arrived I was somewhat non-plussed.  Oh it’s certainly slick and a vastly more usable (and more attractive) UI than it’s super-power competitor, IE7.  But let’s face it, slick only gets you so far these days.  Windows Vista has about the slickest look of any operating system, and just look at how much people malign it.

Returning to Firefox though, I’ve been extremely happy with how well “put together” the whole app seems.  It’s really become a coherant power application with a flair for the indulgent user.  It’s come a long way since those early releases on Windows.  I didn’t realize just how far it had come though until I installed 3.0b3 on my Ubuntu Linux virtual machine (click for larger shot).

image

My first reaction was, “Impressive, they finally managed to make it look like a real Gnome2 application.”  Then I looked down.

firefox-wow-thumb

I nearly wept when I saw these controls.  GTK Linux has been without a browser which could do this literally since the beginning of time.  I was certainly aware that the team was working on this, but I had no idea that it had been activated in 3.0.  Native controls in HTML are incredibly important to the user perception of how well the browser integrates with the platform.

That’s what it really all comes down to: user perception.  Jeff Atwood harps on about this constantly, but just because it’s oft-repeated doesn’t make it less true.  It doesn’t matter what your application can do, just what your users think it can do.  It’s all an elaborate illusion anyway, we just have to realize how complete that illusion really is.  If a user looks at your application and thinks, “Wow!  I don’t know what it is, but it looks powerful,” then you have succeeded as a developer.  Your application could do nothing more than print “Hello, World!” an infinite number of times; so long as it is impressive looking, it will be a success (think iPhoto 1.0).  Likewise, your application may desalinate water and hold the key to world peace, but if it looks wimpy, users will never give it a chance.

Now by “impressive looking” I certainly don’t mean just flashy.  Anyone can Photoshop a fancy interface with lickable buttons and endlessly translucent animations, but that doesn’t mean the interface will “feel powerful”.  The really important test is in those critical first seconds as the user makes his first few clicks through the application.  The user needs to instantly understand the core functionality of the application and why it is better than the competition.  Their eyes need to be draw to the critical areas and they need to be comfortable resting there for long periods at a time.  They should feel an immediate sense of cooperation and team-spirit in the application.  Things should progress as quickly as they can think (but no faster) and transition smoothly from state A to state B.  Oh, and the application should look good.

Firefox on Gnome2 isn’t particularly flashy; it isn’t very lickable, and there’s no translucency.  It does make a statement however, one which is immediate and unmistakably readable by the user: I can do whatever you need me to, and you’re going to like the way I do it.

Wow.

Should We Really Study Other Languages?

4
Mar
2008

The practice of learning multiple languages has really become dogma in the modern developer community.  Everyone knows that you should study different languages, different paradigms and various techniques for accomplishing the same thing.  Why?  The canonical answer is that it helps you grow into a better programmer in your primary language.  After all, if you know how to compose functions in Lisp, naturally that must make it easier to design flexible systems in Java.  I would beg to differ.

I must admit, I used to be a card-carrying member of the “one language per year” club.  I used to push myself and other developers to get their feet wet in alternative languages, even ones which had no practical application (I learned Ruby back when it was just a toy for Asian folks).  Recently though, I’ve been coming more to the conclusion that perhaps this frenetic dash to learn the most languages might not be the “ultimate answer” after all.  A commenter on Reddit relates a story which is (I believe) quite applicable to the subject:

When I was in college, one of the jobs I had was a TA for an intro programming class. For one of their projects, I was asked to whip up a kind of web browser “shell” in Java. The basic idea was to make a browser that would be highly extendable by students, while freeing them from worrying about the overall rendering framework.

Now, the first language that I learned was Smalltalk, which has historically been relatively design-pattern-free due to blocks and whatnot, and I had only learned Java as an afterthought while in college, so I coded in a slightly unorthodox way, making use of anonymous inner classes (i.e., shitty lambdas), reflection, and the like. I ended up with an extremely loosely coupled design that was extremely easy to extend; just unorthodox.

When I gave it to the prof, his first reaction, on reading the code, was…utter bafflement. He and another TA actually went over what I wrote in an attempt to find and catalog the GoF patterns that I’d used when coding the application. Their conclusion after a fairly thorough review was that my main pattern was, “Code well.”

You may laugh (I did), but I can’t tell you how much code I’ve had to work with which reminds me of this tale.  Developers these days pick up patterns and best-practices from dozens of languages and try to apply them to a language for which they are ill suited.  Consider the following code:

public class SortUtils {
    public static <T> List<T> mergeSort(final List<T> list) {
        return new Object() {
            private final List<T>[] divided = divide(list);
 
            public List<T> run() {
                return merge(mergeSort(divided[0]), mergeSort(divided[1]));
            }
        }.run();
    }
 
    public static <T> List<T>[] divide(final List<T> list) {
        if (list.size() == 0) {
            return new List<T>[] {new ArrayList<T>(), new ArrayList<T>()};
        } else if (list.size() == 1) {
            return new List<T>[] {list, new ArrayList<T>()};
        } else {
            return new Object() {
                // this part doesn't really work, it's just illustrative
                private final T first = list.remove();
                private final T second = list.remove();
                private final List<T>[] sub = divide(list);
 
                public List<T>[] run() {
                    return new List<T>[] {new ArrayList<T>() {
                        {
                            addAll(sub[0]);
                            add(first);
                        }
                    }, new ArrayList<T>() {
                        {
                            addAll(sub[1]);
                            add(second);
                        }
                    }};
                }
            }.run();
        }
    }
 
    public static <T> List<T> merge(final List<T> left, final List<T> right) {
        if (left.size() == 0) {
            return right;
        } else if (right.size() == 0) {
            return left;
        } else {
            if (left.get(0) < right.get(0)) {
                return new ArrayList<T>() {
                    {
                        add(left.remove(0));
                        addAll(merge(left, right));
                    }
                };
            } else {
                return new ArrayList<T>() {
                    {
                        add(right.remove(0));
                        addAll(merge(left, right));
                    }
                };
            }
        }
    }
}

I know what you’re thinking: Whoever wrote this is a sick, sick programmer.  Actually I wrote it, but that’s beside the point…  :-)

The point is that taking knowledge gained in one language and directly applying it to another is almost never the right approach.  This isn’t how you sort a list in Java, it’s how you would do it in ML, or maybe even Lisp.  By trying to apply functional idioms directly to an object-oriented language, I’ve accomplished code which does two things.  First, it’s unmaintainable.  No sane-minded Java developer is ever going to be able to take this code and make incremental improvements.  Second (and just as important), it is extremely slow.  Pure-functional languages are written in such a way as to make function calls, recursion and immutable data very performant.  Java just doesn’t work like that.  This code is creating objects left and right, recursing and copying data back and forth so many times its a wonder the whole machine doesn’t crash.

What’s ironic is even with all my efforts, I still couldn’t eliminate mutable data and sequential statements entirely.  Java’s data structures are mutable by design, something which makes it very inefficient to try to do things immutably.  Java’s APIs just aren’t built to comply with expression-based algorithms, something which is an absolute must when working with immutable data.

Throwing away Java’s utter lack of functional constructs like cons (::) and pattern matching, this code is still horrible because it doesn’t follow the Java idioms.  Not only is the Java language built to perform better when used imperatively, but the constructs are such that the code is more concise and maintainable.  Would it have killed readability to use a loop?  Hardly.  In fact, considering the mess we got when trying to avoid imperative constructs, it’s difficult to imagine things getting less readable.

This is certainly an extreme example, no right-minded developer would ever try to do something like this in the real world, but the point remains.  The same problems can be seen even in examples like applying Java’s for-loop idiom for iterating over arrays in Ruby.  Language-specific idioms are important, and we only cripple ourselves if we ignore them.

So at the end of the day, does this really mean we should stop learning new languages?  Absolutely not.  As “Pragmatic Dave” famously pointed out, learning a new language opens our mind to new ways of approaching problems.  It’s not so much how the code is written as much as gaining a deeper insight into the process of solving the problem.  Learning Scala has helped me refine my problem-solving abilities, allowing me to more effectively approach tasks in any language.

However, I think that this “perspective improvement” is a bit over-stressed.  Yes, problem solving is an important skill, one which should be refined through experience with multiple approaches, but I don’t think it’s as critical as many people make it out to be.  I know some really phenomenal developers who only know one language.  Could they be better for learning another?  Probably, but the point is they’re doing just fine right now.  Being multi-lingual is not critical, and as my facetious example illustrates, it can be very harmful.

In the end, the best reason for learning a new language stands unchallenged and unassailable: it’s fun.  I derive a great deal of satisfaction from learning how to do things in a new language, applying my old skills to new tools.  At the end of the day, I don’t really care whether I’m learning anything important to my career.  This may be the case, but the only thing which really matters to me is the challenge of the new puzzle, a new riddle to crack.  I certainly believe that most good developers will derive the same enjoyment, and as long as they keep their idioms straight, I’m perfectly content with that.

Defining High, Mid and Low-Level Languages

27
Feb
2008

I’ve been writing quite a bit recently about the differences between languages.  Mostly I’ve just been whining about how annoying it is that everyone keeps searching for the “one language to rule them all”, the Aryan Language if you will.  Over the course of some of these articles, I’ve made some rather loosely defined references to terms like “general purpose” and “mid-level” when trying to describe these languages. 

Several people have (rightly) called me out on these terms, arguing that I haven’t really defined what they mean, so I shouldn’t be using them to try to argue a certain point.  In the case of “general purpose language”, I have to admit that I tend to horribly misuse the term and any instances within my writing should be discarded without thought.  However, I think with a little bit of reflection, we can come to some reasonable definitions for high-, mid- and low-level languages.  To that end, I present the “Language Spectrum of Science!” (cue reverb)

Language Spectrum of Science 

This scale is admittedly arbitrary and rather loosely defined in and of itself, but I think it should be a sufficient visual aid in conveying my point.  In case you hadn’t guessed, red languages are low-level, green languages are high-level and that narrow strip of yellow represents the mid-level languages.  Obviously I’m leaving out a large number of languages which could be represented with equal validity, but I only have a finite number of pixels in page-width.

The scale is also somewhat myopic.  It defines Ruby as the highest of the high-level languages.  Very few could argue the other side of the scale since there’s not really anything lower than the hardware, but claiming that Ruby is the most high-level language in history seems somewhat odd.  In truth, I picked Ruby as the super high-level language mainly because it’s a) more dynamic than both JavaScript and Perl, b) more prone to RAD frameworks like Rails and c) it’s the most significant high-level language which I’m really familiar with.

It’s also important to note that languages aren’t really points on the spectrum, but rather they span ranges which are more or less wide, depending on the capabilities.  These ranges may overlap considerably (as in the case of Java and Scala) or may be entirely disjoint (Assembly and Ruby).  In short, the scale is somewhat blurry and shouldn’t be taken as a canonical reference.

Low-Level

Of all of the categories, it’s probably easiest to define what it means to be a low-level language.  Machine code is low level because it runs directly on the processor.  Low-level languages are appropriate for writing operating systems or firmware for micro-controllers.  They can do just about anything with a little bit of work, but obviously you wouldn’t want to write the next major web framework in one of them (I can see it now, “Assembly on Rails”).

Characteristics

  • Direct memory management
  • Little-to-no abstraction from the hardware
  • Register access
  • Statements usually have an obvious correspondence with clock cycles
  • Superb performance

C is actually a very interesting language in this category (more so C++) because of how broad its range happens to be.  C allows you direct access to registers and memory locations, but it also has a number of constructs which allow significant abstraction from the hardware itself.  Really, C and C++ probably represent the most broad spectrum languages in existence, which makes them quite interesting from a theoretical standpoint.  In practice, both C and C++ are too low-level to do anything “enterprisy”.

Mid-Level

This is where things start getting vague.  Most high-level languages are well defined, as are low-level languages, but mid-level languages tend to be a bit difficult to box.  I really define the category by the size of application I would be willing to write using a given language.  I would have no problem writing and maintaining a large desktop application in a mid-level language (such as Java), whereas to do so in a low-level language (like Assembly) would lead to unending pain.

This is really the level at which virtual machines start to become common-place.  Java, Scala, C# etc all use a virtual machine to provide an execution environment.  Thus, many mid-level languages don’t compile directly down to the metal (at least, not right away) but represent a blurring between interpreted and compiled languages.  Mid-level languages are almost always defined in terms of low-level languages (e.g. the Java compiler is bootstrapped from C).

Characteristics

  • High level abstractions such as objects (or functionals)
  • Static typing
  • Extremely commonplace (mid-level languages are by far the most widely used)
  • Virtual machines
  • Garbage collection
  • Easy to reason about program flow

High-Level

High-level languages are really interesting if you think about it.  They are essentially mid-level languages which just take the concepts of abstraction and high-level constructs to the extreme.  For example, Java is mostly object-oriented, but it still relies on primitives which are represented directly in memory.  Ruby on the other hand is completely object-oriented.  It has no primitives (outside of the runtime implementation) and everything can be treated as an object.

In short, high-level languages are the logical semantic evolution of mid-level languages.  It makes a lot of sense when you consider the philosophy of simplification and increase of abstraction.  After all, people were n times more productive switching from C to Java with all of its abstractions.  If that really was the case, then can’t we just add more and more layers of abstraction to increase productivity exponentially?

High-level languages tend to be extremely dynamic.  Runtime flow is changed on the fly through the use of things like dynamic typing, open classes, etc.  This sort of technique provides a tremendous amount of flexibility in algorithm design.  However, this sort of mucking about with execution also tends to make the programs harder to reason about.  It can be very difficult to follow the flow of an algorithm written in Ruby.  This “obfuscation of flow” is precisely why I don’t think high-level languages like Ruby are suitable for large applications.  That’s just my opinion though.  :-)

Characteristics

  • Interpreted
  • Dynamic constructs (open classes, message-style methods, etc)
  • Poor performance
  • Concise code
  • Flexible syntax (good for internal DSLs)
  • Hybrid paradigm (object-oriented and functional)
  • Fanatic community

Oddly enough, high-level language developers seem to be much more passionate about their favorite language than low- or mid-level developers.  I’m not entirely sure why it has to be this way, but the trend has been far too universal to ignore (Python, Perl, Ruby, etc).  Ruby is of course the canonical example of this primarily because of the sky-rocket popularity of Rails, but any high-level language has its fanatic evangelists.

What’s really interesting about many high-level languages is the tendency to fall into a hybrid paradigm category.  Python for example is extremely object-oriented, but also allows things like closures and first-class functions.  It’s not as powerful in this respect as a language like Scala (which allows methods within methods within methods), but nevertheless it is capable of representing most elements of a pure-functional language.

As an aside, high-level languages usually perform poorly compared with low- or even mid-level languages.  This is merely a function of the many layers of abstraction between the code and the machine itself.  One instruction in Ruby may translate into literally thousands of machine words.  Of course, high-level languages are almost exclusively used in situations where such “raw-metal” performance is unnecessary, but it’s still a language trait worth remembering.

Conclusion

It’s important to remember that I’m absolutely not recommending one language or “level” over another for the general case.  The very reason we have such a gradient variety of language designs is that there is a need for all of them at some point.  The Linux kernel could never be written in Ruby, and I would never want to write an incremental backup system in Assembly.  All of these languages have their uses, it’s just a matter of identifying which language matches your current problem most closely.