But sometimes you need to pause, reflect, and show your work, in whatever intermediate state it currently is. So without further ado, here is the distributed system, with GUI, in a tweet:
#!env stui
— Marcel Weiher 🇪🇺 (@mpweiher) August 9, 2022
scheme:s3 ← ref:http://defiant.local:2345/ asScheme
text ← #NSTextField{ #stringValue:'',#frame:(10@45 extent:180@24) }.
window ← #NSWindow{ #frame:(300@300 extent:200@105),#title:'S3', #views:#[text]}.
text → ref:s3:bucket1/msg.txt.
app runFromCLI:window.
It pops up a window with a text field, and stores whatever the user enters in an S3 bucket. It continues to do this until the user closes the window, at which point the program exits.
Of course, it's not much of a distributed system, particularly because it doesn't actually include the code for the S3 simulator.
Anyway, despite fitting in a tweet, the Objective-S script is actually not code golf, although it may appear as such to someone not familiar with Objective-S.
Instead, it is a straightforward definition and composition of the elements required:
- A storage combinator for interacting with data in S3.
- A text field inside a window, defined as object literals.
- A connection between the text field and a specific S3 bucket.
S3 via Storage Combinator
The first line of the script sets up an S3 scheme handler so we can interact with the S3 buckets almost as if they were local variables. For example the following assignment statement stores the text 'Hello World!' in the "msg.txt" file of "bucket1":
Retrieving it works similarly: s3:bucket1/msg.txt ← 'Hello World!'
The URL of our S3 simulator is stdout println: s3:bucket1/msg.txt
http://defiant.local:2345/
, so running on host defiant
in the local network, addressed by Bonjour and listening on port 2345. As Objective-S supports Polymorphic Identifiers (pdf),
this URL is a directly evaluable identifier in the language.
Alas, that directness poses a problem, because writing down an identifier in most programming
languages yields the
value of the variable the identifier identifies, and Objective-S is no exception. In the case of
http://defiant.local:2345/
, that value is the directory listing of the root of the S3 server, encoded as the following XML response:
That's not really what we want, we want to refer to the URL itself. The
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Owner><ID>123</ID><DisplayName>FakeS3</DisplayName></Owner>
<Buckets>
<Bucket>
<Name>bucket1</Name>
<CreationDate>2022-08-10T15:18:32.000Z</CreationDate>
</Bucket>
</Buckets>
</ListAllMyBucketsResult>
ref:
allows
us to do this by preventing evaluation and thus returning the reference itself, very similar to
the &
operator that creates pointers in C.
Except that an Objective-S reference (or more precisely, a binding) is much richer than
a C pointer. One of its many capabilities is that it can be turned into a store by sending it
the -asScheme
message. This new store uses the reference it was created from as its
base URL, all the references it receives are evaluated relative to this base reference.
The upshot is that with the s3:
scheme handler defined and installed as described, the
expression s3:bucket1/msg.txt
evaluates to
http://defiant.local:2345/bucket1/msg.txt
.
This way of defining shorthands has proven extremely useful for making complex references usable and modular, and is an extremely common pattern in Objective-S code.
Declarative GUI with object literals
Next, we need to define the GUI: a window with a text field. With object literals, this is pretty trivial. Object literals are similar to dictionary literals, except that you get to define the class of the instance defined by the key/value pairs, instead of it always being a dictionary.
For example, the following literal defines a text field with certain dimensions and assigns it
to the text
local variable:
And a window that contains the text field we just defined:
text ← #NSTextField{ #stringValue:'',#frame:(10@45 extent:180@24) }.
It would have been nice to define the text field inline in its window definition, but we currently
still need a variable so we can connect the text field (see next section).
window ← #NSWindow{ #frame:(300@300 extent:200@105),#title:'S3', #views:#[text]}.
Connecting components
Now that we have a text field (in a window) and somewhere to store the data, we need to connect these two components. Typically, this would involve defining some procedure(s), callback(s) or some extra-linguistics mechanism to mediate or define that connection. In Objective-S, we just connect the components:
That's it. text → ref:s3:bucket1/msg.txt.
The right-arrow "→" is a polymorphic connection "operator". The complete connection is actually significantly more complex:
- From a port of the source component
- To a role of the mediating connector compatible with that source port
- To a role of the mediating connector compatible with the target object's port
- To that compatible port of the target component
If we didn't want a remote S3 bucket, we could also have stored the data in a local file, for example:
That treats the file like a variable, replacing the entire contents of the file with
the text that was entered. Speaking of variables, we could of course also store the
text in a local variable: text → ref:file:/tmp/msg.txt.
In our simple example that doesn't make a lot of sense because the variable
isn't visible anywhere and will disappear once the script terminates, but
in a larger application it could then trigger further processing. text → ref:var:message.
Alternatively, we could also append the individual messages to a stream, for
example to stdout
:
So every time the user hits return in the text field, the content of the text
field is written to the console. Or appended to a file, by connecting to the
stream associated with the file rather the file reference itself:
text → stdout.
This doesn't have to be a single stream sink, it can be a complex
processing pipeline. text → ref:file:/tmp/msg.txt outputStream.
I hope this makes it clear, or at least strongly hints, that this is not the usual low-code/no-code trick of achieving compact code by creating super-specialised components and mechanisms that work well for a specific application, but immediately break down when pushed beyond the demo.
What it is instead is a new way of creating components, defining their interfaces and then gluing them together in a very straightforward fashion.
Eval/apply vs. connect and run
Having constructed our system by configuring and connecting components, what's left is running it.CLIApp
is a subclass of NSApplication
that knows how to run without an associated app wrapper or Info.plist
file. It is actually instantiated by the stui
script runner before
the script is started, with the instance dropped into the app
variable
for the script.
This is where we leave our brave new world of connected components and
return (or connect with) the call/return world, similar to the way Cocoa's auto-generated
main
with call to NSApplicationMain()
works.
The difference between eval/apply (call/return) and connect/run is actually quite profound, but more on that in another post.
Of course, we didn't leave call/return behind, it is still present and useful for certain tasks, such as transforming an element into something slightly different. However, for constructing systems, having components that can be defined, configured and connected directly ("declaratively") is far superior to doing so procedurally, even than the fluent APIs that have recently popped up and that have been mislabeled as "declarative".
This project is turning out even better than I expected. I am stoked.
No comments:
Post a Comment