Jmacro

From HaskellWiki
Revision as of 23:41, 13 September 2024 by Benmachine (talk | contribs) (patch-tag seems to be gone, couldn't find the github repo mentioned in the cabal file, but darcs hub seems to have a nearly-up-to-date repo)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search


JMacro is a library for the programmatic generation of Javascript code. It is designed to be multipurpose -- it is useful whether you are writing nearly vanilla Javascript or you are programmatically generating Javascript either in an ad-hoc fashion or as the backend to a compiler or EDSL.

It provides support for hygienic names, as well as sharing of names between the generated Javascript and embedded Haskell antiquotation.

The jmacro package also provides a standalone executable, which may be used to compile JMacro syntax directly to Javascript.

JMacro provides a simple, lightweight quasiquoted syntax that is mainly compatible with standard Javascript. Most Javascript code in the wild can be used as JMacro code with no or minimal modification. However, JMacro extends Javascript syntax in a number of Haskell-friendly ways, including whitespace function application and single character lambdas. Syntax is statically checked at compile time. JMacro expressions may contain antiquoted Haskell code. This code may generate further JMacro code, or it may generate any of a range of standard Haskell types, which are able to be marshalled into JMacro through typeclass methods.

Jmacro is available from hackage: http://hackage.haskell.org/package/jmacro

A source repository is at: https://hub.darcs.net/gershomb/jmacro

Key Features

Syntax Checking

JMacro provides compile-time guarantees of syntactic correctness. Any jmacro code which compiles will necessarily produce syntactically correct Javascript. JMacro supports Javascript's literal regexes, and regex literals are checked statically at compile time as well.

Note that jmacro itself is untyped, which means that the generated javascript may not be typesafe. This is a feature, as a great deal of useful javascript code is either hard to type, or simply not statically typeable. Take, for example, the commonly used jQuery library. Depending on the string argument to the $ function, the returned value can vary over a range of types.

Functional Syntax

Jmacro provides a mixed-functional syntax. Nearly all valid javascript code is also valid jmacro code. Code from the documentation of an existing library can be copied and pasted directly into Haskell as a jmacro block. However, jmacro also provides a great deal of syntactic sugar that allows code to be written in a considerably more functional style, akin to Haskell or ML. Notably, anonymous functions may be introduced with a single backslash, and function application may be denoted by simple whitespace. Hence, this is valid jmacro describing a function which doubles a number:

\x -> x + x

Hygienic Names, Proper Scoping

It is common, when generating Javascript, to want to compose blocks of code which operate on different parts of a page. These blocks of code may find it necessary to introduce top-level variables for sharing state. Typically, this is resolved by placing these blocks of code within an anonymous function to provide namespacing. When two blocks of jmacro code are composed, however, variable declarations within each will *automatically* be named in the generated code so as not to overlap. This additionally allows jmacro to provide proper block-level scoping, which does not exist in standard Javascript.

Thus, in the following example, the two declarations of `var x` produce *different* identifiers in the generated javascript:

> renderJs $ [$jmacro|var x;] `mappend` [$jmacro|var x;]
var jmId_0;
var jmId_1;

Antiquotation, Marshalling and Unmarshalling, Shared Scope

The ability to provide hygienic names becomes particularly useful when coupled with antiquotation. Within a Haskell program, quoted jmacro code blocks may contain antiquotations, which themselves contain Haskell code. The antiquoted Haskell code may either produce a jmacro expression or statement itself, or it may produce a Haskell value which can be marshalled to jmacro (i.e., is an instance of the ToJExpr class).

Marshalling a number:

> let foo = 5 in renderJs [$jmacro|var x = `(foo)`;|]
var jmId_0;
jmId_0 = 5;

Marshalling a string:

> let foo = "hello" in renderJs [$jmacro|var x = `(foo)`;|]
var jmId_0;
jmId_0 = "hello";

Passing in another expression:

> let foo = [$jmacroE|alert "yo"|] in renderJs [$jmacro|var x = `(foo)`;|]
var jmId_0;
jmId_0 = alert("yo");

Antiquoted Haskell code has full access to all identifiers in scope within the enclosing jmacro block. This allows for the construction of macros:

> let foo = \str -> [$jmacroE|alert `(str)`|] in renderJs [$jmacro|var x = "hello"; var y = `(foo x)`;|]
var jmId_0;
jmId_0 = "hello";
var jmId_1;
jmId_1 = alert(jmId_0);

Usage

{-# LANGUAGE QuasiQuotes #-}
import Language.Javascript.JMacro
import Data.Monoid

-- The jmacro quasiquoter produces a statement or block of statements
-- (i.e., things which may be executed -- something of type JStat).

test :: JStat
test = [$jmacro|

           // function declarations produce absolute (not hygienic) names

           // functional style declarations return the value of an expression
           // note that this is not curried, and thus cannot be partially applied.
           fun add x y -> x + y;

           // this function is explicitly curried, and can/must be partially applied
           fun addTwo x -> \y -> x + y;

           // imperative style declarations execute a block of statements
           fun mult x y {
                     var tmpAns = x * y;
                     return tmpAns;
                   };

           // variable declarations produce hygienic names
           var x = "this will be renamed";

           // we may "escape" hygiene by using bangs
           var !y = "this will really be named y";

           // functions can be declared using anonymous lambda syntax
           // and then assigned hygienic names
           var anonAdd = \x y -> x + y;

           // anonymous functions can also be written in imperative style
           var anonMult = \x y {return (x * y);};

           // functions may be invoked with parenthesis
           var addThenMult = mult(2,add(2,3));

           // functions may also be invoked with juxtaposition, as in Haskell
           var multThenAdd = add 2 (mult 2 3);

         |]

-- The jmacroE quasiquoter produces expressions (i.e. things which
-- have *values*, something of type JExpr).
testE = [$jmacroE|addThenMult + multThenAdd|]

-- Antiquotations in an expression position can contain JExprs (but not JStats)
testAQ = [$jmacro|var x = `(testE)`;|]

-- Antiquotations in a statement position can contain either
-- (but JExprs which are not function invocation will be discarded)
testAQ2 = [$jmacro|`(test)`; `(testE)`;|]

-- Statements may be composed as monoids. (Expressions may not be).
testCompose = test `mappend` test

-- JMacro blocks are rendered to Text.PrettyPrint.HughesPJ Docs.
-- These Docs can be formatted more carefully, or simply
-- converted to Strings via Show.
renderedJavascript :: String
renderedJavascript = show $ renderJs test

-- See the Language.Javascript.JMacro.Prelude module for further examples.

Additional Syntax Extensions

JMacro now has a few extra syntax extensions.

Infix Application

JMacro has an infix application operator, and a reversed infix application operator. foo <| bar <| baz desugars to foo(bar(baz)). Meanwhile, foo |> bar |> baz desugars to to baz(bar(foo)). Since Javascript functions are not curried by convention, these functions are slightly less useful than one might hope. However, an explicit Javascript curry function is easy enough to write.

Destructuring Bind

JMacro features an enhanced destructuring bind, in function and variable declarations, as well as lambda abstractions. The syntax handles lists, maps, and "@" patterns. Maps are denoted with oxford braces ( {| ... |} ) for syntactic clarity. An infix cons (as :|) is also available in both the left and right hand sides of expressions.


fun foo [x,y] -> x + y;

fun someMap@foo{| lbl:val, someotherlbl: anotherVal |} {return [someMap, val]}

var x = \[a,b] -> a;

var [x,y] = foo(bar);

var (x:|xs) = 1 : 2 : 3 : [];