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:
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!
if (state1)
do this_thing
else
do that_thingAnd 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.
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.
The following is the application when compiled and run.
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
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 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 = widththe 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:
Post a Comment