At the end of my previous post, I think I lost most of the readers when I compared Web applications to DSLs, and then talked about implementing them as external or internal DSLs. For people with no actual experience with implementing DSLs this comparison is probably hard, and especially the distinction between internal and external DSLs. I remember having long conversations with fellow DSL researchers not agreeing if a certain DSL framework works with external or internal DSLs. So if I lost you there, don’t feel bad. It’s on me.
In this post I am going to demonstrate this equivalence between DSLs and Web apps by defining the logic of a simple Web application using Cedalion — a language designed to host DSLs. We are not going to implement a Web application here. Unfortunately, no actual application will come out of this exercise, but with a bit of imagination this exercise will demonstrate what the goal of my research is: I want Web applications to be as easy to implement as this!
Note: The code example in this post can be found on GitHub: https://github.com/brosenan/simpleTweet
Defined, not Implemented
Throughout the Cedalion DSL tutorial, I made effort to use the term “define” rather than “implement” when it came to telling Cedalion how a certain DSL concept needs to be understood. To me, “implement” means telling the computer what needs to be done step after step, while “define” means tell the computer what the end result needs to be. “Implement” is imperative, and “define” is declarative.
As part of my effort to distill the contribution of my research to a single sentence I thought about: “Your app defined, not implemented“. It kind-of says it all. I even created an HTML splash screen which I thought I would assign to this blog. I invested in the typography of this page, sifting through hundreds of Google Fonts, and came out with this:
Then I figured, it’s premature to have something like that on my blog. Maybe sometime in the near future.
So in this post I’d like to demonstrate what I mean by “defining” an app.
The Simplified Twitter Model
The simplified Twitter model is something I already introduced in my post on NoDatalog and NoSQL. It is a simple example application in which users can tweet, follow other users and display timelines. This model is so simple that “timelines” do not even include time… Just an unsorted collection of tweets made by users you follow.
Let’s define the simplified Twitter model in Cedalion.
In the Cedalion Workbench, create a project named “simpleTweet”. In that project, create a file named “tweets.ced”.
Create a new statement and enter:
and then, on the underscore (_) enter:
We just added (without definition or declaration) a tweet statement, stating that alice (a user we have not declared) tweeted “Hello, simpleTweet!”.
Double-click the error marker next to “alice”, and declare alice to be a user. Then declare “user” to be a type (Cedalion will know it automatically, you just need to ask for its suggestion and take it), and save.
Take Cedalion’s advice regarding tweet. In the declaration, replace “alice” with variable User, and the tweet text with variable Tweet. Do this on both the left-hand side and the right-hand side of the declaration.
Save and the errors will go away. Since it’s Cedalion, we want things to not only type-check, but also look nice, like Human language or notation often used. Let’s add a projection definition for tweet, by selecting the bullet next to the declaration and pressing F9.
In the visualization (the right-hand side of the projection definition) insert a new element between User and Tweet, and enter:
You can see our original statement as “alice tweeted Hello, simpleTweet!”. Cool, we can tweet…
Now, append a new statement and enter:
Cedalion does not know what bob is (or who…) and does not know what follows is. Let’s help Cedalion. Start with bob. Declare bob as a user (type user is already defined, so you can now select it from the auto-completion menu by typing the beginning (e.g., “us”) and hitting Ctrl+Space). Save to see that bob is now declared.
Then declare follows. Cedalion should be able to infer types (user and user) for both arguments. You should still replace bob’s and alice’s names with variables: B for bob and A for alice.
Let’s add a projection definition for follows. You know… F9 on the bullet next to the declaration… Insert string “follows” between B and A, and save.
As you can see, “bob follows alice”.
Note: Those of you who are bothered by the fact that we use lowercase letters to begin the names of Alice and Bob should recall that here, alice and bob are concepts, and names of concepts in Cedalion start with a lowercase letter. However, you are more than welcome to define projections for bob and alice, in the form of “Bob” and “Alice”, which will present them properly. We did that in our hello, world tutorial.
But What does it all Mean?
OK, so we have a small “DSL” in which we can write things like “bob tweeted this” and “alice follows that guy”, but what does it all mean?
At this point — nothing. This is just “data”. If we compare what we just did to the creation of a Web application, we did two important things:
- We defined two entities: a “tweet” and a “follow”. This is similar to defining a database schema that consists of two tables with these names.
- We defined projections for our data. In a Web application this is what you typically do with templates. A template translates (projects) your data into HTML. Cedalion’s visualization DSL (the one we use on the right-hand side of a “display as” statement) is in fact a template language.
In Web applications, the database schema and the templates are two important parts of the application. They define how the data is structured, and how it is displayed. In DSL terms, these are the abstract syntax (the schema or the declarations) and the concrete syntax (the templates or projection definitions) of the DSL corresponding to the Web app.
While abstract and concrete syntax are important, a DSL is nothing without its semantics. Likewise, a Web application is nothing without its business-logic.
How do we define the semantics of a DSL? When we defined our simpleExpr DSL we gave different DSL concepts such as const and the + operator meaning by defining how they are evaluated. We actually gave the “evaluates to” predicate solutions for different kinds of expressions. In general, Cedalion DSLs are given meaning by defining how different concepts contribute answers to questions. In our case, the interesting question is: “what tweets are on my timeline”?
The equivalent of semantics in the Web app world is business logic. In our little application, the business logic needs to handle the same thing: calculate a user’s timeline. So let’s define ourselves a timeline…
Create a new file named “timeline.ced”. Insert a statement and select “behavior” from the auto-completion menu.
Fill the blanks with:
timeline(B, A, T)::pred
and the string “should succeed for tweet T made by A who B follows”.
When selecting “pred” from the auto-completion menu, be sure to select the one from namespace /bootstrap.
Two double-clicks to declare timeline, then select A and B’s types to be user, and T’s type to be string. Then save.
Now open the behavior, and on the underscore inside it select timeline from the auto-completion menu. The test will fail because timeline is not defined.
Let’s fix this. This time we will fix this in a way that will not produce results. This is in line with a TDD principle stating you should not go beyond fixing the immediate problem during the green phase.
Append a statement after the behavior. Select “timeline” from the auto-completion menu. On the right-hand side of the :-, replace the “true” with a “fail” (of namespace builtin). fail is a builtin predicate that fails unconditionally. It is the exact opposite of true, which always succeeds. Save, and see the exception is replaced with a “normal” failure.
Now the timeline predicate is defined, but does not produce any results. Before we move on, set the first argument of timline in our test to bob. The test will continue to fail the same way.
We want bob as our first parameter so that if we advance this DSL/app further, the test will remain restricted to the world of bob and alice.
To fix our failing test, create a statement that looks like this:
B follows A ~> A tweeted T ~> timeline(B, _, _) :- true
The editing technique here is to start at the left, with “B follows A”, then when selecting all of it, select ~> from the auto-completion menu and edit the right-hand side with “A tweeted T”. Then select the “A tweeted T” part and select ~> again. Select the right-hand underscore and choose “timeline” from the auto-completion menu. At this point, just fill in the B variable so that the test passes. Save, and the test will pass.
The test passes, but the timeline predicate does not yet provide the twitter and the tweet. Check that by replacing the two underscores to the right of bob in the test with variables A and T, and by adding the following lines to the test:
A should equal alice :: user, T should equal Hello, simpleTweet! :: string
(remember, you need to select the whole goal in the test and select “,” from the auto-completion menu to be able to append a goal.)
The test fails. Let’s fix it. Just replace the two underscores in our definition of timeline (the last statement in the file) with A and T. Save, and the test will pass.
This one statement is our entire business logic:
B follows A ~> A tweeted T ~> timeline(B, A, T) :- true
It should be read:
if B follows A and A tweeted T then timeline(B, A, T) should succeed
This one statement gives meaning to ‘follows’ and ‘tweeted’, and at the same time defined the timeline predicate.
Now it’s safe to remove the failing definition of timeline. We don’t need it anymore.
So, What did we just Do?
One thing we didn’t do — implement a Twitter-like Web application. We just played around a little within the (relative) safety of the Cedalion Workbench. With that said, we did define some of the essential parts of a Web application. Our declarations can count as a schema, our projection definitions are templates defining presentation of our data, and the timeline definition provided tweets and following with semantics.
One thing I would take from this tutorial/demo is how easy it is to define business logic this way. The entire business logic of this “application” boils down to just one line…
Sceptics can probably raise two points here:
- That one line can be written in 5 short lines in SQL (see here).
- Would this ease be maintained for the complexity of a real-life application?
Regarding the first point, what makes this a 5-liner in SQL is that SQL, while verbose, is a purely-declarative DSL, designed specifically for defining relations between things like tweets, following relations and timelines. As I explain in that same post, SQL is also very limited, mainly in its ability to scale. In Part 2 of the same post I show how a rule, very similar to the one we defined here, can not only express the semantics of the timeline concisely, but also scale much better than SQL.
To answer the second point, I recently implemented a moderately more complex Twitter-like application using Cedalion and a prototyped NoDatalog database. This version tokenized tweets to identify hashtags and replies, and would index tweets accordingly and support search. I cannot call it a “real life” application, but it gets close. There are more rules, obviously, but the rules themselves are not much more complicated than the one we had here.
Oh, and that one was an actual Web application…
We are so used to implementing everything that we sometimes forget that there are easier ways to do things. I hope in this post I managed to give some intuition as for the similarity between Web applications and DSLs, and as to why I think apps can be defined, and not only implemented.