I’ve been making progress on the Objective-Smalltalk project. Besides the port to GNUstep changes that enabled me to run the official site component (and thankfully survive the influx of attention), I’ve focused on both advancing the core ideas and implementing the practical necessities of a programming language.
My programs are becoming more sophisticated and practical, and I’m starting to see the appeal of choosing Objective-Smalltalk over Objective-C for certain tasks. So, I want to share a larger example, illustrating its mechanics, strengths, quirks, and areas for improvement.
The code is a script that creates a generic scheme handler for sqlite databases. This lets you interact with a database using URIs. When run with the Chinook.db sample database, you can use the db scheme, like so: db:. lists tables:
` ```
db:. ( “albums”,“sqlite_sequence”,“artists”,“customers”,“employees”,“genres”,“invoices”, “invoice_items”,“media_types”,“playlists”,“playlist_track”,“tracks”,“sqlite_stat1”)
``` `
Accessing tables is then possible, e.g., db:albums shows all albums, or a specific one like this:
` ```
db:albums/3 { “AlbumId” = 4; “Title” = “Let There Be Rock”; “ArtistId” = 1; }
``` `
Without further ado, here’s the code :
` ``` #!/usr/local/bin/stsh #-sqlite:dbref
framework:FMDB load.
class ColumnInfo { var name. var type. -description { “Column: {var:self/name} type: {var:self/type}”. } }
class TableInfo { var name. var columns. -description { cd := self columns description. “Table {var:self/name} columns: {cd}”. } }
class SQLiteScheme : MPWScheme { var db.
-initWithPath: dbPath { self setDb:(FMDatabase databaseWithPath:dbPath). self db open. self. }
-dictionariesForResultSet:resultSet { results := NSMutableArray array. { resultSet next } whileTrue: { results addObject:resultSet resultDictionary. }. results. }
-dictionariesForQuery:query { self dictionariesForResultSet:(self db executeQuery:query). }
/. { |= { resultSet := self dictionariesForQuery: ‘select name from sqlite_master where [type] = “table” ‘. resultSet collect at:’name’. } }
/:table/count { |= { self dictionariesForQuery: “select count() from {table}” | firstObject | at:‘count()’. } }
/:table/:index { |= { self dictionariesForQuery: “select * from {table}” | at: index. } }
/:table { |= { self dictionariesForQuery: “select * from {table}”. } }
/:table/:column/:index { |= { self dictionariesForQuery: “select * from {table}” | at: index. } }
/:table/where/:column/:value { |= { self dictionariesForQuery: “select * from {table} where {column} = {value}”. } }
/:table/column/:column { |= { self dictionariesForQuery: “select {column} from {table}"| collect | at:column. } }
/schema/:table { |= { resultSet := self dictionariesForQuery: “PRAGMA table_info({table})”. columns := resultSet collect: { :colDict | #ColumnInfo{ #’name’: (colDict at:’name’) , #’type’: (colDict at:’type’) }. }. #TableInfo{ #’name’: table, #‘columns’: columns }. } }
-tables { db:. collect: { :table| db:schema/{table}. }. } -logTables { stdout do println: scheme:db tables each. } }
extension NSObject { -initWithDictionary:aDict { aDict allKeys do:{ :key | self setValue: (aDict at:key) forKey:key. }. self. } }
scheme:db := SQLiteScheme alloc initWithPath: dbref path. stdout println: db:schema/artists shell runInteractiveLoop. ``` `
Let’s break down the code. The header:
``` #!/usr/local/bin/stsh #-sqlite:dbref ```
This Unix shell script calls stsh, the Smalltalk Shell. While the shell itself is a topic for another time, the second line is a method declaration. This duality allows scripts to be run from the command line or used as methods within a program.
Although interactivity isn’t crucial here, a benefit is automatic parameter parsing. stsh understands this script expects a filename or URL (a reference) and stores it in the dbref variable. Any omission triggers an error, simplifying the script. These declarations are optional; without them, parameters are placed uninterpreted into an args array.
We then import Gus Mueller’s excellent FMDB, a SQLite wrapper.
` ``` framework:FMDB load.
``` `
The framework scheme locates frameworks, and the load message is sent to the resulting [NSBundle](https://developer.apple.com/documentation/foundation/nsbundle).
Next, we define the ColumnInfo class, with name and type instance variables and a -description method.
` ``` class ColumnInfo { var name. var type. -description { “Column: {var:self/name} type: {var:self/type}”. } }
``` `
This is straightforward. The absent superclass defaults to NSObject. The description method, using “-” like in Objective-C, uses string interpolation with curly braces. (The need for fully qualified names like var:self/name for instance variables is temporary). The lack of an explicit return statement simplifies things; the last value is returned.
This emphasizes the core purpose (“this is the description”) over the procedural steps. While small, this hints at a larger shift toward declarative programming.
The definition of instance variables is a work in progress, but the var syntax suffices for now. The TableInfo class mirrors ColumnInfo, both representing database metadata.
Now, the heart of it all: the scheme-handler, a class inheriting from MPWScheme. It has an instance variable and an initialization method:
` ``` class SQLiteScheme : MPWScheme { var db.
-initWithPath: dbPath { self setDb:(FMDatabase databaseWithPath:dbPath). self db open. self. }
``` `
Implementing language features as classes, while natural in an OO language, goes beyond mere implementation. It’s about an stable starting point, where classes represent language features in an Open Implementation way, enabling Open Language Implementation.
Unlike typical MOPs, these classes are meaningful outside the language, usable (though perhaps less conveniently) from other languages. This accessibility is crucial for an architectural language.
With this mapping, a small set of syntactic mechanisms can represent a vast and extensible set of semantic features. This, similar to procedures, methods, and classes, extends to aspects not traditionally as extensible.
Two methods interface with FMDB, clear to both Smalltalk and Objective-C programmers.
` ``` -dictionariesForResultSet:resultSet { results := NSMutableArray array. { resultSet next } whileTrue: { results addObject:resultSet resultDictionary. }. results. }
-dictionariesForQuery:query { self dictionariesForResultSet:(self db executeQuery:query). }
``` `
Smalltalkers might notice curly braces instead of brackets for blocks, a minor concession to mainstream syntax. Objective-C programmers might find the block-based while-loop condition unusual but not conceptually difficult.
Next are property path definitions. Each defines code executed for a specific subset of the scheme’s namespace, determined by the path’s URI pattern. It’s a generalization of Objective-C properties, handling sets, sub-paths, and combinations thereof.
` ``` /. { |= { resultSet := self dictionariesForQuery: ‘select name from sqlite_master where [type] = “table” ‘. resultSet collect at:’name’. } }
``` `
The first definition applies to the period path (db:.). Property paths start with “/”, similar to “-” for instance methods and “+” for class methods in Objective-C/Smalltalk, indicating path/URI definitions.
Like C# or Swift, we need “get” and “set” access. Instead of keywords, I use constraint connectors: “|=” for “get” (left side constrained to the right) and “=|” for “set.” This views the left as the interface, the right as the object’s internals, and properties as mediators.
This is experimental but promising, shifting from action-oriented to relationship-focused code. Delegating “get” and “set” could look like: /myProperty =|= var:delegate/otherroperty.
The result set retrieval is a simple message send with the SQL query. We extract table names using the collect HOM and the -at: message (equivalent to -objectForKey:). Subsequent paths map URIs to table queries. Unlike the first example (a fixed, single-element path akin to a classic property), these involve variable elements, multiple segments, or both.
` ``` /:table/count { |= { self dictionariesForQuery: “select count() from {table}” | firstObject | at:‘count()’. } }
/:table/:index { |= { self dictionariesForQuery: “select * from {table}” | at: index. } }
/:table { |= { self dictionariesForQuery: “select * from {table}”. } }
``` `
Starting from the last one, /:table returns data for the table specified by the :table parameter in the URI. The leading colon signifies a parameter that captures any string. Wildcards are possible too.
Yes, SQL queries are done via simple, unsanitized string interpolation. This is for demonstration purposes ONLY, not for production use.
The second query retrieves a specific table row. The pipe operator enables method chaining with keywords, avoiding excessive parentheses:
` ``` self dictionariesForQuery: “select count() from {table}” | firstObject | at:‘count()’ ((self dictionariesForQuery: “select count() from {table}”) firstObject) at:‘count()’
``` `
The “pipe” version is clearer, replacing nested evaluation with linear flow, a benefit of integrating pipes/filters, part of a larger goal to blend architectural styles. The improvement to procedural code is a pleasant side effect.
The first property path, /:table/count, returns the table’s size using the efficient select count(*) SQL statement. This highlights an interesting aspect. In a typical ORM, getting a table count might look like: db.artists.count. A naive implementation would fetch the entire “artists” table, convert it to an array, then count it, which is inefficient. This is a known problem in ORMs.
This is tricky to solve in OOPLs due to the lack of structural polymorphism. In db.artists.count, something must be returned by artists to receive the count message. The natural choice, the artists table, is inefficient. Solutions like proxies or separate count handling are not straightforward, hence the persistent issue.
Property paths eliminate this. Scheme handlers control sub-structures to any depth.
Queries are handled similarly. db:albums/where/ArtistId/6 fetches Apocalyptica’s albums. While manual, you’d specialize this for any specific database, using named relationships and returning objects instead of dictionaries. The /schema/:table path is a step in that direction:
` ``` /schema/:table { |= { resultSet := self dictionariesForQuery: “PRAGMA table_info({table})”. columns := resultSet collect: { :colDict | #ColumnInfo{ #’name’: (colDict at:’name’) , #’type’: (colDict at:’type’) }. }. #TableInfo{ #’name’: table, #‘columns’: columns }. } }
``` `
This returns the SQL schema as our defined objects. We query the schema information table, getting a dictionary array. These dictionaries are converted to ColumnInfo objects using object literals.
Like the -description method defined as a string literal, object literals allow direct object definition. The collect example defines a ColumnInfo object literal, setting name and type from the database dictionary.
The final TableInfo is similarly defined. Object literals, a simple extension of Objective-Smalltalk’s dictionary literals (#{ #'key': value }) with a class name, allow writing down objects directly, a powerful feature found in WebObjects .wod files, QML, and React’s declarative style.
Configuration in architectural description languages can also be seen as literal object definitions.
With this, and Objective-Smalltalk’s runtime support for defining classes on-the-fly, we can create classes from SQL table definitions without generating and compiling code.
While not implemented here (and not always desirable), it’s a step towards orthogonal modular persistence. Two final utility methods demonstrate the expressiveness of Objective-Smalltalk:
``` -tables { db:. collect: { :table| db:schema/{table}. }. } -logTables { stdout do println: scheme:db tables each. } ```
-tables retrieves schema information for all tables. -logTables prints each table to stdout. Finally, an NSObject extension supports literal syntax, and the script initializes the scheme with a database and starts an interactive session, a useful Smalltalk scripting feature.
And that’s it!
While not groundbreaking, this offers a glimpse into Objective-Smalltalk’s potential. There’s more to explore, like scheme-handlers for composition, not just exploration, and the integration of pipes-and-filters. And of course, much is yet to be done.
As always, feedback is greatly appreciated! The code is available on github.