Re: [Lift] Scala to JavaScript DSL ...

91 views
Skip to first unread message

Naftoli Gugenheim

unread,
Dec 13, 2009, 7:50:50 PM12/13/09
to marius...@gmail.com, lif...@googlegroups.com
Looks like a neat approach.
It would be great if it was possible to get a better syntax though. It might not be worth the effort or stable enough, but have you seen scala.reflect.Code? scalac can compile code into an AST.


-------------------------------------
Marius Danciu<marius...@gmail.com> wrote:

All,

I just want to see if there is any interest in the approach discussed here.
As you know Lift has some interesting support for building JavaScript
constructs from Scala code usig JsExp, JsCmd etc classes. I used quite a lot
this support and it's great but if your JS code that you want to send down
to the browser (say as an Ajax or Comet partial update response) gets a bit
more complicated then constructing the JS fragment leads IMO to some
cumbersome Scala code. I found myselft in quite a few situation to use JsRaw
to write the JavaScript fragment in order for the code reader to understand
what JavaScript code will be generated. But of course with JsRaw we put
everything into a String so I'm not a big fan of this approach. So I started
to define a JavaScript like "DSL" that IMO is closer to JavaScript form.
Attached is a source code smaple of how this looks like, so for instance we
can have something like:


val js = JsFunc('myFunc, 'param1, 'param2) {
JsIf('param1 __< 30) {
Var('home) := Wrap(234 __- 3) __/ 2 `;`
Var('someArray) := JsArray(1, 2, 3, 4, 5) `;`
'myFunc(1, 2, "do it", 'home) `;`
$("#myID") >> 'attr("value", "123") `;`
} ~
JsForEach(Var('i) in 'someArray) {
'console >> 'log("Hi there " __+ 'i) `;`
} ~
JsAnonFunc('arg1, 'arg2) {
'alert("Anonymous function " __+ 'arg1 __+ 'arg2)
}(1, 2) `;`
}

println(js.toJs)

this yields the following JavaScript code:


function myFunc( param1, param2 ) {
if (param1 < 30) {
var home = ( 234 - 3 ) / 2;
var someArray = [ 1, 2, 3, 4, 5 ];
myFunc(1, 2, "do it", home);
$("#myID").attr("value", "123");
}
for (var i in someArray) {
console.log("Hi there " + i);
}
function ( arg1, arg2 ) {
alert("Anonymous function " + arg1 + arg2)
}(1, 2);
}


... ok I just droped nonsense code in there for exemplification. A few
words:

1. JsIf, JsForEach describe JavaScript if and for(each) statements
2. Functions like __<, __>, ... __+, __- are function that alows definition
of boolean and/or algebraic expressions.
3. Wrap just wraps an expression into ()
4. Var defined a variable
5 := defines an assignment
6. JsFunc declares a JS function
7. JsAnonFunc declares an anonymous function
8. 'myFunc(1, 2, "do it", 'home) is simply a javascript function invocation
by providing 4 parameter.
9. ~ is just a function that chains statements that don;t necessarily end in
;


Do you think that something like this would be usable in Lift?

Br's,
Marius

--

You received this message because you are subscribed to the Google Groups "Lift" group.
To post to this group, send email to lif...@googlegroups.com.
To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.


Naftoli Gugenheim

unread,
Dec 13, 2009, 7:52:41 PM12/13/09
to marius...@gmail.com, lif...@googlegroups.com
Other advantages of a DSL include type safety and typo safety. :)

-------------------------------------
Marius<marius...@gmail.com> wrote:

That is certainly one way to go but personally I'm not at all a fan of
this string literals approach,. For instance if Scala would not have
had built in XML support using XML as string literals Lift would
probably loose some of its attractions... but that's my opinion.
Furthermore a DSL like language allows better compositionality than
concatenating strings when we want to iteratively add JS code ... for
instance calling the same function multiple times with different
arguments, or collecting code fragments generated by different
application components etc.

Also it seems to me that your approach (somehow similar with
Velocity's) is more expensive but I admit that this is not at all a
strong argument because we're talking about small javascript fragments
so the delta is negligible.

Br's,
Marius

On Dec 12, 8:42 pm, Alex Boisvert <alex.boisv...@gmail.com> wrote:
> Personally, I would rather go with a JavaScript literal and a simple
> templating mechanism for substitution/binding of Javascript literals + Json
> objects that would drive dynamic code (if conditionals, for-loops, ...).
>
> Taking your example,
>
> def myFunc = {"""
>   function myFunc( param1, param2 ) {
>     if (param1 < ${limit}) {
>       var home = ( ${max} - 3 ) / 2;
>       var someArray = ${array};
>       myFunc(1, 2, "do it", home);
>       $("#myID").attr("value", "123");
>     }
>     for (var i in ${someArray}) {
>       console.log("Hi there " + i);
>     }
>     function ( arg1, arg2 ) {
>       alert("Anonymous function " + arg1 + arg2)
>     }(1, 2);
>   }""".jsBind(
>     "max" -> max,
>     "array" -> array,
>     "someArray" -> someArray,
>     ...
>   )
>
> Not a fully fleshed out example but you get the idea.  You'd still be able
> to bind existing JsExp/JsCmds  so some parts could still be abstracted and
> typed checked.
>
> I would find this more readable and simpler that learning a quirky DSL
> syntax.  And I could still copy/paste Javascript code to/from the browser
> and test it easily.
>
> alex
> > liftweb+u...@googlegroups.com<liftweb%2Bunsu...@googlegroups.com>

Alex Boisvert

unread,
Dec 13, 2009, 9:57:54 PM12/13/09
to lif...@googlegroups.com, marius...@gmail.com
scala.reflect.Code (aka expression trees / AST manipulation) would actually be great -- what was proposed was far from it, though.

As I understand it, this approach has not advanced in about 2 years.  I'd be happy to hear if it was a viable option, even if only on 2.8.

alex

Naftoli Gugenheim

unread,
Dec 17, 2009, 1:58:31 AM12/17/09
to marius...@gmail.com, lif...@googlegroups.com
I'm thinking of an approach to writing a DSL with a much cleaner syntax. I'll try to put something together.

Marius

unread,
Dec 17, 2009, 2:47:49 AM12/17/09
to Lift
Let me know when you have something.

Br's,
Marius

On Dec 17, 8:58 am, Naftoli Gugenheim <naftoli...@gmail.com> wrote:
> I'm thinking of an approach to writing a DSL with a much cleaner syntax. I'll try to put something together.
>
> -------------------------------------
>

Naftoli Gugenheim

unread,
Dec 17, 2009, 5:52:48 PM12/17/09
to marius...@gmail.com, lif...@googlegroups.com
I think I'm nearly there (that is a working equivalent of your sample) with G-d's help...

-------------------------------------

Naftoli Gugenheim

unread,
Dec 17, 2009, 7:42:02 PM12/17/09
to liftweb
Current state attached.


2009/12/17 Marius <marius...@gmail.com>
DSL.scala

Naftoli Gugenheim

unread,
Dec 17, 2009, 9:34:29 PM12/17/09
to liftweb
Okay!
The following code:
    val jsFunc: JSFunc = Function("myFunc")("param1", "param2") {case param1 :: param2 :: Nil =>
        Var("someArray") := Array(1, 2, 3, 4, 5)
        If(param1 < 30) {
          val x = Var("x")               // now we can use either x or 'x
          x := (234 - 3) / 2             // calculation happens in scala
          Var("y") := (2:Expr) * x * 2   // calculation happens in javascript
          'jsFunc(1, 2, "do it", 'y)
          val $ = JSIdent("$")
          $("#myID") >> 'attr("value", "123")
        } Else {
          'console >> 'log(">=30")
        }
        ForEach(Var("i") In 'someArray) {
          'console >> 'log("Hi there " & 'i)
        }
        Function()("arg1", "arg2") { case arg1 :: arg2 :: Nil =>
          'alert("Anonymous function (" & arg1 & ", " & arg2 & ")")
        }(1,2)
    }
    println(jsFunc.toCode)

Produces:

function myFunc(param1, param2) {
var someArray
someArray = [1.0, 2.0, 3.0, 4.0, 5.0]
if((param1 < 30.0)) {
var x
x = 115.0
var y
y = ((2.0 * x) * 2.0)
jsFunc(1.0, 2.0, "do it", y)
($("#myID")).attr("value", "123")
} else {
(console).log(">=30")
}
var i
for(i in someArray) {
(console).log(("Hi there " + i))
}
function (arg1, arg2) {
alert((((("Anonymous function (" + arg1) + ", ") & arg2) + ")"))
}(1.0, 2.0)
}

It may be desirable that instead of defining your own names for Vars and function parameters, names are auto-generated, since you can anyway use typesafe scala identifiers. This would save boilerplate and produce more "obfuscated" code, and other than the name generating algorithm is a trivial change to make.
Note that since I am not familiar with Lift's JavaScript APIs I just wrote my own AST, which consists of case classes that contain their data parameters and a toCode method. Feel free to delete them and plug Lift's classes instead--there is no dependency on anything unique about them.
Also note that to prevent "string" + ident from compiling as a string to be outputted and instead output a + operation, you have two choices: use the & operator instead, which is replaced with + when either operand is a string, or write ("string":Expr) + ident.


2009/12/17 Naftoli Gugenheim <nafto...@gmail.com>
DSL.scala

Ross Mellgren

unread,
Dec 17, 2009, 10:18:34 PM12/17/09
to lif...@googlegroups.com
I like the source for the DSL very much, I think it's very well written. Thanks for sharing it.

-Ross

<DSL.scala>

Naftoli Gugenheim

unread,
Dec 17, 2009, 10:30:32 PM12/17/09
to liftweb
You're welcome. There's still some work to do; I would call it a proof of concept. But I'll let Marius finish it up. :)

Marius

unread,
Dec 19, 2009, 3:26:41 AM12/19/09
to Lift
If I may a few notes:

Syntactically it doesn't seem to me that there are much differences
between this and the initial proposal. Probably the most noticeable
diffs are in algebraic expressions like

Var("y") := (2:Expr) * x * 2

which probably looks more appealing than

Var("y") := 2 __* x __* 2

The other thing is that you don't end JS statements with ; ... I
don't think this is a good JS practice. I added `;` function to allow
user to specify this terminator when needed.

I'm also not at all in favor of function names starting with capital
letter.

I was also running into a Scala compiler crash when using constructs
like: case class Fnc(name: String)(params: String*)(body : => String)
{} :D

I'm not sure why in things like: def Function()(argNames: String*)
(body: PartialFunction[List[JSIdent], Unit]): JSAnonFunc you used
PartialFunctions ... you are callin gthe PF without checking if the
function is defined for its parameter which could lead to MachError.

I see you took a different approach of building JS code in a more
imperative manner where generated code is kept in the ThreadGlobal
state. My initial approach and actually my design intent was to use
functional composition to write the code which tremendously simplifies
the library code.

I made some adjustments to my initial approach and something like:

val js = JsFunc('myFunc, 'param1, 'param2) {

JsIf('param1 < 30) {
Var ('home) := (234: Js) + 3 / 3 `;`
Var ('someArray) := JsArray(1, 2, 3, 4, 5) `;`


'myFunc(1, 2, "do it", 'home) `;`
$("#myID") >> 'attr("value", "123") `;`
} ~
JsForEach(Var('i) in 'someArray) {

'console >> 'log("Hi there " + 'i) `;`
} ~
JsAnonFunc('arg1, 'arg2) {
'alert("Anonymous function " + 'arg1 + 'arg2)
}(1, 2) `;`
}
println(js.toJs)

generates

function myFunc( param1, param2 ) {
if (param1 < 30) {

var home = 234 + 3 / 3;


var someArray = [ 1, 2, 3, 4, 5 ];
myFunc(1, 2, "do it", home);
$("#myID").attr("value", "123");
}
for (var i in someArray) {

console.log("Hi there 'i");


}
function ( arg1, arg2 ) {

alert("Anonymous function 'arg1'arg2")
}(1, 2);
}

I guess in many respects these things are subjective. Personally so
far I am linking better my approach with the changes for expression
building (no more __ prefixed functions).

Br's,
Marius

> 2009/12/17 Naftoli Gugenheim <naftoli...@gmail.com>
>
> > Current state attached.
>
> > 2009/12/17 Marius <marius.dan...@gmail.com>

> >> liftweb+u...@googlegroups.com<liftweb%2Bunsu...@googlegroups.com>


> >> .
> >> > For more options, visit this group athttp://
> >> groups.google.com/group/liftweb?hl=en.
>
> >> --
>
> >> You received this message because you are subscribed to the Google Groups
> >> "Lift" group.
> >> To post to this group, send email to lif...@googlegroups.com.
> >> To unsubscribe from this group, send email to

> >> liftweb+u...@googlegroups.com<liftweb%2Bunsu...@googlegroups.com>


> >> .
> >> For more options, visit this group at
> >>http://groups.google.com/group/liftweb?hl=en.
>
>
>

>  DSL.scala
> 9KViewDownload

Naftoli Gugenheim

unread,
Dec 21, 2009, 11:20:15 PM12/21/09
to liftweb
Inlined

2009/12/19 Marius <marius...@gmail.com>

If I may a few notes:

Syntactically it doesn't seem to me that there are much differences
between this and the initial proposal. Probably the most noticeable
diffs are in algebraic expressions like

Var("y") := (2:Expr) * x * 2

which probably looks more appealing than

Var("y") := 2 __* x __* 2

First, I should confess that I didn't look at your source code before I wrote my approach, although I saw it now. That said, I agree that there is plenty in common between the two approaches, and also a couple of differences. Feel free to use whatever blend you like!


The other thing is that you don't end JS statements with ;  ... I
don't think this is a good JS practice. I added `;` function to allow
user to specify this terminator when needed.

It would be trivial to have my implementation add semicolons in the generated code without requiring any change in the DSL's usage.
 
 
I'm also not at all in favor of function names starting with capital
letter.

Please rename them to whatever you like!
 

I was also running into a Scala compiler crash when using constructs
like: case class Fnc(name: String)(params: String*)(body : => String)
{} :D

Then possibly functions are better suited than case classes for providing the names of the DSL syntax. If that means you prefer lowercase names please change them!

 
I'm not sure why in things like: def Function()(argNames: String*)
(body: PartialFunction[List[JSIdent], Unit]): JSAnonFunc you used
PartialFunctions ... you are callin gthe PF without checking if the
function is defined for its parameter which could lead to MachError.

What would it help to intercept such a situation--to provide a more descriptive error message? It's an error either way.
In any case, I realized after I wrote it (as I mentioned) that the signature could be changed to
def Function(numParams: Int)(body: PartialFunction[List[JSIdent], Unit])
that is, there is no reason to specify names for the parameters. Since the PF format binds scala identifiers to represent the arguments, names could be autogenerated. Similarly,
val x = Var("x")
could be changed to
val x = Var()
and x would represent a JS var whose name is autogenerated.
I could be totally wrong but it seems to me that the ability to have Scala identifiers and not just implicitly converted symbols provides more scalability. For instance you could have utility functions in Scala that help build the javascript but don't need the implicits in scope.

 

I see you took a different approach of building JS code in a more
imperative manner where generated code is kept in the ThreadGlobal
state. My initial approach and actually my design intent was to use
functional composition to write the code which tremendously simplifies
the library code.

Could you elaborate? What makes your code more functional?

With your approach,
     JsIf('param1 < 30) {
       Var ('home) :=  (234: Js) + 3 / 3 `;`
       println("Hi there")

       Var ('someArray) := JsArray(1, 2, 3, 4, 5) `;`
       'myFunc(1, 2, "do it", 'home) `;`
       $("#myID") >> 'attr("value", "123") `;`
     } ~
would not work, because JsIf's second parameter list takes a =>Js, that is, it only uses on the expression returned. In the above example, the Var('home) ... line goes into a black hole.
In my case you have much more flexibility because the entire code block does not have to be one long expression. All DSL invocations are captured.
This is also the reason my approach does not require using semicolons.
Also, it would be advantageous if the result of executing the DSL was the regular Lift JavaScript AST. This would avoid code duplication in terms of the algorithm that generates the output javascript text (which if it does not already could have shared features like whitespace compression or pretty printing), while at the same time allowing the generated AST to be processed by code that can also work with plain Lift JavaScript AST objects. I'm not clear how that would work with your approach, but with mine it's trivial.

I should mention that much of the inspiration for this builder approach came from the SWT DSL, by Rodant.

To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages