Closed lmccart closed 11 years ago
Can the load json function be async? Because in that case the callback option is more versatile.
On Oct 22, 2013, at 2:44 PM, Lauren McCarthy notifications@github.com wrote:
two options for redesign:
var data; loadJSON("file.json", data);
var data; loadJSON("file.json", function(resp) { data = resp; });
— Reply to this email directly or view it on GitHubhttps://github.com/lmccart/processing-js/issues/81 .
we could support both. simple version for beginners, callbacks for those that want to use them!
i think that the second option should be definitely supported (and a Promise-like option even too much!).
the first one looks ok for absolute js beginners, but is confusing too. a sketch could simply not work (or work in unexpected ways), even if it was "officially" correct. this can be even more frustrating for a beginner, than learning how things really ARE in the wonderful async world..
inspired by the solution used in Processing.js, i was thinking that beginners could be invited to define a list of files to load at initialization: within setup() or before, in a dedicated function like preload()
(?). this kind of solution should limit unexpected behaviors for async operations and keep things simple.
// i'm totally ignoring globals/scope problems in this example
function preload() {
var data = loadJSON("file.json");
var xml = loadXML("file.xml");
var img = loadImage("file.jpeg");
var txt = loadStrings("file.txt");
var anyfile = load("http://www.google.com/");
}
setup() {
}
draw() {
}
Sorry for my totally naive question, but what would be the difference between having preload() and calling those functions in setup() itself? I love, btw, the option of having non-async versions for loading at the beginning and fancier async versions for advanced.
I was wondering this also. I guess the case would be when you want to do 3 things in order. 1. Load files, 2. use data to initialize variables, 3. draw using initialized variables.
Not sure this warrants an extra preload method though, maybe this is a use case where you'd use a callback function, loading the files in setup.
My understanding is that the idea behind the preload method is that it would behind the scenes treat setup as the callback. So when everything in preload was done, setup would fire, then draw etc.
Maybe i've just been coding javascript for too long, but despite the ease of learning, i think sync versions of those functions will cause more harm than good. The whole page will freeze while waiting, and that wait could be a long time depending on the network and the size of the asset. Perhaps another route is to set a field on the var to true when the data is available?
var myImage;
function setup() {
myImage = loadImage("foo.jpg");
}
function draw() {
if (myImage.available) {
// do stuff with myImage
}
}
It is a little less straightforward for json and strings because once those are ready you'd probably want to init some stuff just one time. But since those functions are exactly for beginners, perhaps it is ok to force them to either specify a callback, or use a built in one ala movieEvent in the Movie lib.
@shiffman and @lmccart I don't know how you want to stop code execution while you load files in the "beginner way". Am i missing something? Javascript doesn't wait and you'll have to stop it somehow. So if the user wants to use data from the files in setup already, i don't see how it would work exactly without a callback.
@brysonian Promise-like solution looks very easy to use actually. You are right: the problem is again that files loaded in setup won't be ready until draw is called. Concerning the user experience (and to avoid panic), probably a loading icon should be shown, while the application is "freezed".
You can make sync requests with XMLHttpRequest, but it is frowned upon.
True, this could be used in setup() actually. Unintended things could happen though, like the application freezing in the middle of something, but probably this is not too bad, compared to the advantage of simplicity.
Processing.js solved this by introducing something similar to compiler-flags called directives: http://processingjs.org/reference/preload/
These are parsed out by the precompiler and the file(s) are loaded before the sketch is started. I'm not much infavour of these and as we don't have a precompiler they are off the table anyways i guess ..
Are there any plans for a build system? .. if so this problem could easily be fixed by turning the data into js files and have them be "loaded" that way:
// in fancy.json.js which is generated by build system from file fancy.json
var preload = preload || {};
preload['fancy.json'] = '{"my":"json","string":"here"}';
// then somewhere in the sketch
loadJSON('fancy.json'); // first searches preload['fancy.json'], otherwise attempts to load file ...
This is how brunch.io does it with it's require() statements for template data ... they even concatenate all generated js "files" into one large "database" for us this could be:
// build a virtual file system inside the object:
var preload = preload || {};
preload['data/first.json'] = '...';
preload['data/other.json'] = '...';
preload['other-dir/other_file.json'] = '...';
@fjennet probably a very stupid question: the build system you describe works in a browser? What's the difference between the build system and a sync or async request made at any time in a sketch? I am missing something.
No, good question. A build / assembly system would run before you push files onto your server (FTP) or as part of a preview step (maybe in the PDE). For example when you play a sketch in the JavaScript mode in the Processing PDE before the project opens in a browser files will be copied together and precompiled, etc. Another example would be something like brunch.io that does something similar (assembles files, concatenates and minifies them and packages the project).
So the difference then would be that the content to load would / could already be loaded at the moment any of the loadXXX() methods is being run. ... and yes, that would only work if you know which files are being loaded at the time the project is assembled. Does not work for external resources for example.
Ok, thank you for the explanation. I see now what you mean. Interesting to share optimized standalone applications made in js. But as you say it won't work with external resources loaded during runtime.
The simple version won't work because when you do loadJSON("file.json", data)
you are passing in undefined
and you won't be able to modify the external data
reference from within loadJSON()
.
Here are some thoughts regarding each proposal so far:
Callbacks:
Because it's async, before the callback is fired additional draw loops will run when data
is still undefined
. So you either have to handle all the possible exceptions, or force users to embrace the concept of callbacks, get a grasp of single-thread event loop and async i/o (which isn't necessarily a bad thing) and eventually learn to deal with the pyramid of doom.
Maybe you can make setup()
async:
function setup(done) {
// manually handle callbacks
// and call done() when all callbacks are fired?
}
Preload Assuming the syntax is resolved, this gives the smoothest learning curve for beginners as they don't even need to be aware of the async stuff happening under the hood. Consider a syntax like this:
var data;
function preload () {
data = loadJSON('data.json');
}
The idea here is before actually calling preload
, you can parse the content from preload.toString()
and sniff out the things to preload. One minor drawback of this method is that you can't deal with dynamically created filename strings because they need to be visible in the function's source. So if you want to, say, load img01.jpg
to img05.jpg
you have to explicitly list them all.
An alternative solution is instead of directly including sketch.js
, you load it in pjs.js
:
<script src="pjs.js" data-sketch="sketch.js"></script>
When you load the sketch via XHR you can parse the content of the file and preload things needed. Better yet you can even omit the data-sketch
bit if the sketch is using the default filename.
RE: build step ...
Imagine there is something like the current "data" folder ("data-preload"?) and all files in there will be automatically preloaded before your sketch is running (build tool just loops files and generates a preload-list). Then even dynamic names will work and you would not have to write them (any) out.
I think tooling is needed anyways. No?
@fjenett ooh this is a nice idea
@fjenett this would require access to local file system to scan what files exist, so it has to be a legit build step (like setup build/watch via grunt) instead of a runtime thing (save -> refresh). Personally I have no problem with a build step, but that might be too much for a beginner to jump through just to load an external file, unless you have a simpler workflow in mind.
@yyx990803 yes you would need file access.
I think for beginners it needs something like the PDE or similar that will allow for easy sketch creation / export. So with this in place it would be super easy to add that little extra build step. ... i'm not talking about grunt or similar here, but rather a really simple application that only creates / lists / runs and exports p5.js sketches, not a full editor.
@fjenett
I see, that make sense - so I guess for those who use their own editor we can assume they are capable of manually handling callbacks?
Mmm. I wonder a few things about the goals:
Personally, I think what makes web technology great is that it's an open standard and you can author in any environment/editor you want, and your shipped code will run in any modern browser. I feel that binding the library to a specific IDE takes that advantage away. That's why I'm all for the run-time parsing: it allows the user to write in whatever editor they like and not rely on any additional tools but the browser.
I don't think making an IDE (again) is a good idea and i was not talking about that. What i had in mind was a simple tool that would:
No more no less. Something like the JavaScript mode as standalone.
But ... as this is getting far away from the original async question and issue we should maybe move some place else for discussion?
Yes sorry, we moved away from the question. The build tool is a big subject, since js is evolving quickly and we could probably even imagine to build html5 apps, but also desktop node-WebKit apps or mobile apps with pjs one day... Don't know, just absurd ideas maybe. Anyway it definitely deserves a separate discussion, since it is not directly related to the API and we risk to mix too many things.
@yyx990803 Could you explain how you imagine the "run-time parser" of a sketch? Probably I'm naive and did not understand something, but wouldn't it be too complex to code and heavy to maintain? What I understand is that you would first load the sketch as a string, find the "loadFile" functions (json, image, xml, whatever), get the requested files, apply their values somehow and finally execute the sketch by calling setup()
, etc... Is that right?
For example, would it work in a situation like this?
var file = [];
for (var i=0; i<5; i++) {
file[i] = loadJSON("data" + i + ".json");
}
@taseenb basically, yes. It could be as simple as a RegExp match, but cannot deal with the situation you mentioned (dynamic filenames).
I see. Still think that a preload
function could be the simplest: simple for the users (and not too hard for the developers to implement and maintain), and still powerful enough in most cases (dynamic filenames could work).
Callbacks (maybe using @brysonian idea: https://github.com/lmccart/processing-js/issues/81#issuecomment-27189486 ) could be used by advanced users, but the two options (callbacks and preload) would be consistent and the behavior of the API easily predictable.
var file = [], xml, img;
function preload() {
// here you are free to do whatever you want - not only call the load functions...
var rnd = parseInt(Math.random() *1000);
xml = loadXml('file' + rnd + '.xml');
for (var i=0; i<5; i++) {
file[i] = loadJSON("data" + i + ".json");
}
img = loadImage('pic.jpg');
}
Yes to callbacks and the preload() from my side. Although i think the preload should work differently. It only should return a list of items to preload, but not allow to already start working with the data (API should maybe not be available?). That is because without synchronous call returns setting variables like above won't work (without using promises and the like) and if it would there would be no need for the preload.
preload ( files ) {
for ( var i = 0; i < 100; i++ ) {
files.push( "fancy-"+i+".json" );
}
return files;
}
The resulting array could easily be merged with other resources (maybe from the build step) that need to be loaded, there could be something like a progress callback ... and proper error reporting.
Bringing up error reporting, there should be two callbacks: loadXXX( url, success, error)
Yes, sorry: that way wouldn't work. But if you simply use a string to set the name of the global variable, it would, almost exactly the same way:
var text;
function preload() {
load('text.txt', 'text');
}
Hm, that would be an addition to the API then ... no? I think the objective was to decide on how to handle the loadXXX() methods so that they behave similar to the Java version.
I like @fjenett 's preload method because I don't think it's a good idea to limit preload data to global variables only. I'm not sure why we need to pass in files
as an argument though - simply returning an array seems to suffice.
I think I'm in favor of something like:
var data, img;
function preload() {
loadJSON("file.json", data);
loadImage("cat.jpg", img);
}
Maybe only loadXXX methods would work in here. This would work if we wrote the load methods right, wouldn't it?
@lmccart the data
and img
you are passing in here are both undefined
at the time the functions are called, so you won't get a reference to the actual variables defined outside the function. If you pass in strings instead, e.g. "data"
, then you can do window["data"] = ...
, but that would force all preload variables to be global and in general is not a good idea.
@yyx990803 is right. Variables cannot be passed because javascript does not allow to pass them by reference... We should pass a string with the name of the variables...
Check this working Gist (very rough): https://gist.github.com/taseenb/7284649
var text;
function preload() {
load('text.txt', 'text');
}
function setup() {
console.log(text);
}
Any ideas on how to avoid only globals or allow arrays, in a simple way for the user?
@fjenett It does not look a big addition. It's pretty simple. No?
oh sorry, I wasn't thinking. @taseenb I didn't quite understand why your earlier example wouldn't work though? https://github.com/lmccart/processing-js/issues/81#issuecomment-27630235
couldn't we do something like a manual promises...counting up each load call and not proceeding to setup until all things have loaded? this just seems most straightforward, rather than having to pass a string with the var name. but maybe I'm missing something?
@lmccart That example does not work because there is no way to get a reference to those variables. We don't know what the user is doing inside preload()
: we can just determine whether preload()
exists (and is a function) and if the user has called the load()
function. But we don't know the name of the variables, so we don't know where to put the data being loaded.
This one works though: https://github.com/lmccart/processing-js/issues/81#issuecomment-27635147 (it's not exactly the same, but probably the closest).
@yyx990803 You are totally right about globals. But at the moment everything is global. This will probably change in the future.
@taseenb yes - I actually really don't like exposing everything globally as that encourage bad practice for JavaScript beginners and introduces potential naming clashes when using Pjs with other libraries.
@taseenb @yyx990803 yes definitely agree! the plan is to switch away from that. or at least to offer two options so beginners may use global namespace but you can have the option to use scoped namespace instead.
@taseenb sorry, I'm still confused. why do we need a reference to the variables in your earlier example? couldn't we increment a count with each loadimage or loadjson call and decrement on each return? then go on to setup once count is at 0 again?
@lmccart consider this:
User's code:
var data = load('file.json')
Library code:
function load (file) {
// do the XHR request here...
xhr.onload = function () {
// because we have no idea what variable name
// the user used, how can we assign the loaded value?
}
// what do we return here? we don't have anything yet.
// the user expects actual data, not a promise.
}
On the other hand - if we do this:
User code:
var a, b
function preload () {
return ['a.json', 'b.json', 'c.json']
}
function setup () {
a = loadJSON('a.json')
b = loadJSON('b.json')
// local variable works too
var c = loadJSON('c.json')
}
Library code:
var files = preload(),
total = files.length,
preloadedFiles = {}
files.forEach(function (file) {
// do xhr request ...
xhr.onload = function () {
// implementation here depends on file extension
preloadedFiles[file] = JSON.parse(xhr.responseText)
total--
if (total === 0) {
// we are ready
setup()
}
}
})
function loadJSON (file) {
return preloadedFiles[file]
}
@lmccart In the gist https://gist.github.com/taseenb/7284649 this is exactly what i do: count each load()
and wait until they are finished. This is ok.
But the problem comes with variable names set by the user: how do we get those names and put the right data into the right variable? Not simple.
Of course, you could get the body of preload()
and use regular expressions to find variable names (something close to what @yyx990803 suggested some time ago): i tried it, but in my opinion it wasn't nice. The cleanest code i found that works for sure, for the moment, is the one passing the variable names as strings. Only for the moment!
@yyx990803 That looks VERY clean. Very similar to the last idea of @fjenett but only now I understand it.
To be even simpler and avoid that anti-beginner return
it could also be:
preload = ['a.json', 'b.json', 'c.json'];
var a, b, c;
function setup() {
a = loadJSON('a.json')
b = loadJSON('b.json')
c = loadJSON('c.json')
}
perhaps
preload('a.json', 'b.json', 'c.json');
Better expresses that this is an action (function call) whereas the preload array variable feels a bit magical.
Another option is that the user defines a preload function if they want it:
function preload() {
var dataA = loadJSON('a.json');
var dataB = loadJSON('a.json');
}
And the runner in P5.js on init:
looks to see if that function is defined, if so loadJSON is set to a preload version which: incrememts a counter for the number of preloads required returns a P5JS.JSONObject instance with a callback set to an internal method that counts successful preloads when the count is reached calls setup() else loadJSON is set to the "normal" version which requires that the user define a callback herself.
The java version of processing returns an object from loadJSONObject and loadJSONArray with getters for properties:
JSONObject json = loadJSONObject("data.json");
int id = json.getInt("id");
String species = json.getString("species");
String name = json.getString("name");
So the P5JS.JSONObject would behave the same way. Perhaps with an added getter for the raw JS data structure. This technique avoids the jiggery-pokery of parsing the function body as a string and allows for code-generated file and variable names.
... i think it would be very confusing for beginners if the loadXYZ() methods would behave differently in different locations.
Returning an array from preload would
@yyx990803 ... files
argument so you don't have to create that yourself as beginner. For convenience.
The question of what the loadXYZ return is another thing and i think not related to the preload / async question. My view is that the purpose of this project is to assess JS as a main language and therefore i don't see why we should mimic Java here.
But what you are proposing also has the loadXXX functions behaving differently too. In your case if preload has returned a list of files, those have been loaded and stuffed into an array that mimics a file system, and subsequent calls to the loadXXX functions then look in that array for the data. So if my preload() was:
function preload() {
return ['a.json', 'thumbnail.jpg'];
}
And then i had:
var thumb;
var data;
var otherImg;
function setup() {
data = loadJSON('a.json');
thumb = loadImage('thumbnail.jpg');
otherImg = loadImage('other.jpg');
}
So calls to loadImage would behave differently in the same place which is even more confusing.
My suggestion is that the API make the promise that any async calls in the preload function are guaranteed to have completed by the time setup is called, that is all.
Could you guys write a simple working gist of your propositions, if you have a little time? I think that would be of great help to see how it would work.
Yes absolutely. I'm tied up until this evening. But I'll put it together then.
@brysonian i don't think it' confusing because for otherImg that would return null and eventually throw an error. If you want to use additional resources that you did not state in preload() then you should use requestXYZ(): http://processing.org/reference/requestImage_.html ... which is there in the API for async loading.
Sorry, no time to work on an example of this, at least not any time soon.
two options:
var data; loadJSON("file.json", data);
var data; loadJSON("file.json", function(resp) { data = resp; });