n-riesco / ijavascript

IJavascript is a javascript kernel for the Jupyter notebook
Other
2.19k stars 185 forks source link

Document use of `$$.display()` #95

Open n-riesco opened 7 years ago

n-riesco commented 7 years ago

Moved from https://github.com/n-riesco/nel/issues/4

rgbkrk commented 7 years ago

So there might be quite a lot to though about if we want something completely async, one other is the cancellation of task without affecting subsequently submitted tasks.

You don't really want to handle that case. This would mean that frontends would have to provide means for users to declare task dependencies. Do you have an use case in mind for this functionality?

The biggest use case right now is cluster computing where you'll typically be running N jobs + stages of jobs -- spark, dask, etc. Same applies for node developers using Eclair for Spark.

n-riesco commented 7 years ago

@gnestor

OK, if we go with $$.display(id?), this is how the display_id vs no display_id cases would look like:

// No `display_id` case
// (only `display_data` messages)

// In[1]

// append a display output to cell 1
var display1 = $$.display();
display1.text("tic"); // sends a `display_data` with no `display_id` from cell 1

// In[2]

// append a display output to cell 1 from code run in cell 2 but pretending to be from cell 1
display1.text("tac"); // sends a `display_data` with no `display_id` pretending to come from cell 1

// append a display output to cell 2
var display2 = $$.display();
display2.text("toe"); // sends a `display_data` with no `display_id` from cell 2
// `display_id` case
// (`$$.display(id) sends an empty `display_data` that registers the `display_id` with the frontend)
// (`display.text(data)` sends `update_display_data` messages)
// (in this way, IJavascript doesn't need to keep a table of `display_id`'s)

// In[1]

// append a display output to cell 1
var tic1 = $$.display("tic");
tic1.text("tic"); // sends a `display_data` with `display_id: "tic"` from cell 1

// append a display output to cell 1
var tac1 = $$.display("tac");
tac1.text("tac"); // sends a `display_data` with `display_id: "tac"` from cell 1

// In[2]

// replace display output in cell 1 from code run in cell 2 pretending to be from cell 1
tic1.text("TIC"); // sends a `update_display_data` with `display_id: "tic"` pretending to come from cell 1

// replace display output in cell 1 from code run in cell 2
var tac2 = $$.display("tac");
tac2.text("TAC"); // sends a `update_display_data` with `display_id: "tac"` from cell 2
n-riesco commented 7 years ago

@rgbkrk

The biggest use case right now is cluster computing where you'll typically be running N jobs + stages of jobs -- spark, dask, etc. Same applies for node developers using Eclair for Spark.

But in Spark's case, this is hidden from the frontend, isn't it? The user runs a command and the command doesn't complete until all the jobs complete or an exception is triggered.

I probably misunderstood Matthias (I thought by execution tasks he meant the execution of a bunch of cells).

n-riesco commented 7 years ago

@gnestor

Here's a preliminary TODO list of what needs doing to implement display_id:

  1. In the NEL module:

    • [x] Pin testing to avoid regressions (I'm already working on this).
    • [x] Split nel_server.js into smaller files (this file implements IJavascript's Node session as an IPC server; this file was fairly small in the beginning, but after the introduction of $$ has grown to a point that it'll be easier for others to understand if I split it into smaller files).
    • [x] Implement $$.display(id?) in nel_server.js.
    • [x] Update Session#execute to accept an onDisplay handler.
    • [x] Add tests for $$.display(id?).
  2. In the jp-kernel module:

    • [x] Update the execute_request handler to register an onDisplay handler.
    • [x] Implement the onDisplay handler (this is the code that will send display_data and update_display_data messages).
  3. In the jp-Babel kernel:

    • [x] Use the latest jp-kernel (I updated jp-CoffeeScript last week, so I want to do jp-Babel while it's still fresh in my mind)
  4. Implement module ijavascript-ipython-display to replicate the API in IPython.display

    • [ ] This module will use $$.display(id?) under the hood.

I hope this gives us a good plan of action.

I'll ping you back once I'm done splitting nel_server.js (I really think that this file split will make things easier; I should've already done it).

gnestor commented 7 years ago

@n-riesco Great! Sounds good to me. I can start playing around with jp-kernel part in the meantime.

n-riesco commented 7 years ago

@gnestor I'm not sure how far you can go playing with jp-kernel without knowing the details of the onDisplay handler.

I have a suggestion, why don't you start with the implementation of $$.display(id?):

Here's an example of how this mock'd look like:

// In[1]:
var display = require("nel-display-mock");

// In[2]:
var displayInCell2 = display();
displayInCell2.text("tic");  // in mock this could result in `console.log('display_data:', id, mime)`

// In[3]:
var displayInCell3 = display("cell3");
displayInCell3.text("tic");  // in mock this could result in `console.log('display_data:', id, mime)`

// In[4]:
displayInCell3.text("tac");   // in mock this could result in `console.log('display_data:', id, mime)`

Please, have a look at the coding guidelines. To those, I'd add:

gnestor commented 7 years ago

@n-riesco Am I on the right track?

// In[1]
function display(id) {
    this.id = id || '';
    // Mock for `this.send`
    this.send = console.log;
    return {
        text: function sendText(text, keepAlive) {
            this.send({
                displayId: this.id,
                mime: {
                    "text/plain": text,
                },
                end: !keepAlive,
            });
        }.bind(this)
    };
}

// In[2]
var displayInCell2 = display();
displayInCell2.text("tic");

//  { displayId: '', mime: { 'text/plain': 'tic' }, end: true }

// In[3]
var displayInCell3 = display("cell3");
displayInCell3.text("tic");

// { displayId: 'cell3', mime: { 'text/plain': 'tic' }, end: true }

// In[4]
displayInCell3.text("tac");

// { displayId: 'cell3', mime: { 'text/plain': 'tac' }, end: true }

This looks an awful lot like the Context function in nel... I guess that's sorta the point.

n-riesco commented 7 years ago

I hope you won't mind me teasing you a little bit. ;)

I don't think that code does what you expect. Try the following after running it:

// In[5]
id

// Out[5]
'cell3'

We really want display to return a new instance (amongst other reasons so that we can use the operator instaceof). This is what I mean in pseudo-code:

function display(id) {
    return new Display(id);
}

function Display(id) {
    this.id = id;
}

Display.prototype.text = function text(text) {
    // ...
}
gnestor commented 7 years ago

I'm not much of a OOPer 😔 But I see what you mean and your example makes sense:

function Display(id) {
    this.id = id || '';
    this.send = console.log;
}

Display.prototype.text = function text(text) {
    this.send({
        displayId: this.id,
        mime: {
            "text/plain": text,
        }
    });
}

Display.prototype.mime = function mime(bundle) {
    this.send({
        displayId: this.id,
        mime: bundle
    });
}

Display.prototype.text = function text(text) {
    this.send({
        displayId: this.id,
        mime: {
            "text/plain": text,
        }
    });
}

Display.prototype.html = function html(html) {
    this.send({
        displayId: this.id,
        mime: {
            "text/html": html,
        }
    });
}

Display.prototype.svg = function svg(svg) {
    this.send({
        displayId: this.id,
        mime: {
            "image/svg+xml": svg,
        }
    });
}

Display.prototype.svg = function png(png) {
    this.send({
        displayId: this.id,
        mime: {
            "image/png": png,
        }
    });
}

Display.prototype.jpeg = function jpeg(jpeg) {
    this.send({
        displayId: this.id,
        mime: {
            "image/jpg": jpeg,
        }
    });
}

function display(id) {
    return new Display(id);
}
n-riesco commented 7 years ago

That's the idea. That code would go into nel_server.js (probably in its own file after I split nel_serve.js).

nel_server.js and nel.js communicate via IPC. Here's a list of the current message types. We need to add new message type for Display. How about this?


/**
 * Message received from the session server
 *
 * @typedef Message {
 *     module:nel~LogMessage |

 *     module:nel~DisplayMessage |

 *     module:nel~StatusMessage |
 *     module:nel~StdoutMessage |
 *     module:nel~StderrMessage |
 *     module:nel~ErrorMessage |
 *     module:nel~SuccessMessage
 * }
 */

/**
 * Display message received from the session server
 *
 * @typedef DisplayMessage
 *
 * @property {number}  [id]    Execution context id (deleted before the message reaches the API user)
 * @property           display
 * @property {string}  [display.id]    Display id
 * @property           display.mime
 * @property {string}  [display.mime."text/plain"]    Result in plain text
 * @property {string}  [display.mime."text/html"]     Result in HTML format
 * @property {string}  [display.mime."image/svg+xml"] Result in SVG format
 * @property {string}  [display.mime."image/png"]     Result as PNG in a base64 string
 * @property {string}  [display.mime."image/jpeg"]    Result as JPEG in a base64 string
 *
 * @private
 */

In this way the code you wrote now would look like:

function Display(id, context) {
    this.id = id || '';
    this.context = context;
}

Display.prototype.text = function text(text) {
    this.context.send({
        display: {
            id: this.id,
            mime: {
                "text/plain": text,
            }
       }
    });
}

// ...
n-riesco commented 7 years ago

One of the benefits of $$ and Display having the same API is that wrappers like ijavascrip-plotly can accept either a cell or a display output.

gnestor commented 7 years ago

Looks good to me :+1: