jscl-project / jscl

A Lisp-to-JavaScript compiler bootstrapped from Common Lisp
https://jscl-project.github.io
GNU General Public License v3.0
884 stars 109 forks source link

Creating initialized/anonymous Javascript object #178

Open helmutkian opened 9 years ago

helmutkian commented 9 years ago

Is there a way to create an initialized or anonymous Javascript object akin to the results of using the object literal syntax in Javascript?

I've been searching around in the source and haven't found anything suitable yet. I have been using the following code as a work around for now

(defun create-js-object (&rest init-list)
  (let ((object (cl::new)))
    (do ((tail init-list (cddr tail)))
      ((null tail) object)
        (setf (cl::oget object (first tail))
        (second tail)))))

So that

(create-js-object "a" 1 "b" 2)

yields a Javascript object equivilent to

{ 'a': 1, 'b': 2 }

davazp commented 9 years ago

I think we do not have anything like this.

We should provide this and some other helper functions. They can be defined in src/ffi.lisp. I would like to discuss the API it should have, but having a few of functions would be a good starting point.

So, if you want you can create a pull request for it.

I will also try to create a jscl/ffi package for that, so we do not need to use cl::.

helmutkian commented 9 years ago

That sounds good. I can start by contributing a few functions I would find useful.

I have a few questions to help me along the way.

Is there a way to treat JavaScript functions as data and pass them to higher order functions?

Why does this work:

(funcall #j:alert "test")

But this doesn't

(funcall #j:console:log "test")

Furthermore, is there a way to capture the implicit JavaScript variable this? Using (cl::%js-vref "this") always captures window.

The following also does not work, if I define a function in JavaScript:

function f() { return this.a; }
(defvar *o* (cl::new))
(setf (cl::oget *o* "a") 22)
(setf (cl::oget *o* "g") #j:f)
(funcall (cl::oget *o* "g"))
=> NIL ;; Should return 22
helmutkian commented 9 years ago

Is there a way to merge this issue with #6 ?

davazp commented 9 years ago

Is there a way to treat JavaScript functions as data and pass them to higher order functions?

What you did is correct. The problem is the capture of this as you said. So this will also fail (chromium at least)

var x = console.log;
x("test")

The method call syntax (#j:console:log "test") does work, however, because we take care of preserving this. If you want to bind the this, you can call call or apply method, just like in Javascript:

CL-USER> (lambda (x) (#j:console:log:call #j:console x))
CL-USER> (funcall * "test")

Of course, this is ugly.

So we should decide how we want the FFI to work:

  1. Is it okay to bind this automatically, so (#j:obj:method ...) is always equivalent to (funcall #j:obj:method ...)? Does it have any bad consequence for Javascript interoperability? If we want to go this way, we should provide a mechanism to override this.
  2. Instead, we prefer to simplify how to bind this, providing a bind macro, like: (bind #j:console:log). And it could also be extended to bind parameters,

Thoughts?

helmutkian commented 9 years ago

I like the second option because leverages bind already in JavaScript.

(funcall (#j:console:log:bind #j:console) "test")

Similarly my example from above works with this mechanism too

function f() { return this.a; }
(defvar *o* (cl::new))
(setf (cl::oget *o* "a") 22)
(setf (cl::oget *o* "g") #j:f)
(funcall ((cl::oget *o* "g" "bind") *o*))
=> 22

Now this works too

(defun log-test (log-fn)
  (funcall log-fn "test"))

(log-test (#j:console:log:bind #j:console))
(log-test #'print)

All we then need is a bind macro, as you said, to simplify this syntax so the previous could be rewritten

(funcall (bind (cl::oget *o* "g")))
(log-test (bind #j:console:log))

There is finally, one last thing: how to capture this when defining a function in JSCL? I would like to be able to write this in JSCL:

(defun f ()
   ;;; Currently no mechanism for capturing implicit this
   (cl::oget this "a"))

(defvar *o* (cl::new))

(setf (cl::oget *o* "a") 22
      (cl::oget *o* "g") #'f)

((cl::oget *o* "g"))
=> 22

Better would be to provide something akin to ClojureScripts this-as macro (though perhaps with a better name):

(defun f ()
  (this-as (self)
    (cl::oget self "a")))
helmutkian commented 9 years ago

I am running into one wrinkle in the implementation though when matching the test case


(function (x) { return x; }).bind(window)(1);
(funcall ((cl::oget (lambda (x) x) "bind") cl::*root*) 1)
=> ERROR[!]: too few arguments

However passing just one additional argument works:

(funcall ((cl::oget (lambda (x) x) "bind") cl::*root*) 'whatever 1)
=> 1

Is this a bug or a case of JSCL lambda not translating exactly down to JS anonymous function?

davazp commented 9 years ago

There is finally, one last thing: how to capture this when defining a function in JSCL?

This is tricker, because JSCL introduces many internal functions right now. To convert everything in an expression, which will be fixed sometime soon, and to define proper variable scope, which ES6's let would help to clean.

We could hack it into lambda, so it saves this into an ordinary variable, and provide a special this symbol, or as-this macro as you said to access the value.

davazp commented 9 years ago

Done! :-) With this change in the branch https://github.com/davazp/jscl/compare/ffi-improvements I just created, this is captured.

An example:

var y = {};
(defun f () cl::this)
(setf (cl::oget cl::*root* "y" "test") #'f)

and then

y.test()  // => Object { }  ,  actually y
helmutkian commented 9 years ago

Very cool. It's working great so far!

I've begun pushing to my fork of the ffi-improvements branch.

https://github.com/helmutkian/jscl/commit/409add7cbb83a095f15036c4c4879e9c14c768e5

There's basic implementations of the bind macro, a macro for creating anonymous JavaScript objects, an iteration macro and function for JavaScript objects, and a few functions for converting from JavaScript objects to Common Lisp association LISTs and HASH-TABLEs.

Still needs more work, unit tests, and documentation before pull request.