Saturday, May 23, 2020

What Is Declarative Programming in QML

The QML Declarative Programming Scene

It is not uncommon to see many developers, who believe they practise declarative programming in QML, to have been thought the wrong way of declarative programming by their colleagues or incumbent QML experts. With this chain reaction, the wrong way to program declaratively gets propagated from software engineer to software engineer. Before long, the QML code base becomes highly complex and difficult to manage. Sometimes, developers refer to it as a mine field.

What is Declarative Programming

There are many definitions of declarative programming, many of which have been referred to as "difficult to understand" or "can't warp my head around this explanation". It is especially so if one has no prior practical experience with declarative programming.

To compound the problem further, a developer who has been exposed to the "wrong type" of declarative programming thinks declarative programming is as they have been introduced. In my opinion, this is Qt/QML's most fundamental declarative programming gotcha. Many who write QML are not writing QML correctly. It works, but...

So, what is declarative programming? One can refer to wikipedia: https://en.wikipedia.org/wiki/Declarative_programming. It says declarative programming is:

"a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow".

In my simple language, declarative programming is programming:
  • without for-loops
  • without while-loops
  • without conditional if statements
  • without switch statements
  • without function calls

Now that I have captured my readers' attention, and my readers' eyes have popped out of your head, declarative programming is programming that:

  • uses variables (better known as property in QML)
  • uses block structures
  • and that is it!
An example of a declarative programming language is plain old HTML (without any extension like Javascript). There is a block for everything and everything is static as is. What HTML lacks is variables, which is available in QML.

The Real Declarative Programming in QML

Having properties (i.e. variables) in QML means QML maintains states. The use of states is to branch logic. That is:
if (state1) 
    do this_thing 
else 
    do that_thing
And there you have it. That is not declarative programming!

QML's declarative programming cannot survive without all the conditional-if's and for-loops intermingled within its declarative syntax! Yes! You read it right! QML's declarative programming paradigm only solves some of programming problem. The rest are solved using imperative programming. QML's declarative programming paradigm is amalgamated with imperative programming!

As developers confuse themselves between declarative programming and imperative programming, and as developers use more imperative programming in place of declarative programming, the code base becomes increasingly difficult to maintain.

Compounding the problem further, most Qt QML developers know C++ intrinsically well. These developers, having tasted QML, will choose to solve the QML code mess with a simple approach - move as much code and logic to C++ as possible. It's faster and there is structure.

I would disagree with the fact that QML is slower than C++. UI's with a window interface is a very different application from traditional compute intensive applications. UI's need not be fast because human's eye cannot detect anything flickering faster than 1/60 of a second. 1/60 of a second is a lot of time to a CPU. The detail of this topic is better left for another article, another day.

An Example of Declarative Programming in QML

Let us start with an example program - a program with one main window that embeds a single button. The size of the embedded button should always be as big as the main window. No matter how the main window is resized, the button will take up the whole space of the main window.

The following is an example code of a main window with one button. The button does not resize itself to the main window.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Button {
        width: 400
        height: 200

        text: "click me!!"
    }
}


The following is the application when compiled and run.



The Wrong Way To Write Declaratively

We further improve the code above, albeit incorrectly, to resize the button every time the application's main window is resized. The following is the code.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    onWidthChanged: myButton.width = width
    onHeightChanged: myButton.height = height
    Component.onCompleted: {
        myButton.width = width
        myButton.height = height
    }

    Button {
        id: myButton
        text: "click me!!"
    }
}

The example above uses both imperative programming and declarative programming to resize the button.

Line 13-16: When the program is loaded, set the button's width and height to that of the main window.

Line 11: Every time the width of the main window changes, set the width of the button to the width of the main window.

Line 12: This is the same as line 11 above, except it applies to the height of the button.

There is a sense of declarative programming in the example above. Every time main window's width and height change, a signal will be emitted and the button's width is assigned to main window's width. Some would say, the button's width is bound to main window's width.

However, the statement above is not exactly correct. With this statement
myButton.width = width
the original binding of myButton.width has actually been unassigned!

In QML, all bindings will be removed once a property is assigned a value from a Javascript statement. In this particular case, the assignment operator has broken the original binding of myButton.width (which is not bound to anything in this example but it would be a problem/bug if it is bound to something).

When compiled and run, the application would look like the image below.



The Correct Way To Write Declaratively

The following is an ideal way to implement the program declaratively.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Button {
        id: myButton
        text: "click me!!"

        width: parent.width
        height: parent.height
    }
}


Line 15: Bind the width of myButton to parent.width
Line 16: Bind the height of myButton to parent. height

With QML's binding, a signal will be emitted every time parent.width changes and the change will be assigned to myButton.width. That is all one has to write when one is writing good clean code.

Note that the assignment operator does not provide binding and should be avoided but cannot be omitted entirely. Once an assignment operator is used, it would override the original binding of a property (i.e. the original binding would have been deleted). Of course, one can choose to use Qt.binding() but that is again, a topic for another day another article.

Further, the moment any code is inserted inside a signal handler (i.e. "onXxxxxChanged:"), that is Javascript code and the simplest Javascript code like the assignment operator will override (i.e delete) the binding on a property.

In QML's declarative programming paradigm:

  • binding is static
  • binding is implemented using the colon operator (i.e. ":"). Not the assignment operator!
  • bind a property to another property using conditional-if's
  • use of conditional-if is not declarative programming but one cannot live without it
  • the assignment operator deletes binding
  • signal handlers (i.e. "onXxxxxChanged:") delete binding!


No comments: