In our previous article Building a WebAssembly Based Chat App we built a simple chat application using Alusus programming language and WebPlatform framework. In this article I'm going to step back and explain the code in more details.
The entire app was written in a single source file. This single source file contains everything in the project including requesting dependencies, defining and accessing the data model, back-end, front-end, as well as build and publish script. You might think that it's not a good idea to put everything in a single file anyway, and you would be right, but the ability to put everything in a single file is a very useful feature of Alusus language, because it means the language does not dictate how your project should be structured. This has two main benefits:
- You can organize your project based on functionality, rather than based on where the code gets executed.
- You can package related back-end and front-end code together. For example, the Identity library includes front-end components as well as related back-end endpoints, making it much simpler to maintain and use the library.
Requesting Dependencies
The project starts with few import statement, which is pretty standard, but one of the packages it imports is Alusus Package Manager (Apm), which it then use to import external libraries.
Apm takes care of fetching then importing dependencies from an external source. At this point the only external source it supports is GitHub. When a package is imported it looks locally for that package, and if it doesn't find it it automatically fetches it from GitHub, then it imports the package into the project. So, the user can manage his project's dependencies from within the source code, without needing to define those dependencies in external yaml, json, or xml files like most other package managers.
The Data Model
You can see in the code that all it takes to define the data model is to define classes and mark them with some modifiers. The ORM depends on those modifiers for generating the DB schema, while changes in the data model can applied using migrations, which are user defined functions marked with a modifier as well. Take a look at the message model for example:
@model["messages", 2]
class MessageModel {
Rows.define_model_essentials[];
@foreignKey["rooms", "name"]
@VarChar["256"]
@notNull
@column["room_name"]
def roomName: String;
@foreignKey["users", "username"]
@VarChar["256"]
@notNull
@column
def username: String;
@VarChar["1024"]
@notNull
@column
def message: String;
@BigInteger
@notNull
@column["creation_date"]
def creationDate: Int[64];
@migration[1, 2, { RoomModel: 1; UserModel: 1 }]
function migrateToV2(db: ref[Rows.Db]): SrdRef[Error] {
db.exec("alter table messages add column room_name varchar(256) not null references rooms(name)");
db.exec("alter table messages add column username varchar(256) not null references users(username)");
return SrdRef[Error]();
}
}
Most of these attributes modifiers are self-explanatory. The are some details related to migrations, but migrations is a subject that's worth its own article, so will leave it for another time.
Using the model to fetch data from the DB is done through a syntax like this:
def messages: Array[SrdRef[MessageModel]] = db
.from[MessageModel]
.where[roomName == body("roomName")~cast[String].trim()]
.order[-creationDate]
.select()
.slice(0, MAX_MESSAGES);
In the upper statement it's using this model to fetch messages belonging to a specific room. There are few things to note here:
- The use of square brackets. In Alusus square brackets aren't used for arrays like many other languages, instead, square brackets indicate parameters that are processed during compile time, compared to parenthesis which are used for passing parameters at run time. In this statement we are passing few things at compile time. This is because converting this call to an SQL statement happens at compile time, not run time, which is good for both performance as well as security.
- Within the parameters of
.wherewe are passing an expression. At compile time, this expression gets converted into an SQL where clause. This is a pretty clean and easily readable way for writing DB queries. - A similar thing applies to the
.ordercall; what we are passing toorderis an expression that tells the ORM how to order the result. In this case we are order bycreationDatedescending. Notice that we aren't passing a value here; we are passing the name of the model class' member variable.
Back-end and Front-end
You can see two different modifier types used for annotating endpoints: @beEndpoint (back-end endpoint) and @feEndpoint (front-end endpoint). Both of these are applied to functions to make them the entry point of that endpoint. Back-end endpoints are simply functions that receive a connection object that they can use to read request data and send back responses. These functions are part of the main app, so they, unlike front-end endpoints, get compiled to the same instruction set as the main app.
Front-end endpoints on the other hand are more complicated in terms of compilation. When you annotate a function with @feEndpoint that tells WebPlatform to:
- Compile the function into WebAssembly.
- Generate a corresponding HTML file (and potentially other related files). This complexity, however, is handled completely by WebPlatform and the user doesn't need to worry about any of that. The user just uses some APIs to build a UI in a way similar to desktop applications. The user doesn't need to worry about writing HTML, CSS, Javascript, or build scripts; things just work. I don't know if web development can be made simpler and more intuitive than this.
Within the front-end function you'll see a call to Window.instance.setView which sets the browser's view. What gets passed to setView is a UI tree composed of widgets and components. This is similar to many other UI frameworks, but, unlike other frameworks, the syntax used to build the tree is standard Alusus syntax. No html, xml, or jsx syntax; just standard Alusus syntax that can be used to build UI trees and can also be used for other things.
The example also includes two components: TextEntry and Header. Those are simply classes that inherit from the Component class and set the view property of it. They set the view property inside the constructor, which in Alusus is defined using the handler this~init definition. The following syntax is what is used to define the inheritance:
@injection def component: Component;
There is a reason for this unusual syntax but that is worth its own article, so we'll not get into that for now. Finally, you'll often see components include such a definition:
handler this_type(): SrdRef[Header] {
return SrdRef[Header].construct();
}
This handler overrides the parenthesis operator on the class itself, not the class instances. This is used to override the default behavior, which instantiates a temporary object, and replace it with a function that allocates an object on the heap and return a shared reference to it. This makes it easier to later on instantiate objects of this type when creating a UI tree.
The front-end function usually ends with a call to runEventLoop(). This is a blocking function that runs the event loop, similar to native app development.
Build & Publish Script
In the Deployment section of the article we replaced the entry point of the app with this:
Nashir.setup[Nashir.Config().{
projectName = "chat";
projectVersion = "v1";
serverPort = 8000;
initializer~no_deref = initializeServer~ast;
dependencies.add(Rows.PostgresqlDriver.getBuildDependencies())؛
publishDriver = Nashir.AlususNetPublishDriver();
}];
ProgArg.parse(2);
What these 8 lines of code give us is the following:
- An entry point for running the server locally.
- A build script that generates an executable.
- A publish script that publishes the project to Alusus Net.
- A set of command line arguments that the user can use to decide whether to run, build, or publish.
All of that was done with these few lines of code. Behind the scenes, these lines of code searches the code base for front end endpoints as well as static resource routes in order to determine what gets packaged with the app during the build. Basically, what takes a script to accomplish with other languages is done here with these few lines of code.
At the end of the call to Nashir.setup we just call ProgArg.parse to tell it to parse the command line arguments and perform the requested user action. The 2 argument just tells ProgArg the index at which command line arguments start. In this case it's 2 because the first arg will contain alusus and the second is the name of the main file passed to the compiler. If you use ProgArg from within pre-compiled code you'll need to pass 1 instead of 2 since you'll just call the executable itself rather than calling alusus and passing the main source to it.
Low Level Language
What we've seen so far may give you the impression that we are dealing with a pretty high-level language, but don't let this neatness deceive you. Alusus is a low level language; think C language kind of low-level. In fact, it's actually compatible with C so you can directly import C dynamic libraries and use them; all you need to do is to declare those functions then you can call them, just like in C you only need to declare the function or import a header that does it for you. What makes Alusus look like a high level language is the flexibility of the language that allow libraries to define new syntax or make calls to the compiler itself.
Going back to the example at hand, the compiler of Alusus doesn't know anything about packages, GitHub, back-end and front end points, static resource routes, deciding when to build to WebAssembly, data models, compiling DB queries to SQL, etc. All of those were features added by libraries. In fact, you may be surprised to know that Alusus' compiler doesn't even know what closures are; closures are added by the "closures" library which is built entirely in Alusus. This flexibility makes Alusus really unique and opens the door for many great features to be added in the future. Basically, you are getting even more flexibility than what dynamic high level languages provide, and you are combining that with the high performance of low level languages; the best of both worlds.
Stay tuned for future articles in which I'll be covering more areas of Alusus and its libraries. Subscribe to Alusus official social network channels to get notified when new articles or videos arrive.
Top comments (0)