Many, many, many years ago, in my local bookstore, I had my first experiences with computers. I typed in my first program and always thought what it did was cool:
10 PRINT "HELLO"
20 GOTO 10
Today, I'm going to look into iterators and generators, and, yes, make a connection to how these latest features in ECMAScript 7 update of AppStudio 3.3 and Qt 5.12.1 aren't too dissimilar from old school concepts.
In my previous blog, I talked about "For Of" and showed that it works with Javascript iterators and Javascript generators.
function *cities() {
yield { name: "Hawaii", distance: 4221.73 }
yield { name: "Los Angeles", distance: 96.65 }
yield { name: "New York City", distance: 3853.10 }
yield { name: "Redlands", distance: 1.12 }
yield { name: "Seattle", distance: 1566.70 }
}
for ( let city of cities() )
console.log( JSON.stringify( city ) )
View the full ForIter.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
In the above example, we declare a Javascript generator which produces cities with their name and their distance from ESRI. We turn that Javascript generator into a Javascript iterator when we "call" it, i.e. cities(). As a Javascript iterator, we use a "for of" loop to process the results.
So far, nothing special. You can glean this sort of thing from any blog about Javascript iterators.
If we replace the "for of" syntax with a somewhat traditional looking "while" loop, we get to see some inner workings of the Javascript iterator:
function *cities() {
yield { name: "Hawaii", distance: 4221.73 }
yield { name: "Los Angeles", distance: 96.65 }
yield { name: "New York City", distance: 3853.10 }
yield { name: "Redlands", distance: 1.12 }
yield { name: "Seattle", distance: 1566.70 }
}
const iter = cities()
let city = iter.next()
while ( !city.done ) {
console.log( JSON.stringify( city.value ) )
city = iter.next()
}
View the full WhileIter.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
The first impression is this "while" version looks a bit hard to swallow. The "for of" shorten version looks cleaner. When you think about it, this version actually gives you more insight into the "yield" keyword. When "yield" is used, a value is returned from the Javascript generator, and the Javascript generator pauses execution until the next time we call "iter.next()".
At first, this may seem like an alien concept, but, if you look harder, you'll see this pattern in a lot of places, some quite old.
On Unix, we learn that in shell programming, we can join Unix commands together using Unix pipes. A pipe is a form of redirection that is used in Unix to send output from one program to another for further processing. The first process is not completed before the second is started. They are executed concurrently.
$ cat cities.txt
$ cat cities.txt | head -3
$ cat cities.txt | head -3 | tail -2
Whilst cat can produce 5 lines of text, in the above pipeline, it doesn't. The 3 Unix commands, cat, head and tail are executing concurrently. The cat command only needs to generate output, if the subsequent head and tail commands require it. In this example, we only require cat to generate 3 lines of text to fullfill the requirements of the subsequent head command.
We can mirror the above Unix statement in QML.
Button {
text: qsTr("LA and NYC cities")
onClicked: {
function *cities() {
yield { name: "Hawaii", distance: 4221.73 }
yield { name: "Los Angeles", distance: 96.65 }
yield { name: "New York City", distance: 3853.10 }
yield { name: "Redlands", distance: 1.12 }
yield { name: "Seattle", distance: 1566.70 }
}
function* cat( iter ) {
yield* iter
}
function* head( iter, n ) {
if (n <= 0) return
for (let item of iter) {
yield item
if (--n <= 0) break
}
}
function* tail( iter, n ) {
let arr = [ ]
for ( let item of iter ) {
arr.push(item)
if (arr.length > n) arr.shift()
}
yield* arr
}
for ( let city of tail( head( cat( cities() ), 3), 2) )
console.log( JSON.stringify( city ) )
}
}
View the full HeadAndTails.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
Similar to the Unix head, tail, cat commands, the above head, tail, cat Javascript generator functions start executing concurrently. The data pipeline occurs when callers consume data and the Javascript generator functions yield data.
Because of the requirements of the head generator call, we only require the cat generator and the cities generator to generate 3 lines of output.
Traditionally, we consider CPU intensive and/or infinite loops to be taboo in Javascript.
function helloWorldTaboo() {
while (true)
console.log( "Hello World" )
}
They block the UI, they make the program unresponsive, they may eventually lead to a crash. We consider such code taboo and we have been trained to not write in that style. We require the developer to rewrite their applications to use signals and slots or callback functions that help unblock the UI.
Rules changes when it comes to Javascript generators and Javascript iterators. Apparent infinite loops are no longer taboo:
function* helloWorldGenerator() {
while (true)
yield "Hello World"
}
The following demonstrates the Javascript generator working with a Timer object. The program is not CPU intensive and not quite an infinite loop in that one can stop the invoking the Javascript generator by stopping the Timer.
Button {
text: qsTr("Run!")
onClicked: {
function* helloWorldGenerator() {
while (true)
yield "Hello World"
}
helloWorldTimer.iter = helloWorldGenerator()
helloWorldTimer.start()
}
}
Timer {
id: helloWorldTimer
property var iter
running: false
repeat: true
interval: 100
onTriggered: {
let item = iter.next()
if (item.done) {
stop()
return
}
console.log( item.value )
}
}
View the full InfiniteHello.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
In Unix, the data flows in Unix pipes in one direction. With Javascript generators, the data flow can be bidirectional. We can take a value from the Javascript generator modify and send the result back to the Javascript generator.
The following shows the generator yielding 1, 2 and 3 respectively. For each value, we square the result and send the square back to the generator.
function* gen() {
console.log( yield 1 )
console.log( yield 2 )
console.log( yield 3 )
}
let iter = gen()
let v = iter.next()
console.log( JSON.stringify(v) )
v = iter.next( v.value * v.value )
console.log( JSON.stringify(v) )
v = iter.next( v.value * v.value )
console.log( JSON.stringify(v) )
v = iter.next( v.value * v.value )
console.log( JSON.stringify(v) )
View the full Bidirectional.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
In a previous blog on Promises, I talked about async / await missing in AppStudio 3.3 and Qt 5.12.1 and that you can vote to have it included it https://bugreports.qt.io/browse/QTBUG-58620.
I've read in other blogs that async / await is considered by some as syntactic sugar on Javascript generators yielding Promises.
I gave the following example demonstrating what this looks like with _asyncToGenerator() a function that works with Promises inside Javascript generators:
Button {
text: qsTr( "Test Promise async/await (Babel)" )
onClicked: {
_asyncToGenerator( function*() {
try {
showTitle( yield download( "GET", "https://appstudio.arcgis.com" ) )
showTitle( yield download( "GET", "https://community.esri.com/groups/appstudio" ) )
showTitle( yield download( "GET",
"https://community.esri.com/groups/survey123" ) )
} catch ( err ) {
console.log( "Caught Error: ", err.message )
throw err
}
} )()
}
}
View the full PromiseBabel.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
The following uses Promises to invoke the ArcGIS Online Search API iteratively to retrieve titles of over 200+ AppStudio samples:
Button {
text: qsTr("Search")
onClicked: {
_asyncToGenerator( function*() {
networkRequest.url = "https://www.arcgis.com/sharing/rest/search"
networkRequest.responseType = "json"
try {
let f = "pjson"
let q = "type:Native Application owner:appstudio_samples"
let start = 1
let num = 10
while (start !== -1 ) {
let resp = yield networkRequest.sendWithPromise( { f, q, start, num } )
let { nextStart, total, results } = resp.response
for ( let result of results )
console.log( result.title )
start = nextStart
}
} catch (err) {
console.log("Caught exception: ", err.message)
console.log(err.stack)
throw err
}
} )()
}
}
View the full ArcGISOnlineSearch.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
The following demonstrates how you can use Promises in a recursive manner to solve a maze:
Button {
text: qsTr("Solve Maze")
onClicked: {
_asyncToGenerator( function*() {
function solve(x, y) {
return _asyncToGenerator( function*() {
maze[y][x] = someDude
printDaMaze()
yield sleep(100)
if (x === endingPoint[0] && y === endingPoint[1]) return true
if (x > 0 && maze[y][x - 1] === free && (yield solve(x - 1, y))) return true
if (x < mazeWidth && maze[y][x + 1] === free && (yield solve(x + 1, y))) return true
if (y > 0 && maze[y - 1][x] === free && (yield solve(x, y - 1))) return true
if (y < mazeHeight && maze[y + 1][x] === free && (yield solve(x, y + 1))) return true
maze[y][x] = free
printDaMaze()
yield sleep(100)
return false
} )()
}
if (yield solve( startingPoint[0], startingPoint[1]) ) {
console.log( "Solved!" )
printDaMaze()
} else {
console.log( "Cannot solve. :-(" )
}
} )()
}
}
View the full MazeSolver.qml on GitHub. Try it now on AppStudioBlogIterators | WebAssembly.
Qt now offers a WebAssembly target.
This means it is possible to deploy QML applications in a web browser. WebAssembly is an experimental technology meaning it's an evolving platform. Expect a lot of things to not work. With the disclaimer out of the way, you can test all of the code snippets in this blog in a Web Assembly application here: https://stephenquan.github.io/AppStudio/Apps/AppStudioBlogIterators/
Send us your feedback to appstudiofeedback@esri.com
Become an AppStudio for ArcGIS developer! Watch this video on how to sign up for a free trial.
Follow us on Twitter @AppStudioArcGIS to keep up-to-date on the latest information and let us know about your creations built using AppStudio to be featured in the AppStudio Showcase.
The AppStudio team periodically hosts workshops and webinars; please click on this link to leave your email if you are interested in information regarding AppStudio events.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.