QML integration with Fay and Haskell
If you are reading this, you are probably more or less aware of the JavaScript Problem. Like many, I frequently work with JavaScript. There is no avoiding JavaScript on the web and many corners of the software development industry.
My most recent JavaScript endeavor has been in the world of Qt5 and QML. QML is a solid toolkit for writing fluid and cross-platform user interfaces.
Recently, while experimenting with QML, I had a revelation. What if it was possible to generate QML code from Haskell using Fay. Eureka!
Luckily, Fay is up for the job. Fay is a Haskell to Javascript compiler which supports a proper subset of the Haskell language. Fay has several distinct advantageous:
- Statically typed
- Lazy
- Pure by default
- Compiles to JavaScript
- Has fundamental data types (Double, String, etc.) based upon what JS can support, and compound data types (ADTs and GADTs)
- Outputs minifier-aware code for small compressed size
- Has a trivial foreign function interface to JavaScript
- Supports cabal installation of Fay packages
- Can automatically transcode values to/from JSON using the FFI
- Provides an API to transcode on the server side as well
- Lets you call Fay code from JavaScript
- Has the Fay monad for side effects (think of it like IO)
– Fay Wiki
This post will cover how to integrate a JavaScript library generated using Fay into a QML application. The library will be developed purely in Haskell while the UI code will be written in QML.
QML, while a subset of JavaScript, is essentially it’s own language. Fay does not understand QML and therefore cannot compile it to JavaScript yet.
My goal is to, first, get the JS library integration functioning. After that, the issue of getting Fay to compile QML Forms written in Haskell can be evaluated and potentially implemented.
The most up-to-date version of the qmlfay
example source is on my Github. I would like to get this unique example into the Fay repository if anyone else finds it useful.
Interfacing a Haskell Library With QML Using Fay
The first objective is to pass QtObjects, created in QML, to a Fay JavaScript library. In the library, properties should be read from the QtObject and the data can be processed accordingly.
Make sure fay
and fay-base
are installed.
cabal install fay fay-base
Haskell
Let’s start with the fun part. The Haskell module is named Library
and exports a function named qObjectName
. This function should return the objectName property of a QML QtObject.
module Library (qObjectName) where
The data type QObject is defined to represent our QML component (which is based on the QtObject element). Unfortunately, I have not yet found a way to generate this record based on the actual C++ QObject type properties. Fortunately, we only need fields for each property used. As far as I can tell, Fay matches the record entry with the JavaScript object’s property when applying this type.
data QObject = QObject { objectName :: String }
The function qObjectName
takes a QObject parameter and returns the objectName. undefined
will be returned if the objectName
property doesn’t exist.
qObjectName :: QObject -> String
qObjectName qo = objectName qo
Next, some boilerplate is necessary to initialize the QML application.
Boilerplate
First, we need a main.cpp file to run the application. While it is technically possible to run the app using qmlscene
, I wanted to show an example with qmake so it can be more easily adapted to existing qmake build systems.
Main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QUrl>
int main(int argc, char** argv) {
QGuiApplication a(argc, argv);
QQuickView view;
view.setSource(QUrl("./Main.qml"));
view.show();
return a.exec();
}
The QQuickView
’s source is set to Main.qml
which is our QML entry point.
Main.qml
Main.qml
starts off with imports. Our Fay generated JavaScript library, Library.js
, is a qualified import named Library
.
import QtQuick 2.0
import "Library.js" as Library
Next, there is a Rectangle
element. The objectName
property, which will be accessed from Library.js
, is set to "MyObject"
.
We set the component id
to page
for reference.
Rectangle {
id: page
width: 100; height: 100
objectName: "MyObject"
Component.onCompleted: {
After a reference to the Library
object is retrieved, it’s possible to run L.objectName(page)
. Note that the id is being passed to the qObjectName
function. The id is reference to the JavaScript object page
.
var L = Library.Strict.Library; // Pass --strict to fay compiler.
console.log(L.qObjectName(page))
}
}
qmlfay.pro
The qmake project file defines how the project should be built. qmake
offers macros to extend the build with extra compilers. This should work perfectly for the Fay compiler build rules as their compilation is independent of the C++ sources.
TEMPLATE = app
TARGET = qmlfay
INCLUDEPATH += .
QT += gui quick
CONFIG += target_predeps
SOURCES += main.cpp
FAY_SOURCES += Library.hs
fay.name = fay compiling
fay.input = FAY_SOURCES
fay.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.js
Now the actual compile command is defined. Note the --strict
flag.
fay.commands = fay ${QMAKE_FILE_NAME} -p --strict ${QMAKE_FILE_BASE}
fay.variable_out = PRE_TARGETDEPS
QMAKE_EXTRA_COMPILERS += fay
Compile and Run
Now compile and run the application.
qmake-qt5 .
make
./qmlfay
Issues
Expected token identifier
error:
You might run into the following error importing the JavaScript sources from QML:
$ ./qmlfay
file:///.../dev/qmlfay/Main.qml:3:1: Script file:///.../dev/qmlfay/Library.js unavailable
import "Library.js" as Library
^
file:///.../dev/qmlfay/Library.js:2230:17: Expected token `identifier'
var as = $tmp1.cdr;
The workaround, for the time being, is to comment out the following functions from the generated javascript Library.js
: zipWith
,zipWith3
,zip
,zip3
. This also means these functions are unusable for now.
Conclusions
My goal is to bring a usable QtQuick experience to Haskell through the use of Fay. Once I have developed tighter library integration I plan to attempt to write a pass in the Fay compiler to handle pure qml.
I’m not unambiguously certain that all of these things are possible but this is an itch I must scratch.
Happy Hacking!