The Myths of Object-Orientation

The Myths of Object-Orientation, presented by James Noble (whose work has been discussed here before) at ECOOP’09, is a post-Marxist analysis that examines object orientation from a number of angles. From the abstract:

Object-Orientation is now over forty years old. In that time, Object-Oriented programming has moved from a Scandinavian cult to a world-wide standard. In this talk I’ll revisit the essential principles — myths — of object-orientation, and discuss their role in the evolution of languages from SIMULA to Smalltalk to C++ to Java and beyond. Only by keeping the object-oriented faith can we ensure full-spectrum object-oriented dominance for the next forty years in the project for a new object-oriented century!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

history, tragedy, farce

Google has cached an HTML-ed copy of the paper. To lift a quote:

According to George Santayana (via Google): “Those who cannot remember the past are condemned to repeat it”. To paraphrase to Karl Marx (again, and also via Google): “History repeats itself, first as tragedy, second as farce”.

This is as true in software as it is in any other human endeavour. We can see it clearly with respect to object-oriented programming languages: we have SIMULA (history) followed by Smalltalk (tragedy) and finally Java (farce). This makes a great party game — choose any area of software and fill in the blanks yourself!

To get you started, he offers the following examples:

                               History     Tragedy     Farce     
Object-orientation             Simula      Smalltalk   Java      
Nested object languages        Simula      BETA        Scala     
Smalltalk languages            Smalltalk   Self        Newspeak  
Systems languages              BCPL        C           C++       
Wirth languages                Pascal      Modula      Oberon    
Lisp languages                 LISP        Scheme      CommonLisp
ML languages                   ML          O’CAML      F♯        
Languages beginning with “C”   C           ANSI C      C++       
C++ languages                  C++         C+@         C♯        
C♯ languages                   C♯1.0       C♯2.0       C♯ 3.0    
BASIC languages                BASIC       VB          VB.net    
Orthogonal languages           Algol-68    PL/I        Scala     
Computer Companies             DEC         Sun         Oracle    
Haskell                        Haskell     Haskell     Haskell   

I don't understand the C+@ joke. Does the “+” key appear over “1” (just before “@” and “#”) on Kiwi keyboards?

Me neither

But the Haskell punchline is pretty funny. I'll have to check out the whole paper.

Yes, amusing

But the Haskell punchline is pretty funny

Yes, the three-column table is an amusing idea, in a witty form. :)

And one could go on and on, of course. Somewhere in the middle before that punch line I'd probably add something like (if only for the sake of being totally unfair and inaccurate, sure ;) :

                          History     Tragedy     Farce

...

Distributed Computing       WWW         WSDL     Web 2.0

...


C+@

Could this be an oblique reference to Objective-C?

C+@

No, C+@ was a real programming language. I remember reading about it some years back: I believe it came from Bell Labs. Unfortunately, Google doesn't special-case the name, so it can't easily be googled for. At any rate, it nicely combined C with object-orientation; I believe that "@" may have been used for message sends.

Some information about c+@

C+@

(Formerly Calico). An {object-oriented language} from {Bell
Laboratories} which uniformly represents all data as a pointer
to a self-described object. C+@ provides {multiple
inheritance} with {delegation} and with control over which
{method}s come from which delegated object; and {default
methodologies}. It has a simple {syntax} with emphasis on
graphics. It was originally used for prototyping of
telecommunication services.

It runs under {SunOS} and compiles to {Vcode}. Unir Tech,
(800) 222-8647.

E-mail: Jim Vandendorpe .

["A Dynamic C-Based Object-Oriented System for Unix", S.
Engelstad et al, IEEE Software 8(3):73-85 (May 1991)].

["The C+@ Programming Language", J. Fleming, Dr Dobbs J, Oct
1993, pp.24-32].

(31 Oct 1994)

http://www.chavezz.ding.btinternet.co.uk/C.html
[What has "Unir Tech" got to do with it?]

Marx misread (once again?)

The problem with that joke is that Noble totally screwed up his Marx quote, which actually reads:

"Hegel bemerkte irgendwo, dass alle großen weltgeschichtlichen Tatsachen und Personen sich sozusagen zweimal ereignen. Er hat vergessen, hinzuzufügen: das eine Mal als Tragödie, das andere Mal als Farce." - Der achtzehnte Brumaire des Louis Bonaparte. MEW 8, S. 115, 1852 (see e.g. de.wikiquote.org)

That is:

"Hegel remarked somewhere that all significant historic events and persons occur twice, so to say. He forgot to add: one time as tragedy, the other time as farce."

Which, of course, breaks the entire joke and table...

\smartassmodeoff

Top-notch smartassery

Good call!

I have a copy somewhere (heaven knows where that somewhere is) of an English translation of the 18th Brumaire, and I'm quite sure that it does begin with the contracted phrase. Which is actually much punchier. Who cares about Hegel anyway?

This is one of the very rare cases where a joke could be improved with a footnote.

Scott McKay is the original tragedy/farce citation in PL

“C++ is history repeated as tragedy. Java is history repeated as farce.” — Scott McKay

I don't know how far back this quote goes, but the first page of Google search for the quote suggests blogs quoted it as early as 2005.

Re: Scott McKay is the original tragedy/farce citation in PL

Re: comment-62529:

I don't know how far back this quote goes, but the first page of Google search for the quote suggests blogs quoted it as early as 2005.

Way back. The Wayback Machine reports spotting it on Peter Norvig's page http://norvig.com/quotations.html as early as Aug 23, 2000. Scott vouches for the correctness of the attribution but can recall neither the circumstances nor the exact timing of the coinage. Most likely, the remark was made over lunch when Norvig and he “both worked at Harlequin, back in the mid- to late-90s.”

The Essence of Object Orientation

I see the essence of object-orientation as bundling up data along with functions that access data. More specifically: a system is object-oriented if it uses records with fields that may include functions whose bodies may access those fields.

Regarding the myths: "encapsulation of behavior" and "message sending" are just nomenclature for the above, while functional object oriented programming shows that "identity" and "mutable state" are inessential.

single dispatch is of the essence

Re: comment-62502:

… a system is object-oriented if it uses records with fields that may include functions whose bodies may access those fields.

I'm having a hard time deciding whether this view tosses aside more flavorful definitions of OOP.

Well, to pick nits...

It says "if," but it doesn't say "only if"...

Cayenne and Agda2

Cayenne and Agda2 look object-oriented in that kind of light.

They both allow records that contains fields with functions that depend on the values in other fields.

No need for dependent types

Tim Sweeney only specified that the runtime behaviors of the function depends on the runtime value of the fields. What you describe are functions whose type depend on the values of other fields; this is much more sophisticated, not directly related to object orientation (which can mostly be expressed in an untyped world), and certainly not common among object-oriented languages, whatever large your definition of "object oriented" may be.

Object Capability Discipline

I think that object capability model was the only great result from object-oriented programming. But, of the aforementioned languages, the only one to respect object capability model is Gilad Bracha's Newspeak. Further, object capability model certainly doesn't need OO.

The rest of OO is a mess. Message passing has poor scalability and resilience properties on real networks (where we must deal with disruption and partitioning). And the synchronous message passing used in OOP is... well, I simply don't have polite words for it. The pervasive, explicit state encouraged by OOP is something we should be eliminating wherever feasible, not embracing. OO collections management, composite pattern, visitor pattern, etc. have horrible properties for parallelism and concurrency, which prevent us from leveraging any sane model for communication.

OO modeling of domain properties (document object model, magic and monsters in a video game, etc.) are a terrible idea. When modeling domains, we too often need ability to tweak the rules governing elements, too often need to express relationships between them. OO forces shotgun editing to modify those rules and relationships. We should favor instead logic programming, constraint satisfaction, or collections-oriented programming (large sets, perhaps functional relational programming).

I can't access the paper mentioned in the OP, but personally I'd like to see OO retired. I certainly don't want it surviving another 40 years. I do believe that the greater need for CMP, and a growing emphasis on networked programs, will at least force much of OO to change drastically.

(temporary) direct URL to the paper

Re: comment-62503: I can't access the paper mentioned in the OP…

If you click on the URL mentioned in comment-62497, you can find your way to this Google Scholar cluster and, from there, finally access this cached version.

Indeed, on the myths

Indeed, about the myths and/or failures of object-orientation, that seems to be a recurring topic at the various PL conferences, probably for more than a decade (two, even?) already :

e.g., Richard P. Gabriel's (2002) "Notes for a debate : Objects Have Failed" is another (related) interesting read, too, imho.

so the op paper is in the

farce category already? i'm sure it knows that.

Really?

Message passing has poor scalability and resilience properties on real networks

I don't pretend to be an expert on this, but I'd be surprised if that were a consensus view. Isn't Erlang some sort of counter-example?

When modeling domains, we too often need ability to tweak the rules governing elements, too often need to express relationships between them.

Doesn't CLOS allow this?

We should favor instead logic programming, constraint satisfaction, ...

And these are scalable and resilient?

Message passing has poor

Message passing has poor scalability and resilience properties on real networks
I don't pretend to be an expert on this, but I'd be surprised if that were a consensus view. Isn't Erlang some sort of counter-example?

I'm not sure where you'd draw your consensus. These are properties I studied post-grad in various distributed computing and survivable networking courses. With messaging it is difficult to ensure a message arrives once and only once in a network where partitioning and disruption are a problem. For example, in case of disruption, you cannot know which is lost: the message or the acknowledgement. This requires you choose between two failure modes: lost message, or duplicate message. Duplicate message would be ideal if you could ensure that the messages are all idempotent.

Erlang is not a counter-example. Erlang only manages resilience by having the endpoint elements so ready to self-destruct and be replaced at the slightest sign of trouble and by mixing in a bunch of extra paradigms (such as the Erlang database) to avoid losing the important work.

When modeling domains, we too often need ability to tweak the rules governing elements, too often need to express relationships between them.
Doesn't CLOS allow this?

Better than OO systems, yes. ;-)

Multiple-dispatch violates most properties commonly associated with OO systems, including message sending. Whether CLOS is OO has always been very questionable. It's a hybrid model of OO and Procedural, and seems close to CEP. Like most hybrids, CLOS manages to achieve a few strengths and most weaknesses of both paradigms.

We should favor instead logic programming, constraint satisfaction, ...
And these are scalable and resilient?

Yes, very. Replication and sharding and automatic synchronization during moments of connectivity provide scalability and resilience. The paradigms are monotonic and idempotent by nature, and can readily handle a high level of parallelism. Where non-monotonic transforms are needed, one can instead use temporal semantics and temporal databases. So basic communications scalability and resilience is quite high - though the greater expressiveness of these paradigms for domain modeling does make it relatively difficult to control cost of computations, so these paradigms are not known for efficiency.

Given query optimizers or incremental constraint heuristics, these might even be somewhat efficient. :-)

Unfortunately, those paradigms aren't securable at scale, or at least I haven't figured out how to make them so (despite much effort). We need another paradigm in the mix to glue different logic or constraint systems together in a securable way. By themselves, these paradigms will not readily 'scale' beyond a single organization - not unless those organizations are willing to reveal potentially sensitive information to one another.

So we need some other paradigm to serve as securable glue for composing rich domain models between services. I've some ideas on that subject (such as RDP) to maintain properties of idempotence, parallelism, replication, resilience. But I'm using a much weaker paradigm for the domain modeling - functional programming, rather than the more powerful logic programming or constraint programming. I do hope to support functional relational programming, which is weaker for domain-modeling than logic programming but quite flexible compared to the more common scalar functional programming.

More to resilience and robustness than node fault tolerance

For example, Byzantine generals models the networking problem of handling general, arbitrary failure where the nodes might implement a protocol incorrectly, might corrupt their own state-process or internal state e.g. become hijacked... and so on failures.

For example, safety barriers in networks prevent LAN broadcast storms from spreading to the whole network and allow the storm to be contained within the offending subnet.

For example, self-stabilization says that a network should be able to repair itself simply by removing the offending node contributing to the wrong configuration, because the wrong configuration won't spread throughout the network e.g. due to a global feedback loop. For an attacker to attack a self-stabilizing network, it must continuously inject bad packets into the system.

There is also more to scalability than creating 1,000s of Erlang green threads.

For example, autoconfiguration: networks of the future will have trillions of connected nodes, and having to depend on a somebody in charge of addressing to connect a new node to the network is tedious and only really serves to employee people with arcane knowledge in how to tune the network. Networks today don't really do this, but they theoretically can. Here we are talking about the ability for the network to tune itself rather than have a team of experts tune the network.

For example, scalability might mean that smaller-sized networks will contain some degree of overhead and inefficiency, but we assume that by laws of large numbers that it is more important for a general network as a whole to accomodate a wide variety of protocols and application needs. Network scalability in this sense is pretty different from the topologies and routing disciplines used in enterprise integration systems. As we increase in network size, "architecture will dominate materials" - and these overheads fade into the essential requirements for communication.

Message passing?

Message passing has poor scalability and resilience properties on real networks (where we must deal with disruption and partitioning).

I bet you already know this, but in case someone else is confused, the term "message passing" used by object-oriented languages typically has nothing to do with "real networks"; "message passing" is just another name for "function call" and there is no networking involved.

Now, if you are talking about extending function calls into remote procedure calls, that has been known to be a bad idea for like 20 years.

On the other hand, are you are talking about "message passing" in the networking sense, in other words packets, segments, and datagrams? (I am confused by your subsequent comment, "These are properties I studied post-grad in various distributed computing and survivable networking courses. With messaging it is difficult to ensure a message arrives once and only once in a network where partitioning and disruption are a problem.") In that case, I am afraid you are wrong. Message passing has excellent scaling and resilience properties, which is why that is what existing networks use.

In that case, I am afraid

In that case, I am afraid you are wrong. Message passing has excellent scaling and resilience properties, which is why that is what existing networks use.

I think he specifically means message-passing as exposed at the programming language level. Here messaging is a low-level mechanism where you must concern yourself with many details in order to properly distribute a program across many nodes.

Compare to a more abstract means of expression like logic programming, where the details of the computation are so abstract that there is great flexibility in choosing how and where to execute it. The runtime of this logic programming language will invariably be built on messaging of some sort though.

Which isn't to say that messaging is always difficult to distribute, assuming you have the right primitives. Something like E is certainly easier than something like C++, for instance.

Message passing.

I mentioned message-passing in the actors model sense - delivering discrete 'messages' to named components without a longer-lived connection-like concept or protocol. This is similar to the networking sense of the word. One can implement higher-level concepts above message-passing (such as streams or channels or tuple spaces or publish/subscribe architectures) but any properties you might attribute to higher-level architectures do not belong to 'message-passing', so I'm really talking about the direct use of this communications paradigm by programmers.

On ideal networks, such as a machine-local actors-model implementation, where messages are never lost, the network has infinite buffering capacity, networks are secure, messages always arrive in a sane amount of time... under these circumstances, messaging is a decent deal - though there is still much to complain about. Even on an ideal network, messaging offers very little control over logical and physical latencies, and (due to the limited expressiveness of message-passing) it is unduly challenging to coordinate and synchronize multiple interactions on a shared service.

But real networks aren't like that. Connectivity is up and down, nodes are partitioned. Some nodes will never come back up. A common example of 'partitioning' is a laptop with networking turned off: components hosted on the laptop are partitioned from those hosted outside the laptop. A more complex example might involve submersible vehicles communicating by acoustic modems when they get near enough one another, and only occasionally surfacing for radio contact. To offer resilience in these conditions, one must figure out which messages to deliver during the brief moments of connectivity, and possibly even which messages to carry in a sneaker-net fashion (e.g. so one submersible can carry messages from the surface to another submersible).

Unfortunately, as noted earlier, it can be difficult to know which messages were lost while disconnected. This creates a challenge for scalability vs. resilience - how long do we keep messages? How long do we keep a record that we've received and processed messages? Message-passing forces us to pit a scalability issue (how long do we keep these records) vs. a resilience issue (ensuring the messages are processed once and only once).

Further, we must deal with the endpoints. Message-passing is often used for commands and queries. We might send messages asking for then later relinquishing exclusive control. What happens when we lose the message that relinquishes exclusive control? Resilience isn't a property of individual messages - it is a systemic property that means we have sane (elastic) behavior during fault conditions and recover quickly when those conditions are alleviated. I would argue that the message-passing communications and coordination patterns are simply too difficult to make resilient at those endpoints. Even without concern for resilience, the coordination of multiple users of a shared service is often challenging because developers must explicitly maintain state gathered across multiple messages.

Anyhow, I meant what I said: message passing has poor scalability and resilience properties on real networks.

I also mentioned the 'synchronous' message passing used by OO systems, and implied it even worse than real message-passing. That might have been a point of confusion, since I was rather indirect about it. Even local procedure calls are a bad idea for scalability and resilience. Synchronous remote procedure calls are much worse.

Message passing has

Message passing has excellent scaling and resilience properties, which is why that is what existing networks use.

Message passing comes in many forms. Actually enforcing a message passing form is a step below logic programming, and can be thought of as providing an "Architectural Style", because you have converted a nondeterministic, fair execution description of the problem into a more deterministic description of the problem by picking a particular model for (hopefully) a well-matched hardware and/or network topology. It is a fact that some message passing models DO NOT scale on certain architectures. This is what the 80's painfully discovered in trying to figure out general, all-serving compilation techniques and languages for irregular structures.

In this sense, languages arguably should look more like "real networks" if they want to think about scaling to trillions of nodes. We just haven't built software this complex yet or needed it. How much longer before CPU manufacturers (a) won't be able to power the whole chip at once (b) will expect compiler writers to accomodate for partial chip failure (c) be at 6nm dies (d) etc? The ideas will converge.

Edit: A postscript here and more provocative opinion is that languages will eventually subsume network tradeoffs, because they will have to. It's already being foreshadowed by stuff like NoSQL, Clouds, real-time social networking sites like Twitter & Facebook, etc. Well, either that or everything will continue to be written in a mishmash of C++, PHP, Ruby and Scala. But I firmly believe complexity will eventually overwhelm programmers and they will be forced to address their problems using fundamentally different tools than they are using today.

RPCs bad?

Now, if you are talking about extending function calls into remote procedure calls, that has been known to be a bad idea for like 20 years.

This always has seemed to be more a philosophical (ideological?) argument to me than a proven fact. At least it is worth noting that the entire computing infrastructure of Google is based on RPCs, and you cannot say that they haven't been moderately successful with that.

The above author was

The above author was discussing synchronous RPC in particular, possibly of the transparent variety ("function calls into remote procedure calls"). RPC broadly is a catch-all for just about every form of message-passing out there. But with synchronous RPC, the caller waits for a response before making any more progress, and typically times out if the response is lost or takes too long. How common is that pattern in Google's computing infrastructure?

Very common

How common is that pattern in Google's computing infrastructure?

It is the canonical use case.

Interesting. I wonder what

Interesting. I wonder what motivated that decision.

It would work easily enough if they keep the RPC shallow, of course... the severe scalability problems with synchronous RPC become problematic only once you get two or three synchronous RPC calls deep and the latter calls cause the former calls to time out.

Deeper call chains

Oh, they often have much deeper call chains. But there also are sophisticated tracing and monitoring tools.

They've also built custom tools for distributing objects

See Protocol Buffers as an example.

Proto buffers

Well, "protocol buffers" are simply Google's marshalling format. Despite the name, it is not particularly fancy. It basically provides a binary format for ints, bools, strings, and records, lists, and options over those. Nothing more than that, in particular nothing higher-order, so no real objects.

But yes, the RPC infrastructure is using proto buffers for transmitting arguments and results.

Protocol Buffers

I've had opportunity to use protocol buffers, but I'd never have thought to use them for synchronous RPC - freezing up an entire event loop just to wait for a response to a message. I don't even do that for different event-loop threads within a process.

Asynchronous RPCs = synchronous RPCs + threads

I don't see what using or not using the proto buffer format for data has to do with using or not using synchronous RPCs for communication. Aren't those completely orthogonal questions?

Also, if you program with synchronous RPCs, you normally won't program with event loops.

In any case, I think synchronicity is mostly a red herring, since you can easily simulate asynchronous calls with synchronous ones by simply spawning a thread (while it's much more difficult to do the inverse embedding.)

synchronicity is mostly a

synchronicity is mostly a red herring, since you can easily simulate asynchronous calls with synchronous ones by simply spawning a thread (while it's much more difficult to do the inverse embedding.)

I disagree that it's a red herring. One could just as easily say that RPC itself is a red-herring since you can easily implement it in terms of socket programming. We implement all our abstractions in terms of others - whether threading to provide asynchronous RPC events, or call/cc to implement threads in terms of events. (Which embedding is easier is only a question of which your language provides as primitive.) I recall we had a nice article a while back on unifying threads and events.

If we're talking about asynchronous RPC pattern - whether hand implemented by boiler-plate or provided by abstraction - shouldn't we also call it asynchronous RPC?

If you program with synchronous RPCs, you normally won't program with event loops.

I implemented asynchronous RPC in one event-loop as generating queued events inside a remote event-loop, with any result or callback generating another queued event inside the local event loop. Thus, I implemented asynchronous RPC without extra threads. This simply seemed prudent while working with various frameworks (UI, OpenGL) that are also single-threaded. In that context, synchronous RPC is RPC that freezes up an entire event loop. (And, indeed, that's how I learned to grok 'synchronous RPC' in my undergrad operating systems course, with its historical connotations: a bunch of single-threaded procedural-programming Unix processes performing synchronous RPC in addition to the other synchronous IO.) I agree that one would normally resist combining the two - synchronous IO of any sort is problematic for reasoning about progress of event loops.

I don't see what using or not using the proto buffer format for data has to do with using or not using synchronous RPCs for communication. Aren't those completely orthogonal questions?

Protocol buffers does support integration with calls and callbacks, so isn't entirely about data format. And the design of the serialization framework has a considerable effect on whether, for example, synchronous zig-zag call chains are even feasible, much less efficient enough to encourage programmers to use them. So my opinion is: no, these aren't 'completely' orthogonal. But I'll grant they're mostly orthogonal. ;-)

er... RPC = remote procedure

er... RPC = remote procedure call. By definition it is synchronous. What you are talking about (timing out/ repose lost etc.) is the classic RPC as per Bruce Nelson's thesis but it can also be implemented on top of a reliable stream protocol (as in Plan 9) -- this is what most everyone does today. Scalability is handled by having many threads. But latency is certainly an issue.

Terms and assumptions

If we're going to appeal to object-capability languages, it's perhaps worth mentioning E (Miller et al.), which is the language that introduced the term, and the school from which Gilad adopted the concept.

Concerning message passing, OO message passing concerns local procedure calls rather than network traffic, and the marginal cost of local message passing converges rapidly on zero given modern JIT compilation techniques. Better, by all means, to eliminate it statically, but not a big deal if it has to be done dynamically.

While I'm long on record that OO has major issues, it isn't clear that we have a viable alternative yet.

E language

Yeah, E language does a lot of things really well. I do appreciate its value/object/unum (pass-by-copy/pass-by-reference/pass-by-construction) trichotomy. Its promise pipelining, far-refs, and vat concept help alleviate many issues associated with synchronous (local) message passing. The distinctions E makes between stable refs and volatile refs help users reason about failure modes in case of disruption.

I was certainly contemplating its mention, earlier, but I had access to an ocap language already mentioned in the article. ;-)

the marginal cost of local message passing converges rapidly on zero given modern JIT compilation techniques

Efficiency and scalability are very different issues. I wasn't talking about efficiency of message passing. I was talking about its scalability and resilience - the ability for developers to work with message-passing systems at large scales, the performance at scale while maintaining the semantics under real network conditions, etc.

Though, with your interest in Bit-C, I take it you're more interested in the 'downwards' scalability, where fine-grained efficiency seems to matter a lot more.

Efficiency does interest me, but I'm personally more interested in power and energy savings on the large scale. I think that architecture will also win here, that a lot of the prices we pay are unnecessary. (Any communications paradigm whose user stories require polling or periodic messaging → fail for large-scale efficiency.)

While I'm long on record that OO has major issues, it isn't clear that we have a viable alternative yet.

What do you believe it takes for an alternative to be viable?

A bit off the topic

We have a lot of people here on LtU advocating one position or another, and many of the things that they advocate are very attractive in certain use cases. Very few of these things are available as part of a language that works well if your problem doesn't fit their solution. What's missing is a certain kind of continuity of solutions. This is a necessary and inevitable consequence of the way research is (quite correctly) done.

Perhaps I'm just beating a tired drum of my own here, since this is part of what I'm trying to address in BitC. Whether BitC succeeds or not, though, I'm quite certain about the need for somebody to succeed in this regard.

Asymmetric multithreading in the hardware domain throws some really interesting questions into the mix. One certain way to increase the available degree of parallelism by a large factor is to decrease the instruction-grain efficiency of the parallel components. If this makes concurrent programming easier, it may even be very much the right thing to do, but it has some interesting implications for marketing....

Aye

What's missing is a certain kind of continuity of solutions.

Aye, there's a deep one in there. If this language we wish for cannot scale both UP and DOWN, across the functional paradigm and the procedural and the object-oriented, then it only partially covers the design space, and has a devious effect in how it pushes us to design systems one way preferentially....

but to our detriment or to our benefit?

Regarding the myths:

[edit: ack, how did my reply to Tim's reply, The Essence of Object-Orientation, get posted of out order?]

Regarding the myths: "encapsulation of behavior" and "message sending" are just nomenclature for the above, while functional object oriented programming shows that "identity" and "mutable state" are inessential.

Your vocabulary here seems wishywashy to me. I also do not see a direct connection between "functional object oriented programming" (whatever that means) and not needing "identity" and "mutable state". It would probably help if you illustrated with an example to show what you mean. Here is my own personal illustration:

From what I am familiar with, objects always have identities by their very nature -- it is the only stable part of an object over time. However, a functional programming language emphasizes referential transparency rather than identity. It is perfectly okay to have hidden state. Objects, with their identities, are way more interesting when they can make use of hidden state; the trick is that when your type system manages your requests to convert from one type to another that you preserve observational behaviors. I would argue that more important than referential transparency is determinism or at leasted bounded nondeterminism. The more deterministic you can make your system and segregate nondeterminism using a sundry of isolation techniques, the better off you are when a user reports a bug.

I am still wondering what you are referring to. Perhaps you are referring to Rich Hickey's manifesto on Identity and State. One of Rich's classical examples is how a STM interface is less wordy than a Property, Methods and Events (PME) interface done in the "Delphi component" tradition. Both types of sequential interfaces are used in place of a traditional sequential API. Something that doesn't get discussed enough here pedagogically is the Bottom Line: you want some kind of interface that can preserve whatever invariants you need to maintain across sequential calls, without needing to care about the order of those calls.

Here is an example using one of my favorite old saws, Joe Armstrong's strawman rant against OO, Why OO Sucks. Joe uses the example of a Date to illustrate why OO is dumb. But his Erlang code doesn't maintain any interface invariants that represent the concept of a Date in the real world. If you have a Date "class" and the interface is SetDay, GetDay, SetMonth, GetMonth, SetYear, SetYear, then it is simply WRONG. You can't guarantee correctness across calls. The library cannot enforce at compile-time the invariants of a real-world notion of a Date. If you have a type system that can enforce these invariants, then the language -- not the library -- can enforce these invariants for you and you can now freely compose programs that want to interoperate with your Date module modulo some glue code. If you have mutators that throw exceptions at run-time, then your language really sucks because it cannot mediate messages and statically enforce that you must handle sanity checking external to the Date module itself. I don't see any languages doing this sort of approach today. Design by contract and refinement typing comes close, but when handling interfaces with dynamic and nondeterministic parts of the program is fragile and uncertain.

(We have even bigger language issues coming on the horizon with technologies like OWL and RDF. Only languages with metasystems today are prepared to work with these technologies, but none are prepared to do it in a safe way.)

If you don't obey the Bottom Line things, then you have designed your system WRONG. There have been various polemic articles in the OO trade press literature regarding this, such as Kent Beck's To Accessor or Not to Accessor (1993)[1] and Allen I. Holub's Why getter and setter methods are evil (2003). Such trade press articles are really trying to demonstrate how the dumbed down Rapid Application Development, Create-Read-Update-Delete (CRUD) pipeline breakdown in two ways: (a) methodologically, from a requirements analysis perspective (b) systematically, from a system's theory perspective. Most attempts by rank amateurs at designing OO languages the past 30 years have basically been along this axis of configuring your object "models" for "Properties, Methods and Events" APIs. (To handle some communication protocols, such as Model-View-Controller (MVC), this requires interesting explosions in parts of the object model, and the differences among MVC implementations tend to differ based on differences in the sequential interfaces.) Viewed from my perspective, such languages are not very interesting or innovative work, and it gets repeated over and over again because of buzzword-mania. My comments about such complexity explosion are not original. Clemens Szyperski wrote about this in Components and the Way Ahead: "To control the complexity explosion of peer-to-peer component architectures, component frameworks need to be pursued beyond their current weak foundation." In your phrasing, eliminating mutable state and identity, you are refering to what Szyperski calls components, not objects. Which leads me to think of your functional-object languages as really rooted in writing programs using applicative combinators. So I don't think you are capturing Alan Kay's spirit of object-orientation in your criticism.

Clojure is fairly unique in that it is one of the few programming languages that manage references, and the need for object-oriented languages that could handle references was anticipated a long time, such as in Clemens Szyperski's aforementioned work. Clemens notes that identities are usually captured in OO languages as references, but such languages do not afford high-level facilities for capturing those references (but Clojure does, with its programming model).

Fork:

As far as the interesting "A Stack Is Not An Object" section 6 of Noble's paper, I am reminded of simulation guru Bernhard Zeigler's books on object-orientation and how he traces several examples of stack data structures implemented in an OO fashion and basically gives some computer scientists some knocks for not thoroughly proving the properties they claimed their stack to have. For Alan's remarks, you have to realize that to Alan a stack is just an implementation detail, something mathematical and part of the implementation. The idea of a Stack object kind of sprung forth in those programming textbooks Alan mentions as a direct result of the "reuse" argument addressing the "software crisis". Hope that helps (hopefully if Jamie Noble reads this he finds it useful).

[1] also available in Smalltalk Report, June, 1993

Functional Objects

When someone discusses 'functional object oriented programming' they are usually talking about records of functions or existential typing. It is not unusual for textbooks (such as Cardelli's "A theory of objects" or Pierce's TaPL) to initially define objects in this functional manner, without extrinsic identity.

But I think it's fair to say there's a lot of equivocation about what 'object' means. IMO, you don't have 'OO' objects until you have identity that is stable over time. Even Oleg's OOHaskell provides identity via explicit mutable state (IORefs embedded in the objects). And OO requires a bit more than that, because actors model and CSP and pi-calculus and such are not widely considered 'OO' paradigms.

I presume one could also model extrinsic identity over time in a pure manner, but it would probably require explicit temporal semantics and relational identity (surrogate keys et al). Dedalus and temporal concurrent constraint programming do achieve this. At that extreme, however, I don't believe many people would recognize the paradigm as OO.

Fork:

Forks are not objects. There is no fork. ;-)

Archival copy of To Accessor or Not To Accessor

Here is an archival copy of The Smalltalk Report volume that contained To Accessor or Not To Accessor.

late-bound familial ties

Re: comment-62511:

… various polemic articles in the OO trade press literature regarding this, such as Kent Beck's To Accessor or Not to Accessor (available in Smalltalk Report, June, 1993) [mentioned on LtU back in 2003].

Weighing in at two pages, Beck's article must be the shortest one ever mentioned on LtU. A diluted discussion of the same topic can also be found in Beck's Smalltalk Best Practice Patterns (1996) whose draft version (PDF, 144K, 147 pages) is included in Stéphane Ducasse's ever-growing online collection of Smalltalk books. Beck's answer to the question of the article's title is basically it depends. But, truth be told,

Pushing behavior out into objects rather than just getting information from them and making decisions yourself is one of the most difficult, but most rewarding, jobs when programming objects. Making an accessing method public should be done only when you can prove to yourself that there is no way for the object todo the job itself. Making a setting method public requires even more soul-searching, since it gives up even more of an object's sovereignty.

That makes sense to me but then again, I'm not a Smalltalker. What also makes a lot of sense is Beck's take on inheritance:

The argument against automatically using accessors rests on the assumption that inheritance is less important than encapsulation. Rick DeNatale of IBM [one of the drafters of the Smalltalk ANSI Standard] argues that inheritance should be kept “in the family.” … If you want to use inheritance, do it only between classes whose change you control. … I think inheritance is overrated.

That makes a lot of sense to me but I fail to see how you can sell this argument to a principled Smalltalker. If, as Alan Kay believes, Smalltalk rests on two cornerstone ideas of objects and late binding, it follows that in a principled dynamic language, all names ought to be late-bound. After all, if late binding is good, then later binding is even better, and latest binding is double-plus-best. The notion of knowing the names of your next of kin at compile time? How quaint! How 70's. The resolution of the superclass's name can surely be deferred until runtime, can it not? By the same reasoning, the only way to access an instance variable should be through an (overridable) accessor method — even within the variable's own declaring class.

Not Philosophy

I was speaking rigorously:

"Functional Object Oriented Programming": The use of object-oriented programming style in a language that is free of side-effects.

"Mutable State": Any variables whose initial value may be overwritten with new values through the use of side-effects.

"Object Identity": The notion that each object is assigned a unique identity at creation-time, and is deemed unequal to every object created elsewhere. As opposed to structural equality, in which two objects are equal if all of their fields have equal values.

Re: Philosophy

I see the essence of object-orientation as bundling up data along with functions that access data. More specifically: a system is object-oriented if it uses records with fields that may include functions whose bodies may access those fields.

I think you can agree that this much, at least, is philosophy.

I'm quite sure I disagree with it. ;-)

Clarification

What threw me off was your use of the phrase "inessential" along with your (IMHO) strange characterization of what makes the objects paradigm unique.

I leave philosophy to Heidigger. I'm really just going on my understanding of objects as devices that hide their primitive operations from client users. That's why I provided a long, detailed reply to Ben Titzer about object identity and mutable state, looking at how enterprise frameworks can be thought of as having high-level objects that focus on the problem domain and push non-functional requirements into (very large, complex) "primitive operations" like ORMs and scope management services like continuation servers. If you want to do something interesting with objects then you'll want object identity at the top-level in order to meaningfully discuss things like persistence.

Maybe Gilad's Avarice and Sloth will approach things differently from my intuition. I'm not surprised people don't like or find it useful to have to handle identities for even plumbing parts of the state-process, and would prefer a higher-level execution semantics that lets programmers talk about identity using structural equality (if even necessary). Why? Because I don't find it useful, either. :)

I leave philosophy to Heidigger

Best typo in years. I -like- totally dig you, dude!

*for punishment, I read the Gilad post, which turned out to be enjoyable*

Identity, State, Persistence...

The historical trend was for object-oriented systems to grow into all-encompassing frameworks supporting persistence, introspection, networking, and so on. Smalltalk went a long way in that direction, while Java and .NET took it to completion, as did the Unreal Engine.

In retrospect, I think we all made the same mistake of cramming the implementation of those features into monolithic frameworks that presume identity, mutability, and the Big 'Object' Base Class Inherited Everywhere. Now we know we can separate mutability from class constructs by assuming all data constant, and separating out mutable parts using e.g. Haskell IORef's. And we can use constructs like typeclasses to define features (like persistence, introspection, identity, printing) orthogonally to type constructs like classes and interfaces.

I expect the next trend in programming languages and frameworks will be a move away from the sort of "object oriented ideology" that characterizes the systems above, towards foundational type systems that separate the various notions (for example: records, references, equality, mutable data, type classes) and obey nice properties (for example: parametricity and, where appropriate, referential transparency) that enable us to more clearly reason about program behavior. With such a system, we can choose to draw from object-oriented techniques where they're appropriate, without having them forced upon us.

I expect the next trend in

I expect the next trend in programming languages and frameworks will be a move away from the sort of "object oriented ideology" that characterizes the systems above, towards foundational type systems that separate the various notions (for example: records, references, equality, mutable data, type classes) and obey nice properties (for example: parametricity and, where appropriate, referential transparency) that enable us to more clearly reason about program behavior. With such a system, we can choose to draw from object-oriented techniques where they're appropriate, without having them forced upon us.

This sounds idealistic and I'm not sure if it is very feasible: a more formal approach can work in small cases but is not very scalable to larger/more chaotic systems. A huge, and perhaps damning, benefit of classical OOP is the ability to let go of your objects and treat your system like the big mess that is probably being modeled, without many nice properties to rely on even in the initial problem description.

In this case, there is another way OOP can evolve: objects could become more opaque in their functionality in that we have less of an idea of how they are behaving in the system, they really are autonomous agents in the truest sense. Rather than have absolute notions of correctness, we could have statistical evidence of efficiency and accuracy (much like we look at biological system).

Can you clarify what you

Can you clarify what you mean a little more?

The reason there's an Object base class inherited everywhere is because life becomes easier when there's a top type to unify all values in the system with runtime polymorphism.

Meanwhile, typeclasses unify access to features like persistence and printing but are resolved statically, at compile time, in parametric polymorphism contexts.

It's unreasonable (in my experience) to expect long-lived modular systems to use compile-time polymorphism very broadly. To make the right degree of modularity work, you need dynamic dispatch over a runtime polymorphic interface, and if you don't have runtime polymorphism in your language, you end up having to build it yourself. But once a static type has been converted into the top type (Object base class), how do you get access to that persistence, printing etc. in a unified way?

What advantages do typeclasses give you in a language with objects and runtime polymorphism over and above the way interface constraints work in C#'s generics? If you made interfaces work with duck-typing like Google's Go, or were able to subscribe an object type to an interface from the outside like Scala implicits, wouldn't this give you all you need - i.e. the ability to state that a type conforms to a typeclass without having to modify the definition of the type?

In fact...

...in Haskell, typeclasses entail dynamic dictionary-passing in the general case. The reason is that the way parametric polymorphism works is just a little bit more subtle than what you suggest. So, parametric polymorphism, in the way you have described it, is purely prenex. That is, every polymorphic type σ is given by a grammar that looks basically like this:

    base types   τ ::= α | int | τ → τ | ...
    polytypes    σ ::= ∀α. σ | τ

This ensures that all polymorphic quantifiers occur at the outside. So when you apply a polymorphic function, all of the quantifiers are instantiated before any arguments are applied. However, languages like Haskell support a more general form of polymorphism:

    types   τ ::= α | int | τ → τ | ∀α. τ

In particular, this means that you can write a type like (∀α. τ[α]) → τ'. This is the type of a function that gets passed a polymorphic value, and so the function gets to decide, at runtime, what type to instantiate its argument at.

So this means that if you constrain the polymorphic type to support (say) printing, say with a type like:

(∀α. Printable(α) ⇒ τ[α]) → τ'

then the compiler has to be able to dynamically pass the argument the information about how to print that type as well, because obviously the argument cannot contain the information about how to print all types.

It looks like the magic

It looks like the magic phrase, in a Haskell context, is "existential variables", which bundle up the dictionary of operations along with a runtime polymorphic reference to the value. But (as one would expect in a functional language) they seem relatively limited in terms of runtime querying of interface support.

But (as one would expect in

But (as one would expect in a functional language) they seem relatively limited in terms of runtime querying of interface support.

Why would you need to query interface support at runtime? A mechanism like type classes just makes operations late bound so clients can resolve the specific implementations to use. As far as I can see, that's all you'd generally need, since clients should always know what interfaces they're using.

Consider...

...systems like web services or COM. In these cases, you have objects/processes describe themselves to the world via metadata that explains what their interface is.

I don't really see any non-dynamic way of achieving this functionality, though fancy types (eg, dependent types & reflection) certainly offer the potential to make it more reliable.

...systems like web services

...systems like web services or COM. In these cases, you have objects/processes describe themselves to the world via metadata that explains what their interface is.

But then you're just statically typing the metadata. All you need then are first-class labels, and your program is parameterized over the label.

In the limit, the protocol is a Turing complete language, but we can process even such untyped programs in a statically typed way, subject to the limits of the host language of course, ie. dependent types will get you all the way, and anything less is progressively weaker. Have I missed something that invalidates this argument?

Three modules

There's a general pattern which can be reduced to a relatively simple scenario. Consider three modules, {A, B, C}, all independently developed.

A defines some types and some interfaces (or type classes if you prefer) that those types implement. A knows nothing about B or C.

B knows nothing about A or C; it will be loading A and C dynamically, at runtime, and transporting values from A to C.

C knows about the interfaces defined in A, and needs to interrogate values it receives from B to discover what they support; but it knows nothing about B. How does it do this querying?

The problem is that you have multiple modules in the system, runtime composition, and values traveling through code across the different modules. Due to independent development, only a top type or similar fairly generally typed container for values can be used to transport values between the modules. But nevertheless, modules at either end of such a transport chain may know about one another's interfaces, or interfaces provided by a third module, and need to check for the support of such interfaces.

If there's an isomorphism

If there's an isomorphism between the modules to adapt, then I can envision a runtime that could perform the mapping automatically and satisfy the typing (ala Haskell's "deriving"). If there's no isomorphism, a fourth module must be developed which provides the mappings the data from producer to consumer.

To bastdardize Greenspun's 10th rule, anything else would seem to be an ad hoc, informally-specified, slow implementation implementation of half of the above solution.

That's right, exactly.

That's right, exactly. Existential types (which can be implemented by user code once you have first-class parametric polymorphism) give you an unknown, dynamically-computed member of an interface, but don't let you dynamically query what the interface is.

Modulo a little handwaving, to dynamically query a value for its interface, it has to have some metadata available at runtime in order to query it. In OO languages, this is the pointer to the class that every object has. An existential package is not required to have a metadata field, so you can't query it.

So if you want to do runtime querying of interface support, then you need to explicitly add the metadata to your data representation. The tricky part is connecting the analysis of the metadata to the static type, which can't be done easily in ML, but can be done in Haskell using its GADT mechanism.

IMO, this is one of the strengths of decomposing objects into smaller mechanisms, since having the pieces individually lets programmers implement optimizations like saving memory by consolidating the metadata for a whole array of objects, without the need for any special compiler hacks.

Modularity requires run-time dispatch

It's unreasonable (in my experience) to expect long-lived modular systems to use compile-time polymorphism very broadly. To make the right degree of modularity work, you need dynamic dispatch over a runtime polymorphic interface, and if you don't have runtime polymorphism in your language, you end up having to build it yourself.

I'm not sure how to read this comment. Are you saying that in a sufficiently large system, there will be the need for certain abstractions to be based on run-time dispatch? Or that every abstraction will tend to need to become run-time dispatch as the system becomes sufficiently large? The former is much more plausible to me than the latter.

One nice thing about not baking run-time dispatch into your language core is that you can then architect different dispatch policies for different aspects of your software. Look at multiple dispatch rules in languages that support it. They are usually either ad hoc based on some common use case or take an (also ad hoc) kitchen sink approach. I'd much prefer libraries that implement a few patterns cleanly with the option to customize the rules to fit the exact architecture needed. Ditto for mutability and object identity.

Amen.

This is how it ends up actually working in the real world.

Pick One: 'Baked in' Modularity vs. Monolithic Apps

In a sufficiently large system, you'll need some mechanism for composing components that weren't developed together. In my experience, this concern tends to bleed deeply through the other abstractions in such forms as identifiers, concurrency, synchronization, serialization, persistence, distribution, error-handling and recovery, resource management, security, compatibility.

I understand your idealism regarding separation of these concerns, but I do not understand your optimism nor your assertion about what is 'plausible'. When I look through Haskell codebase on Hackage, I see the same story again and again: reinventing or painstakingly adapting libraries that use different in-the-large composition concepts. I'm experiencing this story myself, developing my RDP model in Haskell. My position is: to whatever extent I need to reinvent or adapt libraries, I'm effectively - by most metrics that matter - using a new language. From that perspective, at least, I know no feasible means to separate the in-the-large composition concerns from language design.

This isn't to say that "every abstraction is run-time dispatch". One could easily have different 'layers' of abstractions, and have some layers be relatively 'closed' to avoid run-time dispatch. Peter Van Roy is known for advocating a close-knit pair - in-the-small abstractions are coupled to in-the-large abstractions. In the context of Haskell, the in-the-small abstractions are the pure functions, pure data types, and lazy evaluation, whereas the in-the-large abstractions are monads, arrows, FRP, CHP, sockets, incremental computing, etc. These bleed together in the obvious ways, with much pure data carrying references (such as MVar or IORef) and carrying code to execute in a particular context (IO (), a monad or monad transform, etc.)

I somewhat favor good support for domain modeling be at the in-the-small layers - such as pure functions, large sets, and a relational algebra - but even the lower abstraction layers will often need to be carefully chosen to meet in-the-large composition goals (e.g. for incremental calculation, transparent distribution, orthogonal persistence, tangible values for debugging, etc.).

You and I have discussed these concerns before. (A link might reduce repetition, but I can't recall the topic.) You apparently want a good language for building languages and applications specialized to a need ("fit the exact architecture needed").

But specialization has enormous costs at the in-the-large ecosystem-of-applications scale. A ton of code and productivity hours are spent adapting or reinventing libraries in subtly different ways; adaption tends only to add layers complexity, bugs, indirection, and latency. With so much code for adaption, developers unwittingly place themselves under performance pressure to build monolithic applications and components (pushing all the serialization and integration with other architectures to the outer edges of the app) - it takes a lot of muscle to move all that interface-adaption fat. Additionally, when developers cannot shove a rich 'typeful' programming model or the 'aspect-oriented' rules through the in-the-large composition pipe, they will be under additional pressure to favor 'monolithic' applications to take greater advantage of the 'local' system features.

I think that the problem we should be 'fixing' is that of monolithic applications and wasted hours adapting code. Applications should ideally be small, composable, obtaining or providing services, and widely abundant - i.e. rather than having a file-system, we could have a service-system.

Those libraries that implement architectural patterns are scratching an itch, I'll agree, but this is one of those cases where scratching only damages the skin and worsens the symptom.

PL designers should need be thinking first about the in-the-large composition, and controlling expressiveness as necessary to discourage monolithic applications. This requires 'baking in' many language properties in order that developers can easily leverage them rather than reinvent them per application.

So, basically, I still strongly disagree with your position. I do not mind having a language good for developing languages, but I do not consider that a valid basis for keeping in-the-large systems considerations out of the language. Indeed, my own ideals would ask that our language for developing languages simplifies in-the-large (cross-app, cross-service, cross-network) integration of whichever new languages we develop.

That said, I am certainly not advocating ad-hoc multiple dispatch, nor am I advocating "Big 'Object' Base Class Inherited Everywhere". Actually, I think OO is a poor paradigm for in-the-large composition due to issues under concurrency, synchronization, resource management, persistence and upgrade, distribution and disruption. OO is also not so great for domain modeling (compared to functional-relational, logic programming, constraint programming, or term rewrite systems - especially in combination with temporal semantics). Multiple-dispatch makes OO barely tolerable for domain modeling, but does so by sacrificing OO's value for secure composition (capability security model).

Anyhow, we need to make a choice between 'baking in' properties of modular composition vs. pressuring developers to build monoliths. There is certainly a sliding scale, here, but I'd push it much closer to the 'modularity' side, allowing composition between services and apps and domains, and reducing reliance on libraries (replace libraries with SaaS). I'd favor the modularity choice even though it means architecture will be less than ideal for expressing a few classes of application that had low-priority user-stories during language design.

Standard libraries are still libraries

You're right, we're rehashing, so I'll be brief. The crux of your position is this:

So, basically, I still strongly disagree with your position. I do not mind having a language good for developing languages, but I do not consider that a valid basis for keeping in-the-large systems considerations out of the language. Indeed, my own ideals would ask that our language for developing languages simplifies in-the-large (cross-app, cross-service, cross-network) integration of whichever new languages we develop.

I don't oppose keeping "in-the-large systems considerations" in mind while designing a core language, but just because you ultimately want to support scalable, distributed, low latency, fault tolerant, high throughput, child safe, odor free, low calorie, non-stick software doesn't mean all of those concerns need to have representative concepts in the language core. I think if you start with the right base, you can get to the place you want to be in a modular way, and will have a cleaner, better factored language as a result.

Standard Libraries and Core Languages

Standard libraries are still libraries

It is unclear to me what you mean to imply by this. So I'll disagree with one possible connotation and agree with another:

Presumably we could place concurrency models, string and collection types, dependency injection, data-binding frameworks, serialization, etc. into 'standard' libraries. But libraries, applications, services written with different 'standard' libraries will generally prove incompatible, needing adaptation or outright reinvention. Thus, from many relevant perspectives, we cannot consider 'standard libraries' to be meaningfully distinct from a possible 'core language' - not from the perspective of regular developers, and not even from the perspective of the language designer (who cannot be negligent in designing the standard library to achieve the desired composition properties).

But I do agree that we don't need all our big ideas to have "representative concepts in the language core".

Actually, your wording there, to me, suggests a misconception of what it means for a language to offer a feature. My understanding is that the most important language features are not directly represented as "concepts" within the language. For example, type-safety is not represented by keywords, vocabulary, or operators. Nor is type-inference. Nor is orthogonal persistence. Nor is fault tolerance. Nor is deadlock freedom. Nor is transparent distribution. Nor even is security (for sane models such as information-flow and object capability). Nor do scalability and low-latency and high-throughput typically have vocabulary - it isn't as though you can just tell an algorithm to 'be scalable!'. Many of the most important language features are those your language supports implicitly - through structure, architecture, and carefully chosen constraints on how certain concepts are expressed.

If I figure out how to support child safe, odor free, low calorie, non-stick, law-abiding software, those properties will be implicit, too ;-).

These implicit features are rather difficult to achieve in libraries while still maintaining modularity properties. One can use of design-patterns, frameworks, and self-discipline (if not outright implementing a language inside a library) but then we'd need to adapt libraries to these frameworks, compose frameworks developed for different apps... developed under different assumptions, different forms of self-discipline. Further, if we have a 'core language' capable of expressing these issues in a library, that core language is likely going to be too expressive - enough to defeat whatever properties and self-discipline you wish to ensure. A 'core language' might need to become something to which you restrict or control full access - at which it effectively becomes an intermediate, implementation language.

There is certainly no need for general purpose languages to provide multi-media, robotic motion and kinematics, image facial detection, etc. as 'core language' concepts. But these are exactly the sort of features that would be represented by an explicit ontology or vocabulary, anyway.

I think if you start with the right base, you can get to the place you want to be in a modular way, and will have a cleaner, better factored language as a result.

Well, I certainly can agree with that, though I suspect we'd conflict heavily if attempting to judge 'right base'.

Standards and cores

Presumably we could place concurrency models, string and collection types, dependency injection, data-binding frameworks, serialization, etc. into 'standard' libraries. But libraries, applications, services written with different 'standard' libraries will generally prove incompatible, needing adaptation or outright reinvention. Thus, from many relevant perspectives, we cannot consider 'standard libraries' to be meaningfully distinct from a possible 'core language' - not from the perspective of regular developers, and not even from the perspective of the language designer (who cannot be negligent in designing the standard library to achieve the desired composition properties).

There is definitely an interoperability benefit to standardization on a common set of abstractions, but just redefining "core" to include them is word games. A doctor will learn a specialized subset of English with accompanying concepts, but I don't consider that "core English." Since you're presumably not a domain expert for every possible domain, I think the best you can do for the other domains is manage a social process that encourages standards.

I see networking as just another problem domain. It's an important problem domain, there are dependencies to other problem domains, and there are big advantages to standardizations, but it's still just another problem domain.

Just Another Problem Domain?

I see networking as just another problem domain

Do you also see generic programming to be "just another problem domain"? What about error handling? Communications isn't a domain of its own; it's a characteristic that penetrates and influences all involved services and applications domains. The fault modes for communication similarly cross domains.

"Just another problem domain" connotes something I should be able to grok and implement independently of the other 'mere' problem domains.

I cannot do that with communications. If my language doesn't make it easy to handle communications failure-modes and recovery, these issues infest my 'problem domain' code. If I later want to change things, it will be time to pull out a shotgun to change or adapt the communications model across dozens of libraries from different domains. This is closer in nature to manual memory management, performance, error recovery - cross-cutting concerns. Communications, via network or not, is no more "just another problem domain" than those other cross-cutting concerns.

I have heard your opinion on the subject voiced by other language designers: I argued on PiLuD recently regarding whether concurrency is 'just a problem domain' like tax reports and spell checkers.

Anyhow, in an important sense, networking isn't a 'problem domain' at all: requirements that happen to specify 'networking' are, by nature, prescribing a solution rather than specifying a goal (such as systems integration). True 'problem domains' are the ones that directly concern the requirements of end-users, but networking concerns only the implementor. A tax report, audio-visual remote conferencing, spell-checking - these things are problem domains. Networking is in an 'answer' domain.

Therefore, fundamentally, anything we explicitly write about networking is 'accidental difficulty' (in the sense of Fred Brooks' No Silver Bullet). These are extra concerns that programmers are forced to think about in order to meet user's non-functional requirements. In that sense, these concerns are not especially different from performance or code-reuse.

I'm willing to accept that we'll never be entirely rid of accidental difficulty. But the whole purpose of language design is to claw our way out of the Turing Tarpit, which means we need to control and reduce accidental difficulty. That won't happen while we're still building 'specialized' networking or modularity or concurrency architectures per application, thus preventing them from easily working together, thus pressuring developers to build monolithic apps...

just redefining "core" to include them is word games

Rather than redefining "core", my assertion was that 'core language' is ill-defined in the first place - vague like a fuzzy line in shifting sands. The phrase might be well defined in a few rare operational contexts (core Scheme?) but, in general, 'core language' fails to make any meaningful distinctions to language users and language designers.

You mention "core English" as part of an argument, and I'm sure you could offer me a nice hand-wavy definition for it if I were to ask, but you'd never reach consensus.

So rather than 'core language' properties, what matters is 'language' properties.

Since you're presumably not a domain expert for every possible domain, the best you can do for the other domains is manage a social process that encourages standards

That's a false dichotomy, i.e. of the form: since you can't possibly run from Florida to Bahamas, the best you can do is stay at home (and argue on the Internet with Matt M ;).

Sure, you cannot be a domain expert for every possible domain. And wearing the benevolent dictator hat to encourage standards is a reasonable use of time.

But you can be a domain expert for some domains - especially those related to representing, integrating, coordinating, and composing models and applications and services. And the ability to shape social processes is also achievable, to a large degree, from within the language design: as a language designer, you have great control over the 'paths of least resistance' in your language, the extent to which static analysis and testing are easily supported, and even regarding the granularity at which code can usefully be distributed and reused.

Rambling replies

Do you also see generic programming to be "just another problem domain"? What about error handling?

To me, error handling is definitely just another architectural decision to make, that should be supported by libraries. I'm not going to commit to classifying "generic programming" since it's rather vague. Basically, the core language I have in mind establishes things such as the following:

- A mathematical universe of discourse

- A means of postulating extra-universal meaning

- A system for assigning meaning to code fragments

Specific language mechanisms and abstractions are built on top of the core. The core is not a useful language by itself, so as a language designer, it's important that you have a good story for error handling, concurrency, networking, etc - all the things users need. But you can still look at these things as a web of interdependent abstractions on top of a core.

You can identify some set of the most common abstractions and declare that these form the language (if you're defining a standard, that's even a reasonable thing to do), but that's necessarily going to be an ad hoc process, or at least driven by real world needs rather than theory. And I understand that it's vital the most ubiquitous abstractions "near the core" be carefully designed to play well together, but there's no clear set of which abstraction are "near the core".

"Just another problem domain" connotes something I should be able to grok and implement independently of the other 'mere' problem domains.

It doesn't have that connotation to me. It's not unusual for problem domains to have dependencies; e.g. two pieces of software written to different linear algebra packages will have trouble interoperating. "Problem domain" probably isn't the best term for what I have in mind, since I intend to include abstract mathematical domains as well as domains that correspond to concrete things.

Still, I wouldn't typically consider networking accidental complexity. Supporting networking on that works over existing internet infrastructure is a requirement for many software projects. Of course, you can argue that submitting tax forms online isn't really a requirement of tax software - the real goal is getting the users taxes filed by any means. You can also argue that the real goal is just keeping the user out of federal prison, but it gets ridiculous.

Modularity and Architecture

error handling is definitely "just another architectural decision" to make, that should be supported by libraries.

Interop is hard enough with FFI and other languages, especially across concurrency paradigms. When we have different error-handling mechanisms between components, it can be difficult to make them work together... much less reason about error handling policy, propagation, and recovery at the whole-application level. If working with competing linear algebra packages is difficult, just imagine how difficult interop will be when different packages use competing or incompatible models for memory management, resource management, concurrency, synchronization, state management, and error handling.

You mention dependencies. Dependency management between components is an area where significant improvements are plausible. Gilad Bracha's Newspeak doesn't allow direct dependencies between modules, and he provides a decent argument; see his posts linked at Parameterized Modules. Dependencies are ideally quite modular, which is useful for independent development, testing, and upgrade. I have often advocated an SOA-like approach of introducing a reactive match-maker between modules, with distributed services and code-distribution within services replacing most libraries.

To support modular dependencies requires a common architecture for the modules. It is critical that architectural decisions are embedded within GPPLs - at least if we are ever to simplify software development in terms of interoperable components (modules), and discourage 'monolithic' applications and software components.

I grant that the tradeoff seems acceptable in your 'language for language designers'. Languages tend to be designed by individual developers or tight-knit groups. Significant language extensions* tend to require wholistic knowledge of the language anyway - including of all other simultaneous extensions - which destroys modularity. These issues encourage language design to be monolithic by nature. (*) By 'significant' language extensions, I mean those that adjust expressiveness of a language (as defined by Felleisen) or that adjust local reasoning properties of a language. These usually cannot be achieved by local transforms.

As a language designer, I would love to see a language to help language designers splice together new experimental languages, validate their properties, build effective interpreters, compilers, runtimes, optimizers, JIT, et cetera. That is, at least, what I was looking to find before I chose Haskell to model my language (using type-classes and monad transformers).

But I do not believe that said language should be accessible to regular users of the language. If by 'core language' you mean something that is a subset of another language (core L ⊆ L) then I'd reject this highly reconfigurable language as a host language.

The ability to adjust the host language's architecture is too expressive. More critically, an inability to effectively rely upon stable, pervasive, language architectural properties is too painful. Library developers will be under pressure to provide their own architectures, to ensure their architectural assumptions aren't swept from underneath them. The proposed modularity of the language provides an outlet for this pressure - a path-of-least-resistance that kills interoperability. The result is greater reinvention, less reuse - e.g. libraries from different developers end up inventing or depending upon incompatible error-handling architectures. That way lies a graveyard of monoliths.

If I must make a choice between implementing interpreter frameworks in a stable language vs. reconfiguring the host language, I prefer the former. That allows library and language developers to depend upon stable, cross-cutting, architectural properties - and at least potentially supports integration of multiple independently developed languages in a single project. Assuming the stable host language is expressive enough, one may even be able to implement 'interpreters' without the frameworks and self-discipline that force us to reinvent and adapt libraries.

you can argue that submitting tax forms online isn't really a requirement of tax software - the real goal is getting the users taxes filed by any means.

'Submitting tax forms from my home computer' is a functional requirement describing a communication and user behavior.

That communication is essential difficulty, but does not need to be expressed in terms of networking. The detail questions of which particular network technologies are involved, which particular protocols, sockets and serialization and parsing and encryption and failure characteristics and recovery and session management - all that is accidental difficulty.

[clarified some comments]

Modularity and Architecture

You keep emphasizing that standardization on common abstractions (error handling, communication, etc.) is important for interop, but I'm not disagreeing with that. What I'm advocating is a modular approach with the goal that every language feature should depend only on the language features it uses, such that as many language features as possible can be removed or replaced.

Essentially, I do want (core L ⊆ L), but I don't think this scheme suffers from the problem you're suggesting of being too expressive for end users. If you put a well designed library in the standard, people will use it. How are you going to force people to use a single linear algebra package in your language? Also, it's not like every developer on a large system can introduce his own error handling conventions. The system architects will establish conventions and the type system will enforce them.

'Submitting tax forms from my home computer' is a functional requirement describing a communication and user behavior.

OK, that's fine. I think the argument we might have is whether there is a suitable "communications API" that meets everyone's needs while staying high level. Maybe there is, but it's certainly a much more ambitious project than a socket library, and I want the ability to configure the communications architecture for my project. Imagine you're communicating with some custom piece of hardware that doesn't even have a general purpose CPU on it. You can't bend it to support some unified communications architecture - you have to use the protocols that are there.

A dozen different linear

A dozen different linear algebra packages in the system doesn't cause nearly the same issues of having a dozen variations of garbage collectors, concurrency models, synchronization mechanisms, or error-handling models in the system. One difference is in local reasoning.

Unless your linear algebra package has some pervasive effect (such as walking your code and directly transforming linear equations) its effect is, by nature, local to the code that invokes it. The worst you'll face is some inefficient data translations for different 'types' for vectors and matrices at the boundaries of the libraries.

With different models of exception handling (consider, C++ exceptions, Windows SEH, Unix error signals, and Lisp Condition Reset all independently developed and living in the same language) the effects of the incompatibilities are pervasive, affecting even the code that doesn't depend on those features, including one another. Developers would be far less effective in reasoning about how errors affect their system, what sort of unwinding will be performed, etc.. Developers would need to 'deeply' grok the modules they are using, potentially including all the callbacks into other modules that utilize it, in order to understand the potential sources of error. Developers would probably end up with code coupled to all four error-handling systems, attempting to catch the different errors, which would be a massively redundant effort.

Similar stories can be told for different models of concurrency and synchronization, type safety, memory and resource management.

By my understanding, the basic property of 'modularity' is the ability to add and remove components without knowing very much about the other components in the system. By this understanding, inability to perform local reasoning when adding components means those components are not modular.

When a successful language is inadequate, it doesn't get fixed just once. It gets fixed in a dozen or more oft incompatible ways by independent developers who rarely have even heard of one another. With linear algebra, those incompatibilities are modular - you really can have a dozen linear algebra implementations living in the same application. But you should not generalize that to components that affect architectural (global reasoning) properties.

We cannot ever be entirely rid of waste - duplicate linear algebra packages, adapter code between them. But we should aim to control waste. As a language designer, an opportunity exists when attempting to support effective modularity (with local reasoning).

it's not like every developer on a large system can introduce his own error handling conventions. The system architects will establish conventions and the type system will enforce them.

A large system will typically use a dozen or more independently developed libraries. When the language's error-handling properties are inadequate to the purpose, those libraries will provide their own. (Same is true for concurrency mechanisms, resource management, etc.)

You seem to be assuming that the code in large systems is developed by a close-knit group with common systems architects. But when imagining the developers influencing a project, one should include all the developers of all the components that the project would ideally utilize or leverage. When measuring effectiveness and waste, include the adapter code, reinventions, and lost opportunities for leveraging existing components.

the argument we might have is whether there is a suitable "communications API" that meets everyone's needs while staying high level

I'm quite willing to sacrifice ideal suitability within any given problem domain in order to improve compatibility across problem domains - require a little extra work in-the-small in order to simplify the in-the-large challenges. I believe it important to have a simple communications model that is suitable for a wide variety of known popular communications patterns (discrete events, analog signals, data streaming, etc.) but I think it more important to have a common communications model than one ideally suitable to a particular application.

If we discover we need better support for new communications patterns for some common domains, that will be the job for the next generation of language design.

Using your language, and developers with your attitude, we'd quickly have a dozen incompatible communications architectures for a dozen different projects, then shortly we'd have incompatible libraries built within these architectures. And a similar story would be told for concurrency, synchronization, error handling, and other 'mere' architectural concerns. Developing optimizers that can work across unstable semantics and architectures would be extremely non-trivial, so you'd probably need every project to provide its own.

Rather than modularity, we'd have a monolithic 'silo' per project, each built by a small team of developers and a systems architect, with at most a few components coupled by the project's unique architecture.

I don't believe that's the right direction to be moving. If you could achieve the modularity you declare that you desire while accomplishing your goals, I'd be all for it. But I have no reason to believe that any significant degree of 'modularity' is compatible with your goals. You have so far hedged in tackling the important modularity concerns - such as local reasoning and architecture-level compatibility. Instead you have drawn false analogies between linear algebra libraries and error-handling architectures.

I expect your vision of a language would be excellent for language-designers and OS developers (those are closely related tasks), but terrible, at a community scale, for application and service developers (even before considering my vision for automated code distribution and full service computing).

I don't really disagree

I don't really disagree with anything you've written, except your judgment that merely providing a modular language design is implicitly encouraging duplication. I'd rather prevent reinvention by including powerful and widely applicable abstractions in the standard library (with examples of how to apply them to real world problems, if necessary), rather than by making it painful to use alternatives.

Modularity doesn't

Modularity doesn't implicitly encourage duplication - at least, that was not my judgement. I apologize for the miscommunication. My judgement is that the features you aim to offer are incompatible with modularity, and that you have not properly tackled the concerns needed to achieve modularity. To ensure this is stated clearly: my judgement is that your allegedly 'modular' language design isn't modular.

And it won't be modular until independent, third-party developers can extend the language design without stepping on one another's toes. Until such a time, language design in your system will always be monolithic - handled by a central systems architect who must grok the whole language design. Further, using this feature will hinder reuse of existing libraries and abstractions because one will be 'stepping on toes', which results in the duplication I was describing earlier. Duplication is encouraged because you don't have modularity.

Sure, as you said just now, you can avoid duplication and achieve modularity so long as everybody is using a common 'standard' library of abstractions. But that does not serve as a valid argument that the features you offer are compatible with modularity. Instead, the only conclusion I can make from that point is that people can, at least, have modularity and avoid duplication while not leveraging the features you aim to offer.

[...] rather than by making it painful to use alternatives [...]

Hey, don't mischaracterize here. I don't go out of my way to 'make it painful' to use alternatives. Rather, you're going out of your way to 'make it easier'. I dislike bondage-and-discipline. Wherever possible, I far prefer to control path-of-least-resistance by lowering resistance to whatever I, in my finite wisdom, consider the better path. ;-)

[edited]

Sorry, I was using my

Sorry, I was using my version of modularity there, not yours. I understand you to be saying that attempting to make things modular in the way I use the term is going to lead to interoperability problems and a lack of modularity in the way you use the term.

Configurable

What you are calling a modular language design, I'd call a configurable language design. Perhaps you, too, will find that word more fitting.

Configurability isn't the

Configurability isn't the point, though. The point is to divide the language concepts into the smallest pieces (the "modules") reasonably possible. Configurability is just a byproduct of having isolated which concepts depend on each other, since you can reuse a concept in any context where its dependencies are met, even if other parts of the context have changed.

Modularity isn't about the

Modularity isn't about the 'size' of components, but is instead traditionally discussed in terms of development process and loose coupling between 'modules'. This loose coupling exhibits itself in various forms of local reasoning, such as the ability to understand the relevant behavior of a module in terms of an 'interface' without knowing its implementation details and without considering the other modules sharing the system. I grant that there is much confusion and ambiguity regarding the term 'modular', but these are characteristics I've found in common among systems called 'modular'.

Consider: logic programming is a paradigm that is not typically called 'modular'. We could separate axioms and propositions out to one per file, and it still wouldn't be 'modular' because one generally cannot reason about system behavior without knowing details of every 'piece'. Hewitt has made a few attempts at modular logic programming, breaking logic programs down using theories and namespaces so we can perform local reasoning about behavior. But this is taking some extra design work, and actually involves increasing the size of the individual components (while, at the same time, controlling the coupling between them).

Small pieces do not modules make.

To have a modular language design, you must divide language concepts into modular pieces, such that you can reason about the behavior of the language design locally, without knowing details of the other pieces. That is what it means to have a modular language design. And that is not what you are achieving.

What you are achieving is a configurability property, the ability to plug different implementations of interdependent concepts together in a shared context, and result in a new language product.

Configurability is just a byproduct of

The way you use the word 'just', computation is just manipulation of strings in time and space, and most of my career is just controlling lights in a box.

I agree with your

I agree with your modification to my definition of "modular" - the size and number of the interfaces are indeed what are important to minimize. I also agree that in general it isn't possible to develop language features independently if they interact and that "fully modular" isn't even possible. But let me try this way - do you have an approach in mind that you consider more modular than what I'm proposing?

Modular Languages on the Mind

I certainly don't have an approach in mind that would present more configurable languages than you are proposing. ;-)

Re: asking what I have 'in mind' - be careful what you wish for. The notion of language modularity is a lot older than I am, and for many years I studied and pursued various ways to achieve it. Even though full modularity is infeasible, there are modest forms of modularity for languages - especially in language extensions - that are possible in restricted forms. I'll provide a short summary:

  • For modular, extensible syntax (richer than macros) I've found promise with variations composing Christiansen grammars and PEGs. I'm still looking for something simpler. Like macros, syntax extensions aren't effective at 'significant' language extensions (affecting widespread expressiveness or local reasoning)... but they are still very nice for describing rich data and DSLs.
  • To extend language semantics across components and services unaware of the extension, I have discovered that dynamic scoping (special vars) and various other forms of implicit context are a powerful option. Sealers-unsealers help reconcile this with security. I've written a little on this subject. Unfortunately, this technique is difficult to use with type-safety, and it hinders optimizations that are otherwise possible in my new RDP paradigm.
  • For optimization semantics, support for 'annotations' seem a reasonable basis. Annotations can usefully be provided by different names for an identity function (such as 'par' and 'seq'). We can easily parameterize these functions, too. I expect to use these, heavily. A critical aspect here is that simply removing the annotation should result in an 'equivalent' program for certain functional definitions of 'equivalent', so it doesn't actually affect language semantics.
  • A lot of semantic issues relate to static vs. dynamic code, code-generation, compiling and preprocessing data resources statically. These are common causes for using external languages. So for a long time I pursued various approaches of static behaviors and corresponding support for code-generation. I've since seen many mainstream languages embrace this idea, and Scheme improve upon what it already supported. However, these compile-time behaviors are difficult to reconcile with effective security and code-distribution models. Additionally, a few years back I stopped considering the static/dynamic distinction to be a good idea and I now favor 'continuum' models suitable for live-programming and orthogonal persistence. So I no longer pursue these techniques... except as useful design patterns for reactive dataflow programming.
  • For a long time I contemplated building a pipeline of code-walking transforms over representations of a program's AST. Combined with annotations, this technique is capable of quite arbitrary language extensions. However, it is in pursuing this technique, playing through user-stories, that I reached many of the conclusions I've already described to you. Gilad Bracha's Ban on Imports helped me get past my final stage of grief and finally lay the idea to rest.
  • Fexprs, reflection, introspection - first-class access to a representation of the code or AST at runtime. I'm not fond of this because it hinders partial-evaluation and reordering optimizations, makes it difficult to reason about information flow, and couples the implementation details of third-party modules. I favor the second-class access to code offered by extensible syntax.
  • I've also pursued various forms of languages with relatively adaptive or mutable semantics - aspect oriented programming, declarative meta-programming. Coq with Adam Chlipala's YNot is interesting, but I've yet to study it in depth. I thought IBM's hyperspaces were especially elegant. I'm quite enamored with the idea of distributed 'constraint programming' models that allow constraints to penetrate components and services. I posted recently on the subject. However, I've yet to reconcile such semantics with security and performance.

I've also attacked this problem from a another direction, which I find quite promising. Rather than making it easy to tweak the semantics of a host language, I aim to simplify abstraction and integration of diverse languages developed as a library or service in the host. This is a sort of languages-as-modules approach. The emphasis on integration is important - we need to ensure libraries can be used without adapting or reinventing them for each and every mini-language. This means the mini-languages need to 'fit' the host language's paradigm, so that we can avoid the leaky abstractions that require self-discipline.

Haskell provides a lot of nice features for abstracting languages - GADTs, type families, monad transforms, arrows. So the question is: what obstacles are still hindering clean integration of independently developed languages? Why is it that every Haskell FRP library must reinvent a whole slew of libraries before using them?

I have several answers, but I'll not provide them here. (I did, and the post was way too long and rambling. ;)

Simplifying integration of DSLs and their implementations doesn't provide all the features of a modular host language. But it does cover a lot of important use-cases and user-stories for a modular language.

The reason I ask

The reason I ask is because it's my thesis that whichever of those abstractions you ultimately use, you can organize them in the way I intend and increase modularity. I'd like to let you convince me otherwise, but maybe we should take this offline. I'll see if I can find your email...

I see networking as just

I see networking as just another problem domain. It's an important problem domain, there are dependencies to other problem domains, and there are big advantages to standardizations, but it's still just another problem domain.

The problem with libraries instead of languages is that a language can enforce properties that your library cannot, since the library is limited to the static analysis rules of your language. You can use features like type annotations and create overlay networks on top of your language's static production rules and syntactically implicit, keyword-less type checking rules, and you can build libraries using pluggable types, but currently this is very cutting edge and all pre-existing approaches to library development in this fashion have resulted in research prototypes that fall out of lock-step with the languages they build upon.

I am just speaking from experience, and having conversations with academics who built these tools in graduate school as part of their graduate work. Getting such tools out in the open is hard work, and their is a social process is that does need to be managed, but the issue is not libraries vs. languages.

I think David is talking past you in this regard, and that David and I probably see things similarly, because most of the time I agree with David even if I enjoy playing Devil's Advocate here on LtU and try to slam him with some opposing view or some niggling detail he omitted.

In other words, if anything, all your objections to David can be restated by solving the problem by avoiding it. By changing how we think about languages and how we compose languages we can unify libraries and languages and achieve the best of both worlds. Now, David does tend to emphasize access control and capability control, as well as real-time properties, and wants to achieve this all in a dynamically distributed, dynamically federated setting. To do this he needs to search for commonalities of good design techniques across all these non-functional issues in programming. He will come across to those who don't understand his mission as somebody who is over-engineering and shooting for the stars. He needs to in order to succeed and build a child safe, odor free, swiffer sweeper language.

The problem with libraries

The problem with libraries instead of languages is that a language can enforce properties that your library cannot, since the library is limited to the static analysis rules of your language.

If your language is as powerful as mathematical logic, that may be an acceptable limitation. And who says you're forced to prove your language extensions correct?

Presumably we could place

Presumably we could place concurrency models, string and collection types, dependency injection, data-binding frameworks, serialization, etc. into 'standard' libraries. But libraries, applications, services written with different 'standard' libraries will generally prove incompatible, needing adaptation or outright reinvention. Thus, from many relevant perspectives, we cannot consider 'standard libraries' to be meaningfully distinct from a possible 'core language' - not from the perspective of regular developers, and not even from the perspective of the language designer (who cannot be negligent in designing the standard library to achieve the desired composition properties).

You are describing a platform with no layers. Or, that since it appears to have no layers to application programmers, it shouldn't actually have layers. Or, maybe you are arguing that layers are impossible--I dunno. All I know is I disagree :-)

Layering, even if the application programmer does not see it, has distinct advantages. For example, for me as a language designer and language implementor, the discriminating question for each of these things you mention is:

When is my job done?

That's an over-simplification of course, but what it captures in its spirit is the layering capability. If every new system that is developed with my language entails fundamental, cross-cutting language changes and an expansion of its "core" libraries, then the abstractions are clearly not finished, and there is no layering capability. I have failed, and because of this mistake I am never done. I can never take a vacation. I can never reason about the correctness of my implementation because my implementation is by definition, never finished. In short, as a language designer, or as a compiler writer there is no "done" because there is always the next application or library that produces more work for me to do. Another implementor has even worse prospects, since there is no well-specified interface to guide them in supporting to the rest of the "higher" layers.

Then what you have is no longer a language, but a platform, with no discernable core. It's not even a leaky abstraction--it's worse--information flows from the higher layers to the lower layers, the higher layers demand ever more from the lower layers, and one mind can no longer reason about any layer in isolation. (This is almost, exactly, the Java platform).

Of course it's completely bonkers. You cannot cut this beast apart because it was never designed with internal modularity. You cannot implement a core part of the language on your own for your own purpose. You cannot reuse even the simplest of application pieces without swallowing the entire thing. And worse, you cannot even understand more than a small part of it at any one time.

Look at the domains where layering has been successfully applied: operating system kernels, networking stacks, file systems, removable media. Look at layers in hardware systems such as ISAs, caches, memory hierarchies. These things could never have stayed on the technological curve if they had no layering capability--if they were being continuously rethought and continually broken by the concerns of the ever higher layers. What if every time you had added a new CDROM drive to your desktop system you had to apply a firmware update to your BIOS and a microcode update to your CPU? Preposterous!

Ok, fine. No language has achieved the perfect "core" that many bright theoreticians and budding language designers postulate the existence of...but the idea that we should go the other direction is simply madness. But that's somewhat how I read your comments--there are no visible layers to application programmers--so let's just throw it all in one big mix?

Layering and Interoperability

I do use layering, carefully, within my language designs. I'm currently considering reactive functional-relational code in three layers: the reactive layer (for communications and authority), a total functional layer (which supports ad-hoc calculations over data), and the relational layer (compositional data type is set with restricted relational algebra - suitable for optimization and parallelism - and perhaps some datalog-like transitive compositions). Reactive > Functional > Relational, in this case, based upon which may generally invoke the others. Layering is useful for systems integration because one has a clean set of choices regarding "where do I integrate"?

But there are always cross-layer concerns that introduce 'spikes' in the design, penetrating the layers and nailing them together. For example, the data must carry references from the higher layer. References must mean the same thing among all recipients of the data.

These 'spikes' mean that the layers must ultimately be designed together, at least if they are to interoperate in a sane manner. Layers rarely interoperate effectively when developed independently - rather, you end up with is a bunch of independent stovepipe systems. Sometimes you can use a bunch of indirection and adaptors to get a bare minimum of adaptation, but that is expensive and tends to a lowest common denominator. Alternatively, you can convince everyone to use your layer - reinvention.

People sometimes point to the Internet as an example that layering works, but they forget that it 'works' (with questionable effectiveness) because everyone in the world uses the same stack - e.g. HTML, CSS, JavaScript, DOM, HTTP. Further, every element in this stack is interwoven through design if not dependencies: JavaScript and DOM know about HTTP cookies, HTTP is designed and tweaked for the needs of the HTML, the CSS references HTML tags. URIs are a clear attempt at a universal 'spike' to penetrate all layers. The Internet serves as a very poor example if you are arguing that 'layering' be performed with a purpose of interoperability or modularity.

What does 'independently developed layering' mean within the context of a language? Often, it means frameworks (dependency injection, event-loops) or self-disciplined composition with which the other application components must 'fit'. When they don't fit... they'll need expensive adaptation or outright reinvention. Even when independently developed frameworks do compose, it may require a lot of care and self-discipline and 'wholistic' knowledge of the different framework implementations.

Layering is not something regular developers should need to be performing. Pressure to perform layering means they are using inadequate tools. Frameworks are a language smell - they indicate your language failed you, so you invented a new one, and now you'll be reinventing or adapting your libraries to fit the paradigm expressed in your framework. Layering is the start of a self-reinforcing cycle that results in monolithic applications, in order to fit everything above the 'new' layer you invented and achieve the properties it offers. All this greenspunning is a symptom of ineffectually clawing your way out of the Turing Tarpit. (OTOH, this is also why frameworks are an excellent place to look for new language ideas.)

So I'm not saying that layers are 'impossible'. They exist. Capability security model even has whole layering patterns (membranes). Layers can be quite useful when your language is inadequate to your purpose, and you need invent a new language.

But if the goal is interoperability and modularity, layers won't help you.

If every new system that is developed with my language entails fundamental, cross-cutting language changes and an expansion of its "core" libraries, then the abstractions are clearly not finished

Matt M is the one who wishes for developers to have the ability to make fundamental, cross-cutting changes via libraries.

I'd be the guy in the opposite corner, arguing that we should be trying to get the language right rather than providing developers tools to make a bunch of wrong languages.

it's worse--information flows from the higher layers to the lower layers, the higher layers demand ever more from the lower layers, and one mind can no longer reason about any layer in isolation

The problem with that argument is the implied assumption that thinking in terms of 'layers' is an end, a goal of its own. You should be concerned about local reasoning, where 'local' certainly does not mean 'layer'.

The ability for information to flow in flexible patterns has intrinsic value - that is an end, a goal of its own. It's a good thing if information can flow easily between components, which do not even need to be organized into 'layers'.

It is also important that we can reasoning easily and locally about these information flows, of course. For that purpose, I favor capability security model, sealer-unsealer pairs, and some annotations regarding sensitive information to control distribution of agents. I strongly disfavor layering as basis for local reasoning, though layers can be expressed if necessary (via membrane patterns in the capability model).

I also favor zero-tier architecture via mobile agents and code distribution, though this only affects implementation-level layers (any application-level layers and indirections would remain).

what you have is no longer a language, but a platform, with no discernable core [...] you cannot cut this beast apart because it was never designed with internal modularity. You cannot implement a core part of the language on your own for your own purpose

I made an argument with respect to the questionable distinction between a 'core language' and the 'standard libraries'. That does not preclude the existence of libraries outside the language's standard, and of services.

Certainly, there remain clear distinctions between "in the standard" and "not in the standard", or between "in this library" and "in that one", or between "talking with this object" and "talking with that one".

What if every time you had added a new CDROM drive to your desktop system you had to apply a firmware update to your BIOS and a microcode update to your CPU? Preposterous!

Yeah! Ben Titzer: 1, Straw Man: 0! Woot! Go Ben, go! ;]

No language has achieved the perfect "core" that many bright theoreticians and budding language designers postulate the existence of

And none ever will. Or, at least, if we ever do find the 'perfect' language we'll never be able to prove we found it. Isn't that nice and cruel? But I think we're a long way from finding a perfect language. We can make a lot of steps towards better ones.

...but the idea that we should go the other direction is simply madness. But that's somewhat how I read your comments--there are no visible layers to application programmers--so let's just throw it all in one big mix?

You overrate layering as a strategy. It has some utility, but isn't actually responsible for most of the advantages you are attributing to it.

These 'spikes' mean that

These 'spikes' mean that the layers must ultimately be designed together, at least if they are to interoperate in a sane manner. Layers rarely interoperate effectively when developed independently - rather, you end up with is a bunch of independent stovepipe systems. Sometimes you can use a bunch of indirection and adaptors to get a bare minimum of adaptation, but that is expensive and tends to a lowest common denominator. Alternatively, you can convince everyone to use your layer - reinvention.

I disagree. Layers work best when one is designed on top of the previous, typically long after the previous was already implemented and working. If the previous layer was designed well it will survive and evolve little or not at all due to what is placed on top of it. Networking is instructive here. There was a scad of ad-hoc LAN technologies developed, none were interoperable. So IP was developed as layer on top of these multiple incompatible protocols. None of them required modification to accomodate IP--it used encapsulation instead. UDP and TCP run on top of IP, again without any modifications to IP, just using encapsulation. IP runs a handful of other standardized protocols, again, without modifications. Fast forward a little bit and the number of application-level protocols start to explode--SSH, HTTP, DNS, FTP, SNMP, SMTP, most of them using TCP as their transport layer. Does TCP need to be modified to support them? Do kernels the world over need to be patched to support the fancy new application protocols? No, of course not. And no, this is not a goddamn strawman. Of course, TCP implementations were tuned to the specific usage patterns exhibited by various application protocols, but it didn't have to fundamentally change in any way to support them.

People sometimes point to the Internet as an example that layering works, but they forget that it 'works' (with questionable effectiveness) because everyone in the world uses the same stack - e.g. HTML, CSS, JavaScript, DOM, HTTP. Further, every element in this stack is interwoven through design if not dependencies: JavaScript and DOM know about HTTP cookies, HTTP is designed and tweaked for the needs of the HTML, the CSS references HTML tags. URIs are a clear attempt at a universal 'spike' to penetrate all layers. The Internet serves as a very poor example if you are arguing that 'layering' be performed with a purpose of interoperability or modularity.

As I just pointed out, the internet is a lot more than just webpages. And I'd also like to point out that those examples you just cited actually do exhibit (some) layering, they were ostensibly not codesigned, and yes the internet works to the tune of... more than a billion people having conversations like this one all over earth every single day. HTML was designed before JavaScript, and before CSS. HTTP can deliver formats other than HTML--and URIs are just a naming scheme which can specify any protocol. And people use JavaScript for other purposes than webpages, too.

Hey, I'm not saying I love HTML, JavaScript, or CSS. Actually I hate the lot of them and might actually agree with you if you proposed they all be thrown away and redesigned together, but you're being disingenuous with these examples in your attempt to discredit layering as a concept. And you forgot DNS (which has nothing to do with HTTP, HTML, CSS, or JavaScript) and the rest of the afore-mentioned stack which were already fully-fledged before the web was even a twinkling in Tim Berner Lee's eye.

Layering is not something regular developers should need to be performing.

Well that we can agree on. Except I cannot pin down the definition of "regular" exactly.

Pressure to perform layering means they are using inadequate tools. Frameworks are a language smell - they indicate your language failed you, so you invented a new one, and now you'll be reinventing or adapting your libraries to fit the paradigm expressed in your framework.

I think that's a hopeless outlook, the kind of outlook I objected to so violently in my previous reply. If we accept this premise, then the logical conclusion is that the language must eventually swallow everything. Will the language swallow complex numbers? Matrices? Quaternions? Tensors? Differential equations? And after it eats all of mathematics, will it then eat XML parsing, servlets, webpages, databases, persistent storage of all kinds, filesystems, 3d graphics, game engines, cryptography, networking protocols, dependency injection, etc? I'm actually somewhat serious here--what exactly is a framework then?

Layering is the start of a self-reinforcing cycle that results in monolithic applications, in order to fit everything above the 'new' layer you invented and achieve the properties it offers. All this greenspunning is a symptom of ineffectually clawing your way out of the Turing Tarpit. (OTOH, this is also why frameworks are an excellent place to look for new language ideas.)

I have an ontological object to this statement. If the application is monolithic, then by definition you cannot separate its constituent parts. If layers are separable parts, then it follows the application is not layered, since it has no separable parts.

You overrate layering as a strategy. It has some utility, but isn't actually responsible for most of the advantages you are attributing to it.

I just pointed out a number of examples of layering in indisputably successful systems, like, e.g. networking and the internet, filesystems and kernels. You can only blur the definition of layering so much before your arguments are no longer applicative. There are plenty of other examples in computer architecture to draw from if we want to go down that road.

Not Hopeless

Frameworks are a language smell [...]

I think that's a hopeless outlook [...]. If we accept this premise, then the logical conclusion is that the language must eventually swallow everything.

The corrected logical conclusion is: therefore, languages will subsume the patterns of composition that require frameworks or languages will always stink.

If you accept that languages will always stink (as I do) then you can make a goal of designing languages that stink less by finding common classes of frameworks to subsume. For example, introducing continuous temporal semantics as pervasive in the language (crossing all libraries) can easily subsume frameworks for streaming data in a wide variety of problem domains (physics, multi-media, UI, robotic control, etc.).

Will the language swallow complex numbers? XML parsing? Webpages? Persistent storage? 3d graphics? Game engines? Cryptography? Networking protocols? Dependency injection? etc? I'm actually somewhat serious here--what exactly is a framework then?

There are plenty of (non-mainstream) languages that couldn't represent complex numbers effectively. To represent composite numbers would require a lot of careful self-discipline to always carry two numbers together in the right order, explicitly. Then people wised up and introduced 'composite types' as a language feature. In languages with composite types, developers are freed from attention to certain details and thus can focus on higher-level challenges - such as dealing with concurrency.

The language didn't need to swallow complex numbers in particular. Instead, the language swallowed a whole darn universe in which complex numbers live, and with just one feature also support a variety of other domains involving structured data. Complex numbers could then be provided as a simple library, without any special discipline requirements to use it.

A similar story would be told for other elements and 'potential' frameworks you name. Should a language support matrices and vectors and quaternions? Well, sure - but that doesn't mean it needs to provide vocabulary for those directly.

If the application is monolithic, then by definition you cannot separate its constituent parts. If layers are separable parts, then it follows the application is not layered, since it has no separable parts.

First an application is 'monolithic' to whatever extent it is not 'modular'. Modularity connotes the ability to independently develop and test the modules then have them interoperate in a predictable manner even when one doesn't know all the other modules in the system. Layers can provide separation, but they are rarely modular - one cannot have a bunch of independent third-party developers provide you 'layers' instead of 'modules'. It wouldn't even make sense to try.

Second, layers are rarely separable parts within applications. They can easily become interdependent unless one takes considerable care to avoid even the subtle forms of 'spike' coupling. Within applications, it's very rare to see fully separate layers: the lower layers might be only weakly coupled to those above it (i.e. by callbacks and references) but the converse is rarely true.

I just pointed out a number of examples of layering in indisputably successful systems,

Besides the inherent sample bias of only pointing at successful systems, you didn't present any arguments that layering itself was a cause of success for those systems. Correlation is not causation. One can make a very reasonable argument for the opposite position - that layering often exists because of success: layering is added because successful platform technologies are entrenched (so you have to use them) but inadequate (so you develop and overlay a hopefully more adequate model).

[edit - I initially cut these points; my original versions were a little aggressive after being called 'disingenuous'. But it now seems like I'm ignoring your arguments, so I've restated the points.]

I disagree. Layers work best when one is designed on top of the previous, typically long after the previous was already implemented and working.

What were you disagreeing with, precisely? My assertion was about independently developed layers. When one layer is 'designed' to work atop another, that contradicts independent development. (See below - point on 'not codesigned' - for some clarification on what I mean by 'independently developed'.)

With regards to your examples: that "scad of ad-hoc LAN technologies" is a fine example of layers that were developed independently. And TCP and UDP were designed together with IP, as a single suite. IP was able to 'adapt' to underlying incompatible protocols, but I've never claimed adaptation was impossible - only that it is ineffective and inefficient, and my recollection is that IP was (like many adaptation layers) a least-common-denominator approach - sacrificing effectiveness to achieve compatibility.

the internet is a lot more than just webpages

I agree. But I could have told a similar story for any technology stack on the Internet.

ostensibly not codesigned

There seems to be some terminological confusion, here. By 'independently developed' I meant the sense: 'developed without considerable awareness or knowledge of the other product'. If I develop an application to run on top of Microsoft Windows, it certainly is not 'independently developed' from Microsoft Windows, even though it might be developed independently from other applications that run on top of Microsoft Windows.

This is a much stronger assertion than 'not codesigned'. But it is also this stronger level of 'independence' that I consider critical for modularity. If you were misunderstanding, you should re-read my arguments with this clarification.

In any case, I think you'll find plenty of layers that, while not 'codesigned' to work together, have led to a very 'interactive' design. HTTP has seen several adjustments to meet the needs of browsers and the applications built atop it (e.g. support for cookies and attributes and longer-lived pipes). While HTML was designed before JavaScript, you'll find that special tags and attributes just for scripts are now part of the HTML specification, and JavaScript is mentioned by name in the spec. It hardly matters that layers are not 'codesigned' when they can integrate in these cyclic manners.

When will HTML be 'done'? what about HTTP? Stability, expressiveness, effectiveness, and efficiency are what matter, not 'layering' or reaching a point where we can call something 'done'.

⨯-ref

Re: comment-62635:

… developers unwittingly place themselves under performance pressure to build monolithic applications and components (pushing all the serialization and integration with other architectures to the outer edges of the app) …

Note to self: Software Components: Only The Giants Survive, Butler W. Lampson.

Top types are rarely useful

The reason there's an Object base class inherited everywhere is because life becomes easier when there's a top type to unify all values in the system with runtime polymorphism.

How so? What would it actually be used for in practice? By necessity there's not a whole lot that can be safely done with something of the top type. I've mostly seen the top type used as a placeholder when doing things that flagrantly violate any sort of type safety, which is really not the sort of thing that ought to be encouraged in a language with pretensions of having a static type system.

Not to mention that putting operations on the top type that can be overridden in subtypes will break parametricity for generics which is also a very bad thing.

But once a static type has been converted into the top type (Object base class), how do you get access to that persistence, printing etc. in a unified way?

Well, you probably don't, which is why converting things to the top type is a bad idea.

What advantages do typeclasses give you in a language with objects and runtime polymorphism over and above the way interface constraints work in C#'s generics?

Much greater expressive power? Add multiple dispatch, parameterization over interfaces and generic types, polymorphism based on return type, interface implementation, etc. and then it might be comparable.

Runtime polymorphism just amounts to bundling data together with functions that operate on it; what mechanism the language provides for doing so implicitly isn't that big of a deal. It's not even that hard to do manually.

Big Object Base Class

It's unreasonable (in my experience) to expect long-lived modular systems to use compile-time polymorphism very broadly

Real open-world programs will need a mix of compile-time and run-time polymorphism.

For example, in an extensible gameplay framework, the framework will define a base class for gameplay objects and a set of functions defined by all, and separately-compiled user-made modules will Define subclasses and instances overriding those functions -- all in the standard object-oriented style.

But concerns like persistence, printing, and so on can best be implemented using compile-time polymorphism. So, if your module defines a way to print widgets, and mine defines a way to print arrays of printable things, then modules which statically import both your and my module can print arrays of widgets.

This approach has two benefits: It prevents modules from being able to print things that aren't printable, a situation that commonly arises with the Big Object Base Class that defines system-wide functionality. And it enables features like printing, persistence to be defined independent of the class/interface hierarchy. For example, the Haskell typeclass libraries that define mathematical structures (fields, vector spaces) don't readily translate to C# generics with interface constraints since you need to modify the base class source code in order to define interfaces on them. This does work with Scala implicits because they're instantiated statically and are comparable to Haskell typeclasses in expressive power.

Run-time polymorphism isn't all that different

But concerns like persistence, printing, and so on can best be implemented using compile-time polymorphism. So, if your module defines a way to print widgets, and mine defines a way to print arrays of printable things, then modules which statically import both your and my module can print arrays of widgets.

Printing actually makes for an interestingly trivial example: If you have types A and B, and functions printA :: A -> String and printB :: B -> String, you can write a trivial sort of "runtime-polymorphic" function to print a printable thing with a type like printThing :: forall a. (a, a -> String) -> String. No matter what type it receives at runtime, as long as type safety is maintained, everything works. All that's required is to keep each type's own printing function conveniently at hand, perhaps by bundling them together in a record type.

Of course, that particular function is patently silly because it's equivalent to printThing :: () -> String or just a plain String in a lazy language.

Many examples of runtime-polymorphism can be similarly replaced with functions that define an "interface" and the actual "polymorphic" type being hidden inside a closure or by a quantifier.

Things only start to get tricky when a subtyping relation is introduced and expected to play nicely with the rest of the type system, or when "methods" need access to the representation of data other than the piece they're bundled with. OO languages tend to borrow this trouble right up front and complicate everything, but I'm not convinced either is necessary in most cases.

It prevents modules from being able to print things that aren't printable, a situation that commonly arises with the Big Object Base Class that defines system-wide functionality.

The problem there is defining operations at inappropriately high locations on the subtype graph, right? Because a top type such as "Object" is certainly useful in a language with sensible subtyping that includes parameter variance.

The top type as greatest element of the subtype relation indicates "this is uselessly inclusive and can't do anything interesting", while the bottom type as least element indicates "this is contradictory and can't happen". Neither is useful directly but make it possible to express things that are, such as sibling types with an inherited method taking parameters of unrelated types; the parent type must accept the intersection, which would be the uninhabited bottom type, meaning it is impossible to ever instantiate the parent type directly.

From what I am familiar

From what I am familiar with, objects always have identities by their very nature -- it is the only stable part of an object over time.

While your post is quite lengthy, I disagree with this first statement. It is not identity that implies mutable state, but rather the opposite: mutable state implies identity. Remove mutable state and you can remove identity.

The fact that almost every object-oriented language I know of considers identity intrinsic to objects is severely disappointing to me. This is because they consider immutable objects to be the special case, and not the other way around.

I claim,

Mutable objects are the special case! It is only those ones that need identity!

This seems attached to language design when it arguably...

...shouldn't be.

The fact that almost every object-oriented language I know of considers identity intrinsic to objects is severely disappointing to me.

If your object definitions need datatype generic equality, then you need an "Equatable" interface similar to .NET
s IEqualityComparator. This idiom may be uncommon to you if you are used to Java programming, since the JDK has never implemented the somewhat infamous Equatable proposal.

But Object.equals() doesn't necessarily constitute the identity and state argument. That isn't the issue. In most object-oriented enterprise applications there are tools like ORMs that implement services in the caching layer called "Uniquing": a one-to-one correspondance between every entity object and every datum loaded. Even then the uniquing might break down into a Unit of Work and entities are only uniqued within that context.

And we probably don't need mutating cells to write most of these enterprise apps. It is really only done that way because of EJBs, annotations and reflection (IMHO) facilitating Rapid Application Development, CRUD applications. We could pass pure scalar values around and a GUI could know how to render these, and when we edit an input box on an input screen, the object itself wouldn't be mutated in memory. But ultimately we need some semantic context to perform persistent actions, and that goes back to the Unit of Work I mentioned earlier. What does it mean to "delete" a Customer? Can we delete the same Customer twice? Are we talking physical zeroing of memory kind of deletion, or logical deletion where the Customer record in the database is moved to a History table for auditing purposes and a note is made in a redundant general ledger on another server explaining the action? So there is conceptually an identity at some level, and what you are really doing here by trying to avoid the questions of mutable state and identity is you are really hiding when this happens using a ton of structural recursion and fancy functional programming (at least in my view). Your client API becomes the entity API in terms of what a mutable object is.

Now, whether this is a good idea I think depends entirely on auditability. Old Java enterprise systems such as ones built on Struts 1 contained a lot of boilerplate code and were hard to understand and debug. The code itself didn't bury a lot of marshaling logic and forced the programmer to look at and deal with reams of reptitive marshaling calls due to the God forbid reason there was an error there. Programmers weren't good at writing this code anyway so system's like Spring hid it, and then systems like JBoss Seam hid it even further by providing a stack-based simplification of continuation servers. The downside to such a system like Seam is that it becomes as difficult as understanding a flow chart and scales as poorly in terms of end-user comprehension (IMHO, again). The upside of this is that since systems like Seam are continously executing the same code paths over and over again, QA can be improved consistently - simply by increasing code path coverage and also testing more and more corner cases. Developers don't have to worry as much about there being a bug in this part of the system because it gets tested by millions of users a day, so the QA is there to ensure it Just Works.

Lessons taken away:

  • Enterprise frameworks tend to consider identity intrinsic to entity instances.
  • Languages will never have a general, all-serving built-in notion of equality; datatype generic equality is necessary for rich comparisons among objects
  • Similar to Rich Hickey's remarks on identity and state, arguing that we are more interested in objects at particular points in time, when "something interesting happens", frameworks possess their own notion of identity and equality and these notions are above the level of some class implementation details.
  • it is an intriguing possibility that most enterprise software is really about developing a metasystem that can capture these ideas effectively and allow for efficient, scalable program execution
  • this has nothing to do with writing programs in an applicative style; it is orthogonal and so it doesn't make sense to talk about whether "mutable objects are the special case".

If your object definitions

If your object definitions need datatype generic equality, then you need an "Equatable" interface similar to .NET's IEqualityComparator. This idiom may be uncommon to you if you are used to Java programming, since the JDK has never implemented the somewhat infamous Equatable proposal.

Well I can't correct for a failure of imagination. There is a large design space. A typeclass could have equals() and hash() methods. Or another HashEquality object with hash() and equals() methods. But making an interface with just two methods for the sole purpose of the HashMap class just seems wrong to me. The simplest way (as I do in Virgil) is to instead supply the equals() and hash() methods as first-class functions to the constructor of HashMap. Then there is no need for an IdentityHashMap, no extra interfaces to design, no extra classes to implement, one can just supply different methods in the constructor, everything is type safe...but I digress....

But Object.equals() doesn't necessarily constitute the identity and state argument. That isn't the issue. In most object-oriented enterprise applications there are tools like ORMs that implement services in the caching layer called "Uniquing": a one-to-one correspondance between every entity object and every datum loaded. Even then the uniquing might break down into a Unit of Work and entities are only uniqued within that context.

Ok, so you have discovered the fact that data outside of the program, perhaps in a database, is mutable (and has identity!). And some ORMs model this mutable data with mutable program objects. But that is not inherent in these program objects--one could as well model each datum as immutable and the mapping between its identity (!) and its (structured) value as the mutable part. That actually makes implementing transactions far easier, since the management layer doesn't need to scan data objects to see if they have been mutated, nor implement any pesky accessor methods!

So there is conceptually an identity at some level, and what you are really doing here by trying to avoid the questions of mutable state and identity is you are really hiding when this happens using a ton of structural recursion and fancy functional programming (at least in my view). Your client API becomes the entity API in terms of what a mutable object is.

I did not mean to propose making your program entirely functional, but mostly immutable. I find thinking in this fashion to be far more tractable than thinking in monads, or thinking about a rat's nest of tangled pointers and the hell of idiotic accessor methods. When I think this way, it often turns out that the majority of the program turns out to be immutable, or can be easily designed to be so, and the small minority is mutable. Then again there are some programs that have to manipulate giant arrays of mutable integers or floating point numbers--and hey, it happens. But giant tangles of mutable pointers are almost never indicative of real-world, i.e. persistent and important data structures that would have any chance of existing outside of the twisted imagination of a programmer inside his favorite (or least favorite) program.

A typeclass could have

A typeclass could have equals() and hash() methods. Or another HashEquality object with hash() and equals() methods. But making an interface with just two methods for the sole purpose of the HashMap class just seems wrong to me. The simplest way (as I do in Virgil) is to instead supply the equals() and hash() methods as first-class functions to the constructor of HashMap.

Except first-class functions are sort of like a type classes with an apply() method. So it seems you really are doing the same thing, just a little differently. The question then becomes, why are there two mechanisms of expressing semantically equivalent abstractions? Type classes would seem to be the more general mechanism here, with a little extension.

Why give it a name

Except first-class functions are sort of like a type classes with an apply() method

I'm not sure if you would really want to define a type class for every place in a program that you used a first class function on a type parameter...the declaration overhead would seem prohibitive. It's just a question of how complex the interface has to be before you give it a name.

You could get by without type classes but instead, at every place you declared a type parameter, you also declared a real parameter that was a struct of the methods on that type--i.e. manual dictionary passing.

There's a tradeoff. One mechanism (first class functions) is good for the small scale. Another mechanism (type classes) could do the job of the first function, at higher overhead for small scale, but lower overhead for larger scale. Thus you have both mechanisms in one language for the same reason that you might own both a bicycle and a car.

I'm not sure if you would

I'm not sure if you would really want to define a type class for every place in a program that you used a first class function on a type parameter...the declaration overhead would seem prohibitive. It's just a question of how complex the interface has to be before you give it a name.

I would take this as an argument for reducing the verbosity of class instances, permitting anonymous instances, and making type class instances first-class values. As Occam says, do not multiply entities unnecessarily. The duplication in Haskell's standard library, ie. sort/sortBy and friends, is not a benefit!

That could get tricky

I would take this as an argument for reducing the verbosity of class instances, permitting anonymous instances, and making type class instances first-class values. As Occam says, do not multiply entities unnecessarily. The duplication in Haskell's standard library, ie. sort/sortBy and friends, is not a benefit!

Need to be careful with that sort of thing, though.

Type classes amount to functions from types to values, much like the capital-lambdas in System F (which are roughly equivalent to forall. in Haskell) with the addition of being able to, essentially, pattern match on type constructors of their arguments. Needless to say, this completely breaks parametricity because that's the entire point, so there still has to be some way of indicating it in the type signature.

One possible approach for faking "local" instances is to use GHC's dynamic scoping extension with explicit type class dictionaries to bring mock-instances into scope as needed. Extending this to include dynamic binding of non-parametric type-to-value functions sounds potentially viable, but would require propagating information indicating which types the function is defined on, which probably requires a more sophisticated kind system, and then eventually you reinvent Omega or end up using Agda or something.

Thanks for the heads-up.

Thanks for the heads-up. Entirely coincidentally, I just came across this paper, which would seem to address exactly the issues you raise using kinding to distinguish parametric from non-parametric contexts.

Probably not all that coincidental

How to deal with awkward interactions between type classes and various forms of type-level functions is an active research topic right now. Both that and what you mentioned are, I suspect, at root an issue of taking a vaguely System F style language and bolting on features that, to be first-class entities in full generality, would require dependent types. As GHC piles on the extensions, the inconsistencies become more apparent.

See also this LtU thread.

I have nothing against

I have nothing against dependent types per se, I just haven't come across a dependently typed language I'd actually like to use. Maybe we'll get there some day, or we'll find some sweet spot in between that gets us "just enough" type-level programming to satisfy the vast majority of programming tasks without going to full theorem proving.

Not safe

But making an interface with just two methods for the sole purpose of the HashMap class just seems wrong to me. The simplest way (as I do in Virgil) is to instead supply the equals() and hash() methods as first-class functions to the constructor of HashMap. Then there is no need for an IdentityHashMap, no extra interfaces to design, no extra classes to implement, one can just supply different methods in the constructor, everything is type safe...

It's not, because nothing then prevents you from creating two maps with different eq or hash functions but having the same type, in a way that breaks the implementation of any binary operation that is part of the map abstraction. You really want a functor here (or, equivalently, a type class).

It's not, because nothing

It's not, because nothing then prevents you from creating two maps with different eq or hash functions but having the same type

That's actually by intention.

...in a way that breaks the implementation of any binary operation that is part of the map abstraction. You really want a functor here (or, equivalently, a type class).

If you define it with a type class or functor you preclude this use case by design. I think that is a little too extreme; it's a compromise, but binary operations on maps can only make guarantees when the equality/hash functions match.

well...

I did not mean to propose making your program entirely functional, but mostly immutable.

And I am saying that stuff like talking about "inessential" (Tim Sweeney) and "special cases" (Ben Titzer) doesn't say anything other than misleading things, since object-oriented analysis is orthogonal to language design & implementation issues, and that has mostly beared fruit if you look at real-world frameworks and applications. Phrases like "inessential" and "special cases" seem to miscommunicate how things are Structured. My whole reply was intended to communicate the structure I see in real-world applications. - And when I gave my example, I said nothing about "giant tangles of mutable pointers".

let's quote Alan Kay On Messaging:

If you focus on just messaging - and realize that a good metasystem can
late bind the various 2nd level architectures used in objects - then much
of the language-, UI-, and OS based discussions on this thread are really
quite moot. This was why I complained at the last OOPSLA that - whereas at
PARC we changed Smalltalk constantly, treating it always as a work in
progress - when ST hit the larger world, it was pretty much taken as
"something just to be learned", as though it were Pascal or Algol.
Smalltalk-80 never really was mutated into the next better versions of OOP.
Given the current low state of programming in general, I think this is a
real mistake.

I think I recall also pointing out that it is vitally important not just to
have a complete metasystem, but to have fences that help guard the crossing
of metaboundaries. One of the simplest of these was one of the motivations
for my original excursions in the late sixties: the realization that
assignments are a metalevel change from functions, and therefore should not
be dealt with at the same level - this was one of the motivations to
encapsulate these kinds of state changes, and not let them be done willy
nilly. I would say that a system that allowed other metathings to be done
in the ordinary course of programming (like changing what inheritance
means, or what is an instance) is a bad design. (I believe that systems
should allow these things, but the design should be such that there are
clear fences that have to be crossed when serious extensions are made.)

It is amazing how Kay's quote out of context-- about the discussions being really quite moot-- still applies here. I guess programmers like debating moot stuff.

let's quote Alan Kay

Re: comment-62754:

let's quote Alan Kay On Messaging

Note to self: seems like the link to Kay's post was originally mentioned in comment-48119.

To lift a related quote from a 2003-07-23 email exchange between Alan Kay and Stefan Ram:

OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.

Identity is valuable in the absence of mutability

Mutable objects are the special case! It is only those ones that need identity!

I think this is wrong. Consider a simple applicative-order language without side effects. A linked data structure in this language must therefore be finite, or circular. Given such a structure, we identify a finite structure quite easily; but deciding whether a structure is finite or circular requires object identity. Similarly, determining whether two such structures are equal requires object identity in general: without, we cannot identify the case of two equal circular objects in finite time.

Circular?

Hm, I believe such a language doesn't enable you to construct a circular structure in the first place, unless you throw in a fairly non-standard fixpoint operator.

re: circular?

Hm, I believe such a language doesn't enable you to construct a circular structure in the first place, unless you throw in a fairly non-standard fixpoint operator.

Here, in a purely functional subset of the applicative order language standard Scheme: "(define (make-circular) (cons 'cyc (delay (make-circular))))

So, please exhibit an implementation of a function that returns the length of a finite list or #f if the list is not finite in a Scheme implementation that automatically forces delayed values.

If you added in a non-scheme concept ... a non-standard concept, at least, .... that "(delay (make-circular))" always returns a value that is EQV? to all other values it returns ... that is, if you added that case of object identity in a functional context ... then you could implement the challenge function. Otherwise you can't.

delay is no circularity

I don't think your implementation is fair. The OP (mdw) claimed that in such a language, lists are either finite or circular. Yet delay allows to build infinite, non-circular streams, such as the classical recursive "stream of natural numbers staring from `n`".

About circularity : is it really a good thing that we are able to decide whether a list is circular or infinite ? I'm not sure observing sharing is a "good thing" in a purely functional language. In particular, I suppose it breaks most reasonable definitions of "referential transparency", which usually refers to equality (or equivalence) of syntactic expressions.

I don't know whether it's a

I don't know whether it's a good thing or not. There's a tradeoff between being able to answer questions about circular structures and referential transparency, and I don't know which is more important. But it's certainly an interesting question which arises independently of mutability, which was my point.

I vote for referential

I vote for referential transparency. You are the creator of your data structures, why would you want to find out if they are circular or not? You should know that right when you are creating them.

Well, we just had another

Well, we just had another thread about the limitations of parsing combinators that gives an example of why you might like to test for identity equality, but I'm with you that we should be explicit about where we want that capability and not allow it for every value.

You get around the

You get around the limitations of parsing combinators by using ANTLR. :-)
(yes, I also have developed my own library of parsing combinators, so I know what I am talking about)

Not necessarily;

I vote for referential transparency. You are the creator of your data structures, why would you want to find out if they are circular or not? You should know that right when you are creating them.

That was just one trivial example. More examples:

  • List length calculation, with a reliable exception on a circular list.
  • Guaranteed termination for equality comparisons on linked structures.

But I think this remark misses the point: as a writer of library functions, you're not the creator of the input data structures. The creator probably does know whether the structure is circular; but by the time it's gone through several layers of library functions, that knowledge is typically lost.

You could, I suppose, argue that one should pass knowledge about circularities down through the layers (though this seems rather cumbersome to me); but it doesn't help with structural equality anyway.

I don't see any problem with

I don't see any problem with passing along the information that I have a circle instead of a finite list. This is the same as that I pass a list to a function, not a number.

That you cannot tell in finite time that an infinite list is infinite by traversing it should be clear, and is no reason to redesign your language ... The same holds for comparing infinite structures via structural equality: if they are equal indeed, you are not able to see this in finite time.

As for circular: For example my language Babel-17 (www.babel-17.com) is purely functional. I don't think that you can construct something circular in it, because there is no mutable state. No mutable state makes referential transparency possible. If you say, oh,

def mylist = 1 :: (lazy mylist)

creates a circular list, then I have to tell you, no, it creates an infinite list.

Well, Common Lisp certainly

Well, Common Lisp certainly has literal syntax for circular structures. But, yes, I was expecting some kind of fixpoint operator or similar.

objects ≣ closures

No Mythology of Object Orientation would be complete without at least a nodding acknowledgment of the venerable master Qc Na, the hero with a thousand faces, whose teachings are only known to us through the fragmentary work of his disciples. Here's one of the more widely known koans:

The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said “Master, I have heard that objects are a very good thing — is this true?” Qc Na looked pityingly at his student and replied, “Foolish pupil — objects are merely a poor man's closures.”

Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire “Lambda: The Ultimate...” series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

On his next walk with Qc Na, Anton attempted to impress his master by saying “Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures.” Qc Na responded by hitting Anton with his stick, saying “When will you learn? Closures are a poor man's object.” At that moment, Anton became enlightened.

A more complete account can be found in Objects as Closures by Uday Reddy and closures as objects by the creators of Oaklisp.

See also http://c2.com/cgi/wiki?ClosuresAndObjectsAreEquivalent.

Kill Bill much ?

Kill Bill much ?

psychiatry of object orientation

In a related field of study, Henry Baker's paper CLOStrophobia: Its Etiology and Treatment is considered seminal.

A Stack is not an object. What is?

(This is not on the subject of modularity and configurability of programming languages. Apologies for veering so far off-topic with this post of mine.)

In §6 A Stack Is Not an Object, Noble writes (emphasis in the last quoted paragraph is mine):

If object-oriented programming started with Simula, then “object-orientation” as an idea, a principle, a myth, started with Smalltalk. As Alan Kay (who did, after all, win the Turing award for this — as Nygaard and Dahl did later) puts it in The Early History of Smalltalk (my emphasis):

… a new design paradigm—which I called object-oriented.

A little further on in that chapter, there is another quote:

This [object-orientation] lead to the ubiquitous stack data type example in hundreds of papers. To put it mildly, we were amazed at this.

I was quite amazed with this quote when I first read it, and for several years later I really didn’t understand what Kay meant.

… Fifteen years later I think I understand better what Kay was writing about. A stack is basically a data structure — an abstract data type. A good object should be more than just a data structure: it should represent something outside the program; it should be at a higher level than just a data structure; and it should unify both data and behaviour.

This is in keeping with what Beck says in Smalltalk Best Practice Patterns:

Some of the biggest improvements [of using Beck's patterns] come from figuring out how to eliminate … structural code, where one object treats another as a data structure. … It is much preferable to give an object more responsibility rather than have it act like a data structure.

On this front, Smalltalk certainly delivers. In Java:

  Object.class.getMethods().length ⟹ 9

In Smalltalk (granted, some of these are in the private protocol):

  Object selectors size ⟹ 123 [GNU Smalltalk 3.1]
  Object selectors size ⟹ 470 [Squeak 3.10.2]

with the following selectors being common to both dialects: ~=, =, ->, addDependent:, asOop, at:, at:put:, basicAt:, basicAt:put:, basicSize, changed, changed:, class, copy, deepCopy, dependents, doesNotUnderstand:, error:, finalize, halt, halt:, hash, inspect, instVarAt:, instVarAt:put:, instVarNamed:, instVarNamed:put:, isArray, isBehavior, isCharacter, isFloat, isInteger, isKindOf:, isMemberOf:, isNumber, isString, isSymbol, mustBeBoolean, notNil, notYetImplemented, perform:, perform:with:, perform:withArguments:, perform:with:with:, perform:with:with:with:, postCopy, primitiveFailed, printOn:, printString, release, removeDependent:, respondsTo:, shallowCopy, shouldNotImplement, size, species, storeOn:, storeString, subclassResponsibility, update:, and yourself.

Furthermore,

  String.class.getMethods().length ⟹ 72 (Java 6)
  String allSelectors size ⟹ 338 [GNU Smalltalk]
  String allSelectors size ⟹ 966 [Squeak]

From this we conclude that Smalltalk (and Squeak in particular) is an order of magnitude more object-oriented than Java.

RoarVM

Re: comment-62729: “… Smalltalk (and Squeak in particular) is an order of magnitude more object-oriented than Java.

IBM concurs. From https://github.com/smarr/RoarVM#readme:

RoarVM — The Manycore SqueakVM

RoarVM, formerly known as the Renaissance Virtual Machine (RVM) is developed as part of a IBM Research project to investigate programming paradigms and languages for manycore systems of the future. Specifically, this VM is meant to support manycore systems with more than 1000 cores in the future.

From the Open Source Release Announcement:

The software released here comprises the Renaissance Virtual Machine (RVM) and Smalltalk code, which runs on a Smalltalk environment and implements support code for the RVM as well as an extension to the Smalltalk language called Sly3. All parts of this release have been developed as part of the Renaissance project at IBM Research.

  • The RVM was designed and implemented by David Ungar and Sam Adams.
  • The Smalltalk support code and the Sly language was implemented and designed by David Ungar and Sam Adams, too.
  • The RVM was ported to x86 multicore processors by Stefan Marr, Software Languages Lab, Vrije Universiteit Brussel.

Nice

I am shopping around for a new VM for my language, but I can't find any non-paywalled articles? Any help greatly appreciated.