n-riesco / ijavascript

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

inspect() #59

Closed rgbkrk closed 8 years ago

rgbkrk commented 8 years ago

I'd love to see something similar to IPython's expectation of a _reprhtml, that IJavascript could recognize (for libraries to implement).

function X(x) {this.x = x;}
X.prototype.inspect = function inspect(){ return this.x.toString()}
X.prototype.inspectAsMimeBundle = function inspectAsMimeBundle() {
  return {
    'text/html': `<b>${this.x}</b>`,
    'text/plain': this.inspect(),
  }
}
> x = new X('woo')
woo

This would come out more like this (for frontends that can handle it):

screenshot 2016-01-28 10 18 51

Cool thing then is that you can extend common objects to produce rich representations as well.

We could make a function that returns single mimetypes (though the msg spec expects an entire mime bundle):

X.prototype.inspectAsMimetype = function inspectAsMimetype(mimetype) {
  switch(mimetype) {
    case 'text/html':
      return `<b>X = ${this.x}</b>`
    case 'text/plain':
    default:
      return this.inspect();
  }
}

Implementors can of course write their own functions for html or other mimetypes that they use in these two methods, I kept this encapsulated as a proposal.

jdfreder commented 8 years ago

+1

n-riesco commented 8 years ago

I need to experiment further with this idea. The main "con" is that:

> Object.getOwnPropertyNames(util.inspect)
[ 'length',
  'name',
  'arguments',
  'caller',
  'prototype',
  'colors',
  'styles' ]

In the meantime, similar, experimental functionality is already available through $$mimer$$. See:

http://n-riesco.github.io/ijavascript/doc/mimer.ipynb.html

rgbkrk commented 8 years ago

inspect is something you define on your own objects though that works in Chrome too, you don't have to use Node's util.inspect.

I must be missing what the con is.

n-riesco commented 8 years ago
> var obj = { inspect: function(){}}
undefined
> Object.getOwnPropertyNames(obj.inspect)
[ 'length',
  'name',
  'arguments',
  'caller',
  'prototype' ]
n-riesco commented 8 years ago

My first example of a con was a poor one. A function defines the properties:

[ 'length', 'name', 'arguments', 'caller', 'prototype' ]

This is not a terrible con. A quick solution would be to change the convention, instead of defining obj.inspect["text/plain"], we could use obj.inspect.mime["text/plain"].

rgbkrk commented 8 years ago

Ah, ok, that makes sense. How do you define these in a way that is natural for a JS programmer? I'm assuming we want these to be bound by the caller and I'm not sure how you do it for nested functions.

function X() {}
X.prototype.inspect.mime = {
  'text/html': function html() {
    // How does this get bound to the future `this`?
  }
}
rgbkrk commented 8 years ago
> X.prototype.inspect.mime['text/html'] = function html(){ return `<b>${this.x}</b>`}
[Function: html]
> new X(12)
ITS AN X: 12
> y = new X(12)
ITS AN X: 12
> y.inspect.mime['text/html']
[Function: html]
> y.inspect.mime['text/html']()
'<b>undefined</b>'
n-riesco commented 8 years ago

@rgbkrk How urgent is this issue for you? I was working on #58 , but if we agree on what to do for #59 , I could work on it right away.

rgbkrk commented 8 years ago

This is not urgent, working on #58 is great. @jdfreder was interested in the JavaScript kernel and since I saw the thread going along I wanted to experiment.

rgbkrk commented 8 years ago

Currently there's a compare and contrast with tonic dev and IJavascript in Plotly's plotly-notebook-js and I'd like to make IJavascript take off in the same way that IPython did via libraries like pandas.

n-riesco commented 8 years ago

Here's an alternative using a getter:

function X(x) {
    this.x = x;
}

X.prototype.inspect = function inspect() {
    return "X = " + this.x;
};

X.prototype.inspect.mime = {
    get "text/html"() {
        return "<b>X = " + this.x + "</b>";
    },
};

var x = new X(1);

x.inspect.mime["text/html"];  // output: '<b>X = undefined</b>'

The main con of using a getter is that we would give up passing arguments. For example, obj.inspect.mime["image/jpg"]("lossless") (admittedly, not a terribly good example).

rgbkrk commented 8 years ago

In Python at least, we never pass arguments to the _repr_html_. It's supposed to be a function that takes nothing and returns a string that we can send over the wire.

rgbkrk commented 8 years ago

Uhhhh, appears that getter doesn't work for us since it comes out undefined. Tested locally as well.

n-riesco commented 8 years ago

Oh well... using X.prototype.inspect.mime has an undesired side-effect. this gets assigned to X.prototype.inspect.mime instead of the X instance:

function X(x) {
    this.x = x;
}

X.prototype.inspect = function inspect() {
    return "X = " + this.x;
}

X.prototype.inspect.mime = {};

Object.defineProperties(X.prototype.inspect.mime, {
    "text/html": { get: function html() {
        return this === X.prototype.inspect.mime;
        },
    },
});

var x = new X(1);
x;  // output: X = 1
Object.getOwnPropertyNames(x.inspect.mime); // output: [ 'text/html' ]
x.inspect.mime["text/html"];  // output: true
n-riesco commented 8 years ago

Something like the following would be more in the spirit of the global $$html$$:

function X(x) {
    this.x = x;
}

X.prototype.inspect = function inspect() {
    return "X = " + this.x;
}

Object.defineProperty(X.prototype, "$$html$$", {
    get: function html() {
        return "<b>" + this.inspect() + "</b>";
    },
});

var x = new X(1);
x;  // output: X = 1
x.$$html$$;  // output:<b>X = 1</b>
rgbkrk commented 8 years ago

Considering how you have to send these across the wire, what if the interface is just:

X.prototype.inspectAsMimeBundle = function inspectAsMimeBundle() {
  return {
    'text/html': `<b>${this.x}</b>`,
    'text/plain': this.inspect(),
  }
}
rgbkrk commented 8 years ago

Alternatively, or in addition to (since these are for your own use in calling from ijavascript) they can implement inspectAsMimetype:

X.prototype.inspectAsMimetype = function inspectAsMimetype(mimetype) {
  switch(mimetype) {
    case 'text/html':
      return `<b>X = ${this.x}</b>`
    case 'text/plain':
    default:
      return this.inspect();
  }
}

var x = new X(1);
console.log(x.inspectAsMimetype('text/html')); 
n-riesco commented 8 years ago

In IJavascript, we also have the global $$mime$$:

Object.defineProperty(X.prototype, "$$mime$$", {
    get: function mime() {
        return {
            "text/plain": this.inspect(),
            "text/html": this.$$html$$,
        };
    },
});

x.$$mime$$;  //output: { 'text/plain': 'X = 1', 'text/html': '<b>X = 1</b>' }
rgbkrk commented 8 years ago

The main reason I'm aiming for something similar to inspect is that it's a built in. My big repulsion to the global $$html$$ is that it's on the user to run (or be detected by a library) and I wouldn't know how to use it in an asynchronous context.

jdfreder commented 8 years ago

Alternatively we could model the API closer to IPython's. The way this is done in IPython isvia reprs. Python itself uses x.__repr__ for the string representation of an object. IPython extends this by also looking for x._repr_*_, i.e. x._repr_html_...

In Javascript the equivalent of __repr__ is toString, right? So why not toHTML, toJavascript etc. Or toMimeBundle?

jdfreder commented 8 years ago
function A() {}
A.prototype.toString = function() { return 'b'; }
a = new A();
String(a)
"b"
rgbkrk commented 8 years ago

At least in node and v8 (which are what this kernel is), inspect is the equivalent to __repr__ (the analogue to toString is __str__). inspect has been around at least as far as 0.8 (was easy to launch a docker image with it):

$ docker run -it node:0.8
> X = function(x) { this.x = x; }
[Function]
> X.prototype.inspect = function inspect(){ return 'x = ' + this.x; }
[Function: inspect]
> trial = new X(23)
x = 23
> trial
x = 23
n-riesco commented 8 years ago

I've put together a document with all the display conventions mentioned here. I think it'll help understand the current conventions in IJavascript:

Display Conventions  
1. Javascript `toString()`  
2. Node.js `inspect()`  
3. Python `__repr__()`  
4. IPython `__repr_*__()`  
5. IJavascript  
5.1. Using `util.inspect()` (except for functions)  
5.2. Using globals: `$$mime$$`, `$$html$$`, ...  
5.3. Extensions:  
5.3.1. Using global `$$mimer$$()`  
5.3.2. Using Node.js `inspect()`  
5.3.3. Using IPython's `__repr_*__()`  
5.3.4. Using getters: `$$mime$$`, `$$html$$`, ...  

Display Conventions

Javascript toString()

In Javascript, a class can define its string representation by defining the method toString().

$ node
> function F() {}
undefined
> F.prototype.toString = function toString() { return "World!"; };
[Function: toString]
> "Hello, " + new F()
'Hello, World!'

See documentation here.

Node.js inspect()

Node.js introduced another convention. The string representation of an object in a Node.js shell is determined by the method inspect([depth]).

$ node
> function F() {}
undefined
> F.prototype.inspect = function inspect() { return "Hello, World!"; };
[Function: inspect]
> new F()
Hello, World!

See documentation here.

Python __repr__()

Similarly to Javascript, Python uses the method __repr__() to determine the string representation of an object.

See documentation here.

IPython __repr_*__()

And similarly to Node.js, the IPython shell introduces the methods __repr_*__() to define rich representations of an object.

See documentation here.

IJavascript

The sections below describe how IJavascript displays objects, and discuss potential extensions.

Using util.inspect() (except for functions)

With the exception of functions, IJavascript displays objects like Node.js does; i.e. by invoking util.inspect(), which, in turn, invokes the method inspect() of the object.

Functions, however, are displayed using the method toString(). See the difference:

$ node
> function f() { return "Hello, World!"; }
undefined
> f.toString()
'function f() { return "Hello, World!"; }'
> util.inspect(f)
'[Function: f]'

Thus, in IJavascript, it is possible to define an alternative string representation of an object by defining the method inspect() (or the method toString() in case of functions).

Using globals: $$mime$$, $$html$$, ...

The Jupyter notebook accepts object representations in a number of MIME formats. To produce these MIME representations, IJavascript defines a convention in terms of the global variables: $$html$$, $$svg$$, $$png$$, $$jpeg$$ and $$mime$$.

See documentation here.

The choice of names was guided by:

Extensions:

In this thread, we're discussing new conventions so that it is possible to customise an object representation based on its class.

Using global $$mimer$$

The global $$mimer$$ was experimentally introduced in NEL@0.1.1.

$$mimer$$ is the function that IJavascript uses to generate the MIME representation of an object. By making this function accessible, a library can redefine $$mimer$$ to produce a custom representation.

See here for some examples of use.

I want to deprecate this convention because of its cons:

Using Node.js inspect()

All the ideas we've discussed so far that extend the inspect() convention have serious drawbacks:

Using getters: $$mime$$, $$html$$, ...

From the experience with X.prototype.inspect["text/html"] and X.prototype.mime["text/html"], it is clear that for this to be properly defined a method or a getter must be used to generate a rich representation of an object. Once this is agreed, the issue becomes a naming issue. The first choice that came to my mind was to use the names already used in the global variables. The pros are:

Using IPython __repr_*__()

Jonathan suggested an alternative naming convention, inspired by IPython. The advantage I see is that:

The cons are:

rgbkrk commented 8 years ago

Thank you so much for putting this together @n-riesco, this helped me understand the whole picture quite a bit more.

rgbkrk commented 8 years ago

The major con to me of using _repr_*_ is that it should appeal to and be idiomatic for the kernel's language.

It's worth pointing out that the global variables do have an analogue to IPython as well, IPython.display.display(X) and helpers like IPython.display.HTML('<b>test</b>').

mdtusz commented 8 years ago

This may be a silly question, but what's the issue with just using a prototype method that takes an argument?

$ node
> function X(n){
    this.y = n; return this; 
  }
> X.prototype.inspectMime = function(t){ 
    var types = {
      'text/html': '<h1>' + this.y + '</h1>'
    };
    return types[t];
  }
> var x = new X(100);
undefined
> x
X { y: 100 }
> x.inspectMime('text/html')
<h1>100</h1>
rgbkrk commented 8 years ago

I like inspectMime

n-riesco commented 8 years ago

@mdtusz Kyle suggested something very similar here.

I feel wary of using unmangled names like inspectAs*(). inspect() is fine because it's a Node.js convention.

I think it's easier for IJavascript to introduce a convention with mangled names, that are unlikely to clash with other naming conventions.

n-riesco commented 8 years ago

I'm preparing a new release in the async-stdio branch, here and here.

Apart from improving the handling of asynchronous output, it also includes an initial implementation of a convention to fix this issue: Class.prototype.inspect, Class.prototype._toMime, Class.prototype._toHtml, Class.prototype._toSvg, Class.prototype._toPng and Class.prototype._toJpg.

Here's an example:

In[1]:
function F(f) {
    this.f = f;
}

F.prototype.inspect = function inspect() {
    return "F { f: " + this.f + " }";
}

F.prototype._toHtml = function toHtml() {
    return "<div style='background-color:yellow'>" + this.inspect() + "</div>";
}

new F("Hello, World!");

Out[1]:
{ mime: 
   { 'text/plain': 'F { f: Hello, World! }',
     'text/html': '<div style=\'background-color:yellow\'>F { f: Hello, World! }</div>' } }
mdtusz commented 8 years ago

Nice!

So to clarify, one of the mime properties will be displayed as output then - presumably defaulting to text/html if provided?

n-riesco commented 8 years ago

@mdtusz My understanding is that the frontend chooses amongst all the available. For example, the notebook prefers text/html over text/plain.

rgbkrk commented 8 years ago

The frontend chooses once it gets the display_data or execute_reply message.

Example from Pandas:

screenshot 2016-03-04 13 39 24

The raw messages that the kernel (JavaScript in this case) sends over look like this:

{ 
  content:
   { data:
      { 'text/plain': '                   A         B         C         D\n2000-01-01  1.153881 -0.462802 -1.304602 -0.112967\n2000-01-02  2.041548  0.193145 -2.154538 -0.026075\n2000-01-03  2.277393 -0.990363 -1.676727  0.463335\n2000-01-04  2.472088  0.376239 -2.691538 -0.329923\n2000-01-05  3.426450 -0.052412 -3.275203 -1.422521',
        'text/html': '<div style="max-height:1000px;max-width:1500px;overflow:auto;">\n<table border="1" class="dataframe">\n  <thead>\n    <tr style="text-align: right;">\n      <th></th>\n      <th>A</th>\n      <th>B</th>\n      <th>C</th>\n      <th>D</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>2000-01-01</th>\n      <td> 1.153881</td>\n      <td>-0.462802</td>\n      <td>-1.304602</td>\n      <td>-0.112967</td>\n    </tr>\n    <tr>\n      <th>2000-01-02</th>\n      <td> 2.041548</td>\n      <td> 0.193145</td>\n      <td>-2.154538</td>\n      <td>-0.026075</td>\n    </tr>\n    <tr>\n      <th>2000-01-03</th>\n      <td> 2.277393</td>\n      <td>-0.990363</td>\n      <td>-1.676727</td>\n      <td> 0.463335</td>\n    </tr>\n    <tr>\n      <th>2000-01-04</th>\n      <td> 2.472088</td>\n      <td> 0.376239</td>\n      <td>-2.691538</td>\n      <td>-0.329923</td>\n    </tr>\n    <tr>\n      <th>2000-01-05</th>\n      <td> 3.426450</td>\n      <td>-0.052412</td>\n      <td>-3.275203</td>\n      <td>-1.422521</td>\n    </tr>\n  </tbody>\n</table>\n</div>' },
     execution_count: 21,
     metadata: {} },
  header:
   { username: 'kyle6475',
     msg_id: 'e697b50a-9aa0-4d7a-a825-27ac99479679',
     msg_type: 'execute_result',
     version: '5.0',
     date: '2015-07-26T15:52:59.473133',
     session: 'c297c1c0-89bb-4ded-9720-d298c33b2595' },
  parentHeader:
   { username: 'kyle6475',
     msg_id: '841e2314-0af6-46c8-8083-d4b544f91e0e',
     version: '5.0',
     session: 'b4e75019-83e8-4bd4-99f5-e462d5203886',
     date: '2015-07-26T15:52:59.468333',
     msg_type: 'execute_request' },
  metadata: {},
  signature: '6c41e0ecbbb50010c26d8e04e8ff2c634816761f610b30ab1765539c216be591',
  blobs: []
}

and a frontend will choose how to wrap those up accordingly

screenshot 2016-03-04 13 41 02

n-riesco commented 8 years ago

I've made a pre-release with the changes in the async-stdio branch.

I will keep this issue open until I make the release.

If you have any more feedback that I can address before the release, please, keep posting here.

n-riesco commented 8 years ago

I've just released IJavascript@5.0.11, which uses NEL@0.4.0, and fixes this issue.

rgbkrk commented 8 years ago

Awesome, I look forward to installing and using this.