This month, Swift has dropped to #19 almost looking like it's going to fall out of the top 20 altogether.
Strange times.
This month, Swift has dropped to #19 almost looking like it's going to fall out of the top 20 altogether.
Strange times.
The example is a set of commands for moving a robot:
-moveNorth.
-moveSouth.
-moveWest.
-moveEast.
"It’s awkward to work with this kind of interface because you can’t pass around, store or perform calculations on the direction at all."
move: message
with a separate direction argument. The current fashion would be to make direction an enum, but he (wisely, IMHO) turns it into a class that can encode different directions:
-move:direction.
class Direction {
...
}
...we have this message obsessions at a massively larger scale with accessors.
-attribute.
-setAttribute:newValue.
-get:identifier.
-set:identifier to:value.
And it turns out that passing those identifiers around performing calculations on them is tremendously powerful, even if you don't have language support. Without language support, the interface between the world of reified identifiers and objects can be a bit awkward.
Imagine my surprise when I looked earlier this March and found it back up, no, not in the lofty heights it used to occupy, but at least in tenth place (up from 14th a year earlier), and actually surpassing Swift again, which dropped by almost half in its percent rating and from 12th to 17th place in the rankings.
What's going on here?
The question, which has been bugging me for some time, is when do we actually need metaprogramming facilities like macros, and why? After all, we already have functions and methods for capturing and extracting common functionality. A facile answer is that "Macros extend the language", but so do functions, in their way. Another answer is that you have to use Macros when you can't make progress any other way, but that doesn't really answer the question either.
The reason the question is relevant is, of course, that although it is fun to play around with powerful mechanisms, we should always use the least powerful mechanism that will accomplish our goal, as it will be easier to program with, easier to understand, easier to analyse and build tools for, and easier to maintain.
Anyway, the answer in this case seemed to be that macros were needed in order to "delay evaluation", to send unevaluated parameters to the macros. A quick question to the presenter confirmed that this was the case for most of the examples. Which begs the question: if we had a generic mechanism for delaying evluation, could we have used plain functions (or methods) instead, and indeed the answer was that this was the case.
One of the examples was a way to build your own if, which most languages have built in, but
Smalltalk famously implements in the class library: there is an ifTrue:ifFalse: message
that takes two blocks (closures) as parameters. The True class evaluates the first block
parameter and ignores the second, the False class evaluates the second block parameter and
ignores the first.
The Clojure macro example worked almost exactly the same way, but where Smalltalk uses blocks to delay evaluation, the example used macros. So where LISP might use macros, Smalltalk uses blocks. That macros and blocks might be related was new to me, and took me a while to process. Once I had processed it, a bit of Smalltalk history that I had always struggled with, this bit about Smalltalk-76, suddenly made sense:
Why did it "have to" provide such a mechanism? It doesn't say. It says this mechanism
was replaced by the equivalent blocks, but blocks/anonymous functions seem quite different from alternate argument-passing mechanisms. Huh?
With this new insight, it suddenly makes sense. Smalltalk-72 just had a token-stream, there were no "arguments" as such, the new method just took over parsing the token stream and picked up the paramters from there. In a sense, the ultimate macro system and ultimately powerful, but also quite unusable, incomprehensible, unmaintainable and not compilable. In that system, "arguments" are per-definition unevaluated and so you can do all the macro-like magic you want.
Dan's Smalltalk-76 effort was largely about compiling for better performance and having a stable,
comprehensible and composable syntax. But there are times you still need unevaluated arguments,
for example if you want to implement an if that only evaluates one of its branches,
not both of them, without baking it into the language. Smalltalk did not have a macro mechanism,
and it no longer had the Smalltalk-72 token-stream where un-evaluated "arguments" came for free,
so yes, there "had" to be some sort of mechanism for unevaluated arguments.
Hence the open-colon syntax.
And we have a progression of: Smalltalk-72 token stream → Smalltalk-76 open colon parameters → Smalltalk-80 blocks.
All serving the purpose of enabling macro-like capabilities without actually having macros by providing a general language facility for passing un-evaluated parameters.
Aha!
To reach this not-quite-there-yet state took almost 5 years, which is pretty much the total time NeXT shipped their hardware, and it mirrors the state with C++, which is still not generally suitable for binary distribution of libraries. Objective-C didn't have these problems, and as it turns out this is not a coincidence.
As the name suggests the intention was to bring the benefits the hardware world had reaped from the introduction of the Integrated Circuits to the software world.
It is probably hard to overstate the importance of ICs to the development of the computer industry. Instead of assembling computers from discrete components, you could now put entire subsystem onto one component, and then compose these subsystems to form systems. The interfaces are standardised pins, and the relationship between the outside interface and the complexity hidden inside can be staggering. Although the socket of the CPU I am writing is a beast, with 1151 pins, the chip inside has a staggering 2.1 billion transistors. With a ratio of one million to one, that's a very deep interface, even if you disregard the fact that the bulk of those pins are actually voltage supply and ground pins.
The important point is that you do not have to, and in fact cannot, look inside the IC. You get the pins, very much a binary interface, and the documentation, a specification sheet. With Software-ICs, the idea was the same: you get a binary, the interface and a specification sheet. Here are two BYTE articles that describe the concepts:
A lot of what they write seems quaint now, for example a MailFolder that inherits from Array(!),
but the concepts are very relevant, particularly with a couple of decades worth of perspective and the new circumstances
we find ourselves in.
Although the authors pretty much equate Software-ICs with objects and object-oriented programming, it is a slightly different form of object-oriented programming than the one we mostly use today. They do write about object/message programming, similar to Alan Kay's note that 'The big idea is "messaging"'.
With messaging as the interconnect, similar to Unix pipes, our interfaces are sufficiently well-defined and dynamic that we really can deliver our Software-ICs in binary form and be compatible, something our more static languages like C++ and Swift struggle with.
ObjC is pretty awesome in how it manages to embed a “COM”. Swift doesn’t (currently) provide anything like it (and IMO it lost a chance not just using the ObjC runtime for that)
— Helge Heß (@helje5) January 6, 2019
Objective-C is middleware with language features.
In fact, the problem of binary compatibility of C++ objects was one of the reasons for creating COM:
Unlike C++, COM provides a stable application binary interface (ABI) that does not change between compiler releases.COM has been incredibly successful, it enables(-ed?) much of the Windows and Office ecosystems. In fact, there is even a COM implementation on macOS:
CFPlugin, part of CoreFoundation.
CFPlugIn provides a standard architecture for application extensions. With CFPlugIn, you can design your application as a host framework that uses a set of executable code modules called plug-ins to provide certain well-defined areas of functionality. This approach allows third-party developers to add features to your application without requiring access to your source code. You can also bundle together plug-ins for multiple platforms and let CFPlugIn transparently load the appropriate plug-in at runtime. You can use CFPlugIn to add plug-in capability to, or write a plug-in for, your application.That COM implementation is still in use, for example for writing Spotlight importers. However, there are, er, issues:
Creating a new Spotlight importer is tricky because they are based on CFPlugIn, and CFPlugIn is… well, how to say this diplomatically?… super ugly )-: One option here is to use Xcode 9 to create your plug-in based on the old template. Honestly though, I don’t recommend that because the old template… again, diplomatically… well, let’s just say that the old template lets the true nature of CFPlugIn shine through! (-:Having written both Spotlight importers and even some COM component on Windows (I think it was just for testing), I can confirm that COM's success is not due to the elegance or ease-of-use of the implementation, but due to the fact that having an interoperable, stable binary interface is incredibly enabling for a platform.
That said, all this talk of COM is a bit confusing, because we already have NSBundle.
Apple uses bundles to represent apps, frameworks, plug-ins, and many other specific types of content.So
NSBundle already does everything a CFPlugin does and a lot more, but is really just a tiny
wrapper around a directory that may contain a dynamic shared library. All the interfacing, introspection and binary
compatibility features come automagically with Objective-C. In fact, NeXT had a Windows product called d'OLE that pretty
automagically turned Objective-C libraries into COM-comptible OLE servers (.NET has similar capabilities). Again, this is not a coincidence, the
Software-IC concept that Objective-C is based on is predicated on exactly this sort of interoperation scenario.Objective-C is middleware with language features.
NSBundle, so
they aren't limited to being linked into an application, they can also be loaded dynamically.
I use that capability in Objective-Smalltalk, particularly together with
the stsh the Smalltalk Scripting Shell. By loading
frameworks, this shell can easily be transformed into an application-specific scripting language. An
example of this is pdfsh, a shell for examining an manipulating PDF files using EGOS, the
Extensible Graphical Object System.
#!/usr/local/bin/stsh
#-<void>pdfsh:<ref>file
framework:EGOS_Cocoa load.
pdf := MPWPDFDocument alloc initWithData: file value.
shell runInteractiveLoop
Framework-oriented programming is awesome, alas it was very much deprecated by Apple for quite some time, in fact even impossible on iOS until dynamic libraries were allowed. Even now, though, the idea is that you create an app, which consists of all the source-code needed to create it (exception: Apple code!), even if some of that code may be organised into framework units that otherwise don't have much meaning to the build.
Apps, however are not Software-ICs, they aren't the right packaging technology for reuse (AppleScript notwithstanding). And so iOS and macOS development shops routinely get themselves into big messes, also known as the Big Ball of Mud architectural pattern.
Of course, there are reasons that things didn't quite work out the way we would have liked. Certainly Apple's initial Mac OS X System Architecture book showed a much more flexible arrangement, with groups of applications able to share a set of frameworks, for example. However, DLL hell is a thing, and so we got a much more restricted approach where every app is a little fortress and frameworks in general and binary frameworks in particular are really something for Apple to provide and for the rest to use. However, the fact that we didn't manage to get this right doesn't mean that the need went away.
Swift has been making this worse, by strongly "suggesting" that everything be compiled together and leading to such wonderful oxymorons as "whole module optimisation in debug mode", meaning without optimisation. That and not having a binary modularity story for going on half a decade. The reason for compiling whole modules together is that the modularity mechanism is, practically speaking, very much source-code based, with generics and specialisation etc. (Ironically, Swift also does some pretty crazy things to enable separate compilation, but that hasn't really panned out so far).
On the other hand, Swift's compiler is so slow that teams are rediscovering forms of framework-oriented programming as a self-defense mechanism. In order to get feedback cycles down from ludicrously bad to just plain awful, they split up their projects into independent frameworks that they then compile and run independently during development. So in a somewhat roundabout way, Swift is encouraging good development practices.
I find it somewhat interesting that the industry is rediscovering variants of the Software-IC, in this case on the backend in the form of Microservices. Why do I say that Microservices are a form of Software-IC? Well, they are a binary unit of deployability, fairly loosely coupled and dynamically typed. In fact, Fred George, one of the people who came up with the idea refers to them as Smalltalk objects:
Of course, there are issues with this approach, one being that reliable method calls are replaced with unreliable network calls. Stepping back for a second should make it clear that the purported benefits of Microservices also largely apply to Software-ICs. At least real Software-ICs. Objective-C made the mistake of equating Software-ICs with objects, and while the concepts are similar with quite a bit of overlap, they are not quite the same. You certainly can use Objective-C to build and connect Software-ICs if you want to do that. It will also help you in this endeavour, but of course you have to know that this is something you want. It doesn't do this automatically and over time the usage of Objective-C has shifted to just a regular old object-oriented language, something it is OK but not that brilliant at.
Microservices are pretty good at this, Unix filters probably the most extreme example and just about every language and every kind of application on Windows can talk to and via COM. NeXT only ever sold 50000 computers, but in a short number of years the NeXT community had bridges to just about every language imaginable. There were a number of Objective- languages, including Objective-Fortran. Apple alone has around 140K employees (though probably a large number of those in retail), and there are over 2.8 million iOS developers, yet the only language integration for Swift I know of is the Python support, and that took significant effort, compiler changes and the original Swift creator, Chris Lattner.
This is not a coincidence. Swift is designed as a programming language, not as middleware with language features. Therefore its modularity features are an add-on to the language, and try to transport the full richness of that programming model. And Swift's programming model is very rich.
SOM allows classes of objects to be defined in one programming language and used in another, and it allows libraries of such classes to be updated without requiring client code to be recompiled.So you define interfaces separately from their implementations. I am guessing this is part of the reason we have
@interface in Objective-C. Having to write things down twice can be a pain (and I've worked on projects
that auto-generated Objective-C headers from implementation files), but having a concrete
manifestation of the interface that precedes the implementation is also very valuable. (One of the reasons TDD is
so useful is that it also forces you to think about the interface to your object before you implement it).In Swift, a class is a single implementation-focused entity, with its interface at best a second-class and second-order effect. This makes writing components more convenient (no need to auto-generate headers...), but connecting components is more complicated.
Which brings us back to that other complication, the lack of stable binary compatibility for libraries and frameworks. One consequence of this is to write frameworks exclusively in Objective-C, which was particularly necessary before ABI stability had been reached. The other workaround, if you have Swift code, is to have an Objective-C wrapper as an interface to your framework. The fact that Swift interoperates transparently with the Objective-C runtime makes this fairly straightforward.
Did I mention that Objective-C is middleware with language features?
So maybe this supposed "workaround" is actually the solution? Have Objective-C or Objective- as our message-oriented middleware, the way it was always intended? Maybe with a bit of a tweak so that it loses most of the C legacy and gains support for pipes and filters, REST/Microservices and other architectural patterns?
Just sayin'.
So instead I have chosen to just integrate the code into that Objective-C code base. So load the code:
NSData *classdefSchemeCode=[self frameworkResource:@"classdef-method-browser-scheme" category:@"stsh"];
[interpreter evaluateScriptString:[classdefSchemeCode stringValue]];
NSClassFromString()
-(void)awakeFromNib
{
...
self.methodStore = [NSClassFromString(@"ClassBrowser") store];
}
But wait! This is a scheme-handler, which is a store, meaning it doesn't really have any unique interface of its
own, but rather just implements the MPWStorage protocol. So all I have to do is the following:
@property (nonatomic, strong) id methodStore;
This is unexpected in two ways: first, the integration pain I was expecting just didn't appear. Happy. Second, and maybe more importantly, the benefit of uniform interfaces, which I thought should appear, actually did appear!
So very happy.
It seems I have now reached that point with Objective-Smalltalk. Now it was pretty much always the case that I preferred the Objective-Smalltalk code to equivalent Objective-C code, but since it was mostly scripts and other "final" code, the comparison never really came up. With a somewhat workable if incomplete class and scheme (and filter) definition syntax in place, and therefore Objective-Smalltalk now at least theoretically capable of delivering reusable code, that is no longer the case.
Specifically, I have a little Smalltalk-inspired method browser that I use for ObjST-based live coding environments
(AppLive for Apps and SiteBuilder for websites).
Yes: "Programmer UI". I'll clean that up later. What I am currently cleaning up is the interface between the browser and the "method store", which is horrendous. It is also tied to a specific, property-list based implementation of the method store.
The idea there is to allow external editing of the classes/methods. A browser for hierarchically nested structures...could this be a job for scheme handlers? Why yes, glad you asked!
#!/usr/local/bin/stsh
#-methodbrowser:classdef
scheme ClassBrowser {
var dictionary.
-initWithDictionary:aDict {
self setDictionary:aDict.
self.
}
-classDefs {
self dictionary at:'methodDict'.
}
/. {
|= {
self classDefs allKeys.
}
}
/:className/:which/:methodName {
|= {
self classDefs at:className | at:which | at:methodName.
}
=| {
self classDefs at:className | at:which | at:methodName put:newValue.
}
}
/:className/:which {
|= {
self classDefs at:className | at:which | allKeys.
}
}
}
scheme:browser := ClassBrowser alloc initWithDictionary: classdef value propertyList.
stdout do println: browser:. each.
shell runInteractiveLoop.
And I really don't want to.
I really, really don't want to. The idea of recasting this logic just strikes me as abhorrent, so much that the somewhat daunting prospect of significantly improving my Objective-Smalltalk native compilation facilities looks much more attractive.
That's the tipping point.
For reference, here's the original Objective-C code. What, you didn't believe me that it's horrendous? It also does a few
things that the Objective-Smalltalk code doesn't do yet, but those aren't that significant.
//
// MethodDict.h
// MPWTalk
//
// Created by Marcel Weiher on 10/16/11.
// Copyright (c) 2012 Marcel Weiher. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol MethodDict
-(NSArray*)instanceMethodsForClass:(NSString*)className;
-(NSArray*)classMethodsForClass:(NSString*)className;
-(NSString*)fullNameForMethodName:(NSString*)shortName ofClass:(NSString*)className;
-(NSString*)methodForClass:(NSString*)className methodName:(NSString*)methodName;
-(void)setClassMethod:(NSString*)methodBody name:(NSString*)methodName forClass:(NSString*)className;
-(void)setInstanceMethod:(NSString*)methodBody name:(NSString*)methodName forClass:(NSString*)className;
-(void)deleteInstanceMethodName:(NSString*)methodName forClass:(NSString*)className;
-(void)deleteClassMethodName:(NSString*)methodName forClass:(NSString*)className;
-(NSMutableDictionary*)addClassWithName:(NSString*)newClassName;
-(void)deleteClass:(NSString*)className;
-(NSString*)instanceMethodForClass:(NSString*)className methodName:(NSString*)methodName;
-(NSString*)classMethodForClass:(NSString*)className methodName:(NSString*)methodName;
@end
@interface MethodDict : NSObject
{
NSMutableDictionary *dict;
}
- (NSDictionary *)dict;
-initWithDict:(NSDictionary*)newDict;
-(NSArray*)classes;
@end
//
// MethodDict.m
// MPWTalk
//
// Created by Marcel Weiher on 10/16/11.
// Copyright (c) 2012 Marcel Weiher. All rights reserved.
//
#import "MethodDict.h"
#import <MPWFoundation/MPWFoundation.h>
#import <ObjectiveSmalltalk/MethodHeader.h>
@implementation NSString(methodName)
-methodName
{
MPWMethodHeader *header=[MPWMethodHeader methodHeaderWithString:self];
return [header methodName];
}
@end
@implementation MethodDict
objectAccessor(NSMutableDictionary, dict, setDict)
-initWithDict:(NSDictionary*)newDict
{
self = [super init];
[self setDict:[[newDict mutableCopy] autorelease]];
return self;
}
-(NSData*)asXml
{
NSData *data=[NSPropertyListSerialization dataFromPropertyList:[self dict] format:NSPropertyListXMLFormat_v1_0 errorDescription:nil];
return data;
}
-(NSArray*)classes
{
return [[[self dict] allKeys] sortedArrayUsingSelector:@selector(compare:)];
}
-(NSMutableDictionary*)classDictForName:(NSString*)className
{
return [[self dict] objectForKey:className];
}
-(void)deleteClass:(NSString*)className
{
return [[self dict] removeObjectForKey:className];
}
-(NSMutableDictionary*)methodDictForClass:(NSString*)className classMethods:(BOOL)isClassMethod
{
NSString *key=isClassMethod ? @"classMethods" : @"instanceMethods";
return [[[self dict] objectForKey:className] objectForKey:key];
}
-(NSArray*)methdodsForClass:(NSString*)className getClassMethods:(BOOL)classMethods
{
NSDictionary *methodDict=[self methodDictForClass:className classMethods:classMethods];
NSArray* methodKeys = [methodDict allKeys];
if ( [methodKeys count]) {
return [(NSArray*)[[methodKeys collect] methodName] sortedArrayUsingSelector:@selector(compare:)];
}
return [NSArray array];
}
-(NSArray*)instanceMethodsForClass:(NSString*)className
{
return [self methdodsForClass:className getClassMethods:NO];
}
-(NSArray*)classMethodsForClass:(NSString*)className
{
return [self methdodsForClass:className getClassMethods:YES];
}
-(NSString*)fullNameForMethodName:(NSString*)shortName ofClass:(NSString*)className
{
NSArray *fullNames = [[self methodDictForClass:className classMethods:NO] allKeys];
fullNames=[fullNames arrayByAddingObjectsFromArray:[[self methodDictForClass:className classMethods:YES] allKeys]];
for ( NSString *fullName in fullNames ) {
if ( [[fullName methodName] isEqual:shortName] ) {
return fullName;
}
}
return nil;
}
-(NSString*)instanceMethodForClass:(NSString*)className methodName:(NSString*)methodName
{
return [[self methodDictForClass:className classMethods:NO] objectForKey:[self fullNameForMethodName:methodName ofClass:className]];
}
-(NSString*)classMethodForClass:(NSString*)className methodName:(NSString*)methodName
{
return [[self methodDictForClass:className classMethods:YES] objectForKey:[self fullNameForMethodName:methodName ofClass:className]];
}
-(NSString*)methodForClass:(NSString*)className methodName:(NSString*)methodName
{
return [self instanceMethodForClass:className methodName:methodName];
}
-(void)setMethod:(NSString*)methodBody name:(NSString*)methodName forClass:(NSString*)className isClassMethod:(BOOL)isClassMethod
{
NSMutableDictionary *methodDict = [self methodDictForClass:className classMethods:isClassMethod];
if ( !methodDict ) {
[self addClassWithName:className];
methodDict = [self methodDictForClass:className classMethods:isClassMethod];
}
[methodDict setObject:methodBody forKey:methodName];
}
-(void)setClassMethod:(NSString*)methodBody name:(NSString*)methodName forClass:(NSString*)className
{
[self setMethod:methodBody name:methodName forClass:className isClassMethod:YES];
}
-(void)setInstanceMethod:(NSString*)methodBody name:(NSString*)methodName forClass:(NSString*)className
{
[self setMethod:methodBody name:methodName forClass:className isClassMethod:NO];
}
-(NSMutableDictionary*)addClassWithName:(NSString*)newClassName
{
NSMutableDictionary *classDict=[self classDictForName:newClassName];
if ( !classDict) {
classDict=[NSMutableDictionary dictionary];
classDict[@"instanceMethods"]=[NSMutableDictionary dictionary];
classDict[@"classMethods"]=[NSMutableDictionary dictionary];
dict[newClassName]=classDict;
}
return classDict;
}
-(void)deleteMethodName:(NSString*)methodName forClass:(NSString*)className isClassMethod:(BOOL)isClassMethod
{
NSMutableDictionary *methodDict = [self methodDictForClass:className classMethods:isClassMethod];
[methodDict removeObjectForKey:[self fullNameForMethodName:methodName ofClass:className]];
}
-(void)deleteInstanceMethodName:(NSString*)methodName forClass:(NSString*)className
{
[[self methodDictForClass:className classMethods:NO] removeObjectForKey:[self fullNameForMethodName:methodName ofClass:className]];
}
-(void)deleteClassMethodName:(NSString*)methodName forClass:(NSString*)className
{
[[self methodDictForClass:className classMethods:YES] removeObjectForKey:[self fullNameForMethodName:methodName ofClass:className]];
}
-(NSString*)description
{
return [NSString stringWithFormat:@"<%@:%p: %@>",[self class],self,dict];
}
@end