Concurrent objects and SCOOP

Representing concurrency in an object-oriented system has been a long-standing problem. Encapsulating the concurrency primitives via objects and methods is easy enough, but doesn’t get us anywhere. We still end up composing our programs out of threads and mutexes and semaphores, which is still hard.

Prior Art

It’s worth skimming the things that I’ve written about here: I’ve put quite a lot of time into this concurrency problem and have written different models as my understanding changed.

Clearly, this is a broad problem. It’s one I’ve spent a lot of time on, and have a few satisfactory answers to if no definitive answer. I’m not the only one. Over at codeotaku they’ve concluded that Fibers are the right solution, though apparently on the basis of performance.

HPC programs are often based on concurrent execution through message passing, though common patterns keep it to a minimum: the batch processor starts all of the processes, each process finds its node number, node 0 divvies up the work to all of the nodes (a message send), then they each run through their part of the work on their own. Eventually they get their answer and send a message back to node 0, and when it has gathered all of the results everything is done. So really, the HPC people solve this problem by avoiding it.

You’re braining it wrong, Graham

Many of these designs try to solve for concurrency in quite a general way. Serial execution is a special case, where you only have one object or you don’t submit commands to the bus. The problem with this design approach, as described by Bertrand Meyer in his webinar on concurrent Object-Oriented Programming, is that serial execution is the only version we really understand. So designing for the general, and hard-to-understand, case means that generally we won’t understand what’s going on.

The reason he says this is so is that we’re better at understanding static relationships between things than the dynamic evolution of a system. As soon as you have mutexes and condition locks and so on, you are forced to understand the dynamic behaviour of the system (is this lock available? Is this condition met?). Worse: you have to understand it holistically (can anything that’s going on at the moment have changed this value?).

Enter SCOOP

Meyer’s proposal is that as serial programs are much easier to understand (solved, one might say, if one has read Dijkstra’s A Discipline of Programming) we should make our model as close to serial programming as possible. Anything that adds concurrency should be unsurprising, and not violate any expectations we had if we tried to understand our program as a sequential process.

He introduced SCOOP (Simple Concurrent Object-Oriented Programming) developed by the Concurrency Made Easy group at ETH Zürich and part of Eiffel. Some of the design decisions he presented:

  • a processor is an abstraction representing sequential execution
  • there is a many-to-one mapping of objects to processors (this means that an object’s execution is always serial, and that all objects are effectively mutexes)
  • where an object messages another on a different processor, commands will be asynchronous (but executed in order) and queries will be synchronous
  • processors are created dynamically and opportunistically (i.e. whenever you create an object in a “separate” and as-yet unpopulated domain)

An implementation of this concurrency model in Objective-C is really easy. A proxy object representing the domain separation intercepts messages, determines whether they are commands or queries and arranges for them to be run on the processor. It inspects the objects returned from methods, introducing proxies to tie them to the relevant processor. In this implementation a “processor” is a serial operation queue, but it could equivalently be a dedicated thread, a thread pulled from a pool, a dedicated CPU, or anything else that can run one thing at a time.

This implementation does not yield all of the stated benefits of SCOOP. Two in particular:

  1. The interaction of SCOOP with the Eiffel type system is such that while a local (to this processor) object can be referred to through a “separate” variable (i.e. one that potentially could be on a different processor), it is an error to try to use a “separate” object directly as if it were local. I do not see a way, in either Swift’s type system or ObjC’s, to maintain that property. It looks like this proposal, were it to cover generic or associated types, would address that deficiency.

  2. SCOOP turns Eiffel’s correctness preconditions into wait conditions. A serial program will fail if it tries to send a message without satisfying preconditions. When the message is sent to a “separate” object, this instead turns into a requirement to wait for the precondition to be true before execution.

Conclusions

Meyer is right: concurrent programming is difficult, because we are bad at considering all of the different combinations of states that a concurrent system can be in. A concurrent design can best be understood if it is constrained to be mostly like a serial one, and not require lots of scary non-local comprehension to understand the program’s behaviour. SCOOP is a really nice tool for realising such designs.

This is something I can help your team with! As you can see, I’ve spent actual years understanding and thinking about software concurrency, and while I’m not arrogant enough to claim I have solved it I can certainly provide a fresh perspective to your team’s architects and developers. Book an office hours appointment and let’s take a (free!) hour to look at your concurrency problems.

Microservices for the Desktop

In OOP the Easy Way, I make the argument that microservices are a rare instance of OOP done well:

Microservice adopters are able to implement different services in different technologies, to think about changes to a given service only in terms of how they satisfy the message contract, and to independently replace individual services without disrupting the whole system. This […] sounds a lot like OOP.

Microservices are an idea from service-oriented architecture (SOA) in which each application—each microservice—represents a distinct bounded context in the problem domain. If you’re a movie theatre complex, then selling tickets to people is a very different thing from showing movies in theatres, that are coupled loosely at the point that a ticket represents the right to a given seat in a given theatre at a given showing. So you might have a microservice that can tell people what showings there are at what times and where, and another microservice that can sell people tickets.

People who want to write scalable systems like microservices, because they can scale different parts of their application separately. Maybe each franchisee in the theatre chain needs one instance of one service, but another should scale as demand grows, sharing a central resource pool.

Never mind all of that. The real benefit of microservices is that they make boundary-crossing more obvious, maybe even more costly, and as a result developers think about where the boundaries should be. The “problem” with monolithic (single-process) applications was never, really, that the deployment cost too much: one corollary of scale is that you have more customers. It was that there was no real enforcement of separate parts of the problem domain. If you’ve got a thing over here that needs that data over there, it’s easy to just change its visibility modifier and grab it. Now this thing and that thing are coupled, whoops!

When this thing and that thing are in separate services, you’re going to have to expose a new endpoint to get that data out. That’s going to make it part of that thing’s public commitment: a slightly stronger signal that you’re going down a complex path.

It’s possible to take the microservices idea and use it in other contexts than “the backend”. In one Cocoa app I’m working on, I’ve taken the model (the representation in objects of the problem I’m solving) and put it into an XPC Plugin. XPC is a lot like old-style Distributed Objects or even CORBA or DCOM, with the exception that there are more safety checks, and everything is asynchronous. In my case, the model is in Objective-C in the plugin, and the application is in Swift in the host process.

“Everything is asynchronous” is a great reminder that the application and the model are communicating in an arm’s-reach fashion. My model is a program that represents the domain problem, as mentioned before. All it can do is react to events in the problem domain and represent the changes in that domain. My application is a reification of the Cocoa framework to expose a user interface. All it can do is draw stuff to the screen, and react to events in the user interface. The app and the model have to collaborate, because the stuff that gets drawn should be related to the problem, and the UI events should be related to desired changes in the domain. But they are restricted to collaborating over the published interface of the XPC service: a single protocol.

XPC was designed for factoring applications, separating the security contexts of different components and giving the host application the chance to stay alive when parts of the system fail. Those are valid and valuable benefits: the XPC service hosting the model only needs to do computation and allocate memory. Drawing (i.e. messaging the window server) is done elsewhere. So is saving and loading. And that helps enforce the contract, because if I ever find myself wanting to put drawing in the model I’m going to cross a service boundary, and I’m going to need to think long and hard about whether that is correct.

If you want to talk more about microservices, XPC services, and how they’re different or the same, and how I can help your team get the most out of them, you’re in luck! I’ve recently launched the Labrary—the intersection of the library and the laboratory—for exactly that purpose.

Beginner thoughts

Back story: my period of walkabout, in which I went to see the rest of the computing world beyond Apple land, started in November 2014. This was shortly after Swift’s introduction at WWDC 2014. It ended in October 2018, by which time the language had evolved considerably, its position in the community had advanced greatly, and SourceKitService had stopped crashing.

I have previously written on the learning phases I encountered on exposure to Haskell, now what about Swift? I have the opportunity to reflect on how I react as a beginner, and share that so that we all learn how we (well, I) learn, and maybe discover how we can teach.

About the project

I’m writing a tool that I want, which takes files in one format (RSS) and writes them out in another format (Maildir). You can follow along. The reason for mentioning this here are twofold:

  • I do not know what I’m doing, but I’m willing to share that.
  • To let you understand the (limited, I think) complexity of the thing I’m trying to build.

Thinks: This should not be that hard

I often feel like Swift is making me feel like an idiot. This is because my expectation is too high: I know the platform fairly well. I know the Foundation framework pretty well. I know Xcode pretty well. I understand my problem to some extent. It should just be the programming language that’s different.

And I do different programming languages all the time. What’s another one going to do?

But of course it’s not just the programming language that changed. It changed the conventions for things like naming methods or raising errors, and that means that the framework methods have changed, which means that things I used to know have changed, which means that I do not know as much as I assume. It introduced a new library, which I also don’t know.

Thinks: That unimportant thing was really frustrating

Two such convention changes are correlated: classes that used to be Foundation and are now standard library (or maybe are Foundation still but have been renamed on being bridged, I’m not sure) are renamed from NSThing to Thing. That means that the name of NSURL is now URL.

That means that if you have a variable that represents a URL, you can’t follow the Cocoa convention of leaving the abbreviation uppercased and calling it URL, because now it’s got the same name as the type URL. So the new convention is to call it url.

Objectively, that’s not a big deal. Subjectively, this stuff is baked in pretty deep, and changing it is hard.

Thinks: Even learning something is frustrating

The last event to make me get up and walk around a field was actually discovering something new about Swift, which should be the point, but nonetheless made me feel bad.

I have discovered that when it comes to working with optionals, the language syntax means that There Is More Than One Way To Do It. When I learned about if let and guard let, I was confused by the fact that the thing on the right needed to be an optional, not unwrap one: surely if my rvalue is an optional, then my lvalue should be, too?

Then, when I learned about the ?. and subsequently ?? operators, I thought “there’s no way I would ever have thought to type that, or known how to search for those things”. And even though they only make things shorter, not different, I still felt frustration at the fact that I’d gone through typing things out the long way.

Thinks: There’s More Than One Way Not To Do It

One of the Broken Expectations™ is that I know how to use Strings. Way back when, NeXT apps used char * as their string type. Then Enterprise Objects Framework came along with its Foundation library of data types, including a new-fangled Unicode string class, NSString. Then, well, that was it for absolute ages.

So, when I had a String and I wanted to take the substring to an index, I was familiar with -substringToIndex: and tried to apply that. That method is deprecated, so I didn’t want to use it. OK, well I can string[0..<N]. Apparently not, integer subscripting is not allowed, and the error message tells me to read a code comment to understand why. I wish it told me where that code comment was, or just showed it to me, instead!

Eventually I found that there’s a .prefix(N) method, again this is the sort of thing that makes me think: what’s wrong with me? I’ve been programming for years, I’ve been programming on this platform for years, I should be able to get this.

Conclusion: Read a Book

I had expected that my knowledge of the Mac, Xcode, and Cocoa would be sufficient to carry me through a four-year gap on picking up a language, particularly with the occasional observation of a conference talk about the Swift language (I’ve even given one!). I had expected that picking up a project to build a thing would give me a chance to get acquainted.

I was reflecting on my early experiences with writing NeXT and Mac applications in Objective-C. I had my copy of the NeXT Developer Documentation, or Cocoa in a Nutshell, open on the desk, looking at the methods available and thinking “I have this, I want that, can I find one of these that gets me there?” I had expected that auto-complete in Xcode would be my modern equivalent of working that way.

Evidently not. Picking up the new standard library things, and the new operators, will require deliberate learning. I’ve got some videos lined up, but I think my next action is to find a good book to read.

On null

I’ve had an interesting conversation on the topic of null over the last few days, spurred by the logical disaster of null. I disagreed with the statement in the post that:

Logically-speaking, there is no such thing as Null

This is true in some logics, but not in all logics. Boolean logic as described in An Investigation of the Laws of Thought, admits only two values:

instead of determining the measure of formal agreement of the symbols of Logic with those of Number generally, it is more immediately suggested to us to compare them with symbols of quantity admitting only of the values 0 and 1.

…and in a later chapter he goes on to introduce probabilities. Anyway. A statement either does hold (it has value 1, with some probability p) or it does not (it has value 0, with some probability 1-p; the Law of the Excluded Middle means that there is no probability that the statement has any other value).

Let’s look at an example. Let x be the lemma “All people are mortal”, and y be the conclusion “Socrates is mortal”. What is the value of y? It isn’t true, because we only know that people are mortal and do not know that Socrates is a person. On the other hand, we don’t know that Socrates is not a person, so it isn’t false either. We need another value, that means “this cannot be decided given the current state of our knowledge”.

In SQL, we find a logic that encodes true, false, and “unknown/undecided” as three different outcomes of a predicate, with the third state being given the name null. If we had a table linking Identity to Class and a table listing the Mortality of different Classes of things, then we could join those two tables on their Class and ask “what is the Mortality of the Class of which Socrates is a member”, and find the answer null.

But there’s a different mathematics behind relational databases, the Relational Calculus, of which SQL is an imperfect imitation. In the relational calculus predicates can only be true or false, there is no “undecided” state. Now that doesn’t mean that the answer to the above question is either true or false, it means that that question cannot be asked. We must ask a different question.

“What is the set of all Mortality values m in the set of tuples (m, c) where c is any of the values of Class that appear in the set of tuples (x, c) where x is Socrates?”

Whew! It’s long-winded, but we can ask it, and the answer has a value: the empty set. By extension, we could always change any question we don’t yet know the answer to into a question of the form “what is the set of known answers to this question”. If we know that the set has a maximum cardinality of 1, then we have reinvented the Optional/Maybe type: it either contains a value or it does not. You get its possible value to do something by sending it a foreach message.

And so we ask whether we would rather model our problem using a binary logic, where we have to consider each question asked in the problem to decide whether it needs to be rewritten as a set membership test, or a ternary logic, where we have to consider that the answer to any question may be the “I don’t know” value.

Implementationally-speaking, there are too many damn nulls

We’ve chosen a design, and now we get to implement it. In an implementation language like Java, Objective-C or Ruby, a null value is supplied as a bottom type, which is to say that there is a magic null or nil keyword whose value acts as a subtype of all other types in the system. Good: we get “I don’t know” behaviour for free anywhere we might want it. Bad: we get that behaviour anywhere else too, so we need to think to be sure that in all places where “I don’t know” is not an answer, that invariant holds in our implementation, or for those of us who don’t like thinking we have to pepper our programs with defensive checks.

I picked those three languages as examples, by the way, because their implementations of null are totally different so ruin the “you should never use a language with null because X” trope.

  • Java nulls are terminal: if you see a null, you blew it.
  • Objective-C nulls are viral: if you see a null, you say a null.[*]
  • Ruby nulls are whatever you want: monkey-patch NilClass until it does your thing.

[*] Objective-C really secretly has _objc_setNilReceiver(id), but I didn’t tell you that.

Languages like Haskell don’t have an empty bottom, so anywhere you might want a null you are going to need to build a thing that represents a null. On the other hand, anywhere you do not want a null you are not going to get one, because you didn’t tell your program how to build one.

Either approach will work. There may be others.

In defense of `id`

Something you can’t see about my dotSwift talk on OOP in FP in Swift is that to make the conference more interesting while the AV was set up for the next speaker, Daniel Steinberg invited me over to a side table for a question and answer session. He had some great questions and I had some adequate answers; that is now lost to time.

One thing he asked was about how I do things differently when I’m working in Objective-C and in Swift, and I mentioned that I tend not to use the type system in ObjC and just call all of my variables id unless they are C types or the compiler asks otherwise. You can see an example of this in my UIKonf 1995 talk.

I argue (back in 2018, at dotSwift, in the bit that was videoed) that all objects have the same type. Just as I can define the “type” Set through its function signature:

typealias Set<T> = (T) -> Bool

so I can define the “type” object through its function signature. An object – any object – is a function that responds to messages by turning selectors into methods:

typealias Object = (Selector) -> IMP

Now if all objects have the same type, why would I want to use different types for different objects?

Of course, there are reasons to want to refine the definition of an object from “any object” to “an object like this”, but these refinements are inaccessible using Objective-C’s type system (or Java’s, or Swift’s, or most other programming languages). Any object responds to any message, but for the most part they respond by doing whatever the default error-raising behaviour is which is not particularly interesting and doesn’t solve your customer’s problem. So what we want to be able to say is “this object is one that responds to a particular collection of messages in a way that is what I need here”.

We have two tools available to us, and neither gives us an answer to that question. The first is the protocol (or Java interface): we can say “this object is one that has implementations of methods for a particular collection of messages”. That’s not the same as the question we want to answer – it says nothing about whether the object responds in the ways we want. It’s also not generally the correct answer – an object that has the methods we want and behaves in the expected way but that didn’t have the protocol conformance recorded at compile time (even if it conformsToProtocol: at run time) does not satisfy the compiler type check for protocol conformance.

Even less generally useful is using the class name as the type. Classes are ways to say “here is a collection of objects that all have common behaviour and data”; well for starters I don’t care about the data, but also just because those objects have the properties I want, doesn’t mean that others outside that place in the inheritance tree don’t.

I also can’t rely on subtypes behaving as I need, but the compiler type checker will pretend that if I asked for an instance of one class, and got an instance of a subclass, then I got the thing I wanted. Inheritance, in many languages including Objective-C, Java and similar, is a way to borrow behaviour from one class in another class. If we also want refinement from inheritance we have to add a bunch of rules that almost every programming language not named after the designer of the Garabit Viaduct does not support.

So there are three imprecise ways to ask for objects by behaviour in Objective-C, and I choose the one with the least typing. Even once you amortise the cost of this blog post.

machoo – Object-Oriented Programming in Object-Oriented Programming in the GNU HURD

For the last few weeks, my when-I-get-to-it project has been machoo, which is sort of an object-oriented system on the HURD but mostly an excuse to learn about how Mach messaging works.

I decided to build a Smalltalk-style “give me any old selector, and I’ll do something with it” messaging system on Mach, which is already a messaging system. I don’t yet know whether this is a good idea, but I like the design principle of highly loosely-coupled objects telling each other the names of the things to do.

The substrate for this system is MIG, the Mach Interface Generator. It takes descriptions of routines in an IDL that include the names of the supported routines and the argument names, types and return types. It’d certainly be possible to build each message I want to be able to send as a MIG routine, but then the client would have to import interfaces for every type it wanted to use: no bad thing, and many OO systems work that way, but not what I want.

MIG generates a ‘demuxer’ function that examines a message sent to a port, and dispatches it to a handler to the named routine. As an aside, it is highly likely that I’ve got one to many layers of indirection; that if I wrote my own function to take the place of the demuxer I could handle dispatch there without receiving arguments from a dispatcher to do more dispatch. OK, I could, but I don’t currently know how.

So what I ended up with is a two-level system, something like Smalltalk or ObjC. A “class” is a HURD translator registered on the filesystem, you send the zero-arguments message machoo_create_object to the class and get a send right to the new object. This is two-level in that if you need to initialise the object, you would then send an init message to the created object. Objects are messaged using the machoo_msg_send routine, which takes a string selector like Smalltalk.

There’s currently exactly one implementation of a class, a do-nothing null object class. It does the thing that I would extract out into a pattern for other classes: its response to a machoo_create_object message is to spawn a new thread to back the new object. So it’s like an Erlang-style system where each object has its own execution context, except that task switching is handled by the kernel rather than some virtual machine. All of the objects of a class are in a single task, and they are each on their own thread.

I still need to solve some problems, like how to return a reference to ‘this object’ from a message. I need to build more than one class of object, and extract out the common parts of class-ness so that a new class is only defined by its response to messages. So far this is proving to be an interesting and educational project.

Reasoning about reasoning about software

Functional programmers like to claim that you can’t reason about mutable state programs. Some thoughts:

  • the first half of the book A Discipline of Programming by Edsger W. Dijkstra tells you how to do it. That half of the book is approximately 100 pages (the remainder of the book is worked examples).
  • object-oriented programming breaks a software system up into separate systems running miniature, message-driven programs as if on separate computers. Therefore the consideration of “mutable state” can be split in two: the state internal to the object and the state external to the object which sends messages to the object but is ignorant of its internals. If you can’t split the state that way, you have bad encapsulation.
  • The reasoning done about the external and internal behaviours had better match at the interface. Design by contract probably helps here.
  • Given a state S, an operation O can be defined as \(O(args \times S) \rightarrow (R \times S’)\), i.e. it returns a result R and updates the state to S’.
  • However, Bertrand Meyer introduced Command-Query Separation in the 1980s, so you only need to know \(O(args \times S) \rightarrow (R \times S)\) and \(O(args \times S) \rightarrow (\emptyset \times S’)\).
  • Various history “traces” can be considered equivalent and therefore a lot of knowledge about the historical state transitions elided, simplifying the reasoning. For example, given a well-designed stack, it is impossible to distinguish the history of stack.push(3); stack.pop(); stack.push(7) from stack.push(7).
  • Various operations on the state are irrelevant to the behaviour of an operation under consideration. In reasoning about the final operation in a = 3; b = 7; c = 9; stack.push(2) you do not need to consider the assignment operations (and indeed their presence may indicate a cohesion problem in your design).
  • The one remaining source of difficulty is aliasing; I do need to know about the elided operations in the sequence x = 7; *y = &x; ...; z=f(x). This is aliasing, not mutable state.

Falsehoods programmers believe about programming

  • There is no ethical impact of my job; I build technological systems and it’s up to others how they use them.
  • Software is a purely technical discipline.
  • There is some innate affinity for computer programming which you must be born with, and cannot be taught.
  • Allowing people who are unlike me to program can only be achieved by “lowering the bar”.
  • Compiled languages are always faster.
  • floating point calculations will introduce non-deterministic errors into numerical results.
  • OK, they introduce some errors into numerical results.
  • alright I understand that floating point calculations are imprecise not inaccurate, Mister Pedantic Blog Author, but I cannot know what that imprecision is.
  • at least the outcome of integer maths is always defined.
  • fine, it’s not defined. But whatever it was, the result of doing arithmetic on two numbers that each fit in a data register itself fits in a data register.
  • every computer on sale today (2017) uses two’s complement notation for negative numbers.
  • every computer on sale today uses a register width that’s a multiple of eight bits.
  • the bug isn’t in my code.
  • the bug isn’t in the library.
  • the bug isn’t in the operating system.
  • the bug isn’t in the compiler.
  • the bug isn’t in the kernel.
  • the bug isn’t in the hardware.
  • bug-free computer hardware is completely deterministic.
  • the lines on the hardware bus/bridge are always either at the voltage that represents 0 or the voltage that represents 1.
  • if my tests cover 100% of my lines of code then I have complete coverage.
  • if my tests cover 100% of my statements then I have complete coverage.
  • if my tests cover 100% of my conditions then I have complete coverage.
  • if I have complete test coverage then I have no bugs.
  • if I have complete test coverage then I do not need a type system.
  • if I have a type system then I do not need complete test coverage.
  • no hacker will target my system.
  • information security is about protecting systems from hackers.
  • if the problem is SQL Injection, then the solution is to replace SQL; NoSQL Injection is impossible.
  • my project is a special snowflake; I can reject that technique I read about without considering it.
  • my project is much like that unicorn startup or public company’s project; I can adopt that technique I read about without considering it.
  • people who do not use my language, technique, framework, tool, methodology, paradigm or other practice do not get it.
  • any metaprogramming expansion will resolve in reasonable time.
  • any type annotation will resolve in reasonable time.
  • OK, well at least any regular expression will resolve in reasonable time.
  • can you at least, please, allow that regular expressions are regular?

I’m sure there must be more.

Update The following were added later; where they were supplied by others there is citing link. There are also good examples in the comments.

  • You need a computer science degree to be a good programmer.
  • A computer science course contains nothing useful to programmers.
  • Functional Programming is a silver bullet.
  • Rust is a silver bullet.
  • There is a silver bullet.
  • There can be no silver bullet.
  • Rewriting a working software system is a good idea.
  • I can write a large system in a memory unsafe language without introducing vulnerabilities.
  • I can write a large system in a memory safe language without introducing vulnerabilities.
  • Software is an engineering discipline.
  • Software is a scientific discipline.
  • Discourse on a topic is furthered by commenting on how I already knew a fact that was stated.
  • A falsehood about programming has no value unless the author of the falsehood provides supporting evidence to my satisfaction.

Two ways of thinking

I’ve used this idea in conversations for years, and can’t find a post on it, which I find surprising but there you go. There are, broadly speaking, two different ways to look at programming languages. And I think that these mean two different ways to select programming languages, which are asymmetric. However, they can lead to the same choice, but viewed in different ways: if you use one of those languages then you need to understand that these different views exist to understand decisions made by programmers working in those languages.

On the one hand, the Language Lawyer seeks out languages with plenty of esoterica and specifics, in order to become proficient at recalling those esoterica and demonstrating their correct application. The Language Lawyer loves that reference and value types in swift are actually called class and struct, or that the difference between class and struct in C++ is the default member visibility, and that neither is actually related to the words class or struct. They are delighted both to know and to explain that in Perl 5, @var, \var and $var refer to the same variable but are accessed in different contexts and what they evaluate to in those contexts. They are excited to explain that technically the admonition that you must always return a value from a non-void function in C is not true, because since C99 if you have a function called main that returns control without returning a value, the compiler will implicitly return 0.

The Language Lawyer seeks out a language with such esoterica. But learning it is complex and time-consuming, so they probably don’t change particularly often. In fact, changing language may be deleterious, because then other people will know the esoterica that they get caught out on. Imagine knowing how the function overloading rules in C++ work, then trying something similar in Java and finding that you’re wrong. And that somebody else knows that. The shame! Once their language is chosen, the Language Lawyer will form heuristic rules of which language feature to use when solving which problem.

Standing in the other corner, the Lazy Evaluator wants to avoid those edge cases, because it makes them think about the programming language, not the software problem. They’d much prefer to have an environment in which there’s as much as one way to express their solution, then worry about how to express their solution using that one tool. The Lazy Evaluator loves Ruby because Everything Is An Object (and they love Io more because in Ruby there are also classes). The Lazy Evaluator is delighted to know that in Lisp, Everything Is A List. They are excited to be programming in Haskell, where Everything Is A Function.

Both of these people can be happy with the same programming language, though clearly this will be for different reasons, and their different thought processes will bring them into conflict. The Lazy Evaluator will be happy enough using the third-most common C-family language, C++– (full name: “C++98 but never use exceptions or templates, avoid multiple inheritance, and [never/always] mark methods as virtual”). The Language Lawyer will enjoy demonstrating const-correctness and the difference between l-, r-, pr-, gl- and x-values, but the two will come to blows over whether the Lazy Evaluator is denying the full elegant expressiveness of the language by proscribing exceptions.

Similarly, the Lazy Evaluator can look at JavaScript, see the simple classless object model with dynamic dispatch remembered from Self or Io, even notice that Functions Are Objects Too and work in that Everything Is An Object paradigm. The Language Lawyer can be confident that at some point, all of the weird coercion behaviour, the Which this Is That this Anyway question, and the You Need That Flag With This Polyfill To Get That Language Feature issues will come up. But the Lazy Evaluator will notice that That Flag also enables class syntax and arrow functions, we already have prototypes and function functions, and disagreement will ensue.

So in what way are those ways of thinking asymmetric? Invoking Hickey’s Corollary, you can never be recomplecting. It’s easier to compromise on C++ But Without Exceptions than it is to compromise on Lisp But We’ll Add Other Atom Types. You can choose JavaScript But Never Use this, you can’t choose Haskell But With Type-Escaping Side Effects.

This isn’t really about programming languages, it’s about any form of model or abstraction. Languages are a great example though. I think it’s a mindset thing. Your task is to solve this problem, using this tool. Are you focusing on the bit that’s about the problem, or the bit that’s about the tool? Both are needed.