A few weeks ago, I showcased a small http backend. To be more precise, “tiny” better describes its size and functionality, which is practically non-existent. Its sole purpose is to define a basic Task class, create an array containing two example instances, and then serve this array over http. Instead of providing useful data like a JSON encoded format, it serves the -description of these tasks.
Here’s the initial code, quickly put together in approximately 15 minutes:
` ``` #!env stsh framework:ObjectiveHTTPD load.
class Task { var done. var title. -description { “Task: {this:title} done: {this:done}”. } }
taskList ← #( #Task{ #title: ‘Clean my room’, #done: false }, #Task{ #title: ‘Check twitter feed’, #done: true } ).
scheme todo { var taskList. /tasks { |= { this:taskList. } } }.
todo := #todo{ #taskList: taskList }. server := #MPWSchemeHttpServer{ #scheme: todo, #port: 8082 }. server start. shell runInteractiveLoop. ``` `
To enhance its usefulness, we need to encode the output as JSON instead of serving a description. This is where Storage Combinators prove valuable. We now utilize MPWJSONConverterStore, a mapping store that processes “REST” requests while applying specific transformations to the data and/or references. In this scenario, the transformation involves serializing objects to JSON when reading from its source and deserializing JSON to objects when writing to its source, depending on the request direction and converter orientation.
In this case, the converter is oriented “up,” meaning it serializes objects read from its source to JSON and deserializes data written to its source from JSON to objects. Additionally, we specify that it handles Task objects. After setting up the converter, we connect it to our todo scheme and instruct the HTTP server to communicate with the JSON converter, which in turn interacts with our todo scheme:
` ``` todo := #todo{ #taskList: taskList, #store: persistence }. json := #MPWJSONConverterStore{ #up: true, #class: class:Task }. json → todo. server := #MPWSchemeHttpServer{ #scheme: json, #port: 8082 }.
``` `
Furthermore, we aim to interact with individual tasks. This can be easily achieved by adding a /task/:id property path to our store/scheme handler, incorporating GET ("|=") and PUT ("=|") handlers. While I’m not entirely convinced about using “|=” syntax for this purpose, I prefer to avoid using names for structural elements. Perhaps arrows would be more suitable.
` ``` /task/:id { |= { this:taskDict at:id . } =| { this:taskDict at:id put:newValue. }
``` `
To facilitate this, we’ve replaced the taskList with a dictionary. When modifying data, persistence becomes crucial. Storing tasks as JSON on disk offers a straightforward solution, allowing us to reuse the previously mentioned JSON converter, but this time pointing “down.” We connect this converter to the filesystem at the /tmp/tasks directory and to the store:
` ``` json → todo → #MPWJSONConverterStore{ #class: class:Task } → ref:file:/tmp/tasks/ asScheme.
``` `
Moreover, we need to trigger the saving process within the PUT handler:
``` =| { this:taskDict at:id put:newValue. self persist. } -persist { source:tasks := this:taskDict allValues. } } ```
This modification results in synchronously writing the entire task list with every PUT request. The complete code is provided below:
` ``` #!env stsh framework:ObjectiveHTTPD load.
class Task { var id. var done. var title. -description { “Task: {this:title} done: {this:done} id: {this:id}”. } -writeOnJSONStream:aStream { aStream writeDictionaryLikeObject:self withContentBlock:{ :writer | writer writeInteger: this:id forKey:‘id’. writer writeString: this:title forKey:’title’. writer writeInteger: this:done forKey:‘done’. }. } }
taskList ← #( #Task{ #id: ‘1’, #title: ‘Clean Room’, #done: false }, #Task{ #id: ‘2’, #title: ‘Check Twitter’, #done: true } ).
scheme todo : MPWMappingStore { var taskDict. -setTaskList:aList { this:taskDict := NSMutableDictionary dictionaryWithObjects: aList forKeys: aList collect id. } /tasks { |= { this:taskDict allValues. } } /task/:id { |= { this:taskDict at:id . } =| { this:taskDict at:id put:newValue. self persist. } } -persist { source:tasks := this:taskDict allValues. } }.
todo := #todo{ #taskList: taskList }. json := #MPWJSONConverterStore{ #up: true, #class: class:Task }. json → todo → #MPWJSONConverterStore{ #class: class:Task } → ref:file:/tmp/tasks/ asScheme. server := #MPWSchemeHttpServer{ #scheme: json, #port: 8082 }. server start. shell runInteractiveLoop. ``` `
Currently, the serializer still requires the writeOnJSONStream: method to encode the task object as JSON. The parser doesn’t need any specific support; it can handle simple mappings independently. While this might seem illogical, as serialization is typically simpler than parsing, I haven’t implemented automation for serialization yet.
Analysis
There you have it - a nearly functional Todo backend implemented in a remarkably concise manner, with minimal reliance on magic. What I find particularly appealing is that this conciseness is achieved while maintaining a clear and visible architecture, adhering to a hexagonal/ports-and-adapters style.
The architecture of this application is evident at the end of the code: the server is parameterized by its scheme, which consists of a JSON serializer connected to our todo scheme handler, which is further connected to another JSON serializer linked to the /tmp/tasks directory.
Although a Rails app might have a similar amount of code, it is dispersed across different classes and is only understandable within the context of Rails. The entire architecture is concealed within Rails, making it invisible and impossible to discern solely from the code. While there are various reasons for this, a fundamental one is that Ruby is a call/return language, and Rails strives to translate the REST architectural style into something more compatible with the call/return paradigm. And it does a commendable job at that.
I believe this example offers a glimpse into the potential of Architecture Oriented Programming: the power and conciseness of frameworks combined with the simplicity, straightforwardness, and reusability of more library-oriented approaches.
Performance
Intrigued by its performance, I couldn’t resist benchmarking the application. To my delight, wrk is now functional on the M1. Due to the interpreter’s lack of thread safety, I was limited to using a single connection and thread. My expectation was a requests/s rate in the low hundreds, but I feared it might drop to single digits. This fear stemmed from the writeOnJSONStream: method, invoked for every serialized object and executed in interpreted Objective-S, potentially one of the slowest language implementations in existence. To say I was surprised would be an understatement; “stunned” would be a more accurate description:
` ``` wrk -c 1 -t 1 http://localhost:8082/task/1 Running 10s test @ http://localhost:8082/task/1 1 threads and 1 connections Thread Stats Avg Stdev Max +/- Stdev Latency 133.62us 14.45us 0.97ms 98.52% Req/Sec 7.50k 311.09 7.62k 99.01% 75326 requests in 10.10s, 12.28MB read Requests/sec: 7458.60 Transfer/sec: 1.22MBTransfer/sec: 1.97MB
``` `
The results revealed a rate of over 7K requests per second! The M1 Macs are undeniably fast. I am eager to see the performance improvement once the manual writeOnJSONStream: method is eliminated.
(Note: The previous version mentioned a rate of >12K requests/s, which is even more impressive but was based on an incorrect URL that resulted in 404 errors from the server.)