神刀安全网

Generalizing JSX: Delivering on the Dream of Curried Named Parameters

When I first saw JSX, I was intrigued that there might be more to it than just the simple syntax sugar it provided for React. Given how there’s recently been a lot of discussion as to why you shouldn’t use JSX with React, I thought it might be fun consider just the opposite: why you might want to use JSX outside of React for general purpose programming.

In this post I’d like to separate JSX the React DSL from JSX the code transformation , completely rethinking its utility from the perspective of a core language feature: what would you use JSX for if you’d never heard of HTML? I’ll start by showing you how to hack JSX to make it compile down to pretty much anything you want. Then we’ll use it to make JavaScript a more expressive functional programming language (borrowing some ideas from OCaml along the way) and apply it to data structures as well. Finally, we’ll come full circle by rebuilding React from scratch from this “general purpose JSX”. All the code samples in this blog post are editable and runnable , so feel free to tinker (that’s the whole point of this post!)

Hacking JSX

JSX is just a source transformation, and a pretty straight-forward one at that. For any particular XML tag in your JavaScript code, the transformation simply passes the tag’s name, properties, and children to a function called React.createElement . In the example below, feel free to change the JSX string to see what the resulting transpiled JavaScript looks like:

var transform = require("generic-jsx/display-transform");  // Edit the code below to see what JSX usually transpiles to. transform("<div id = 'my-element'>hi!</div>") 

This React.createElement function is responsible for instantiating the virtual DOM element behind most of React’s magic. As I’m sure you’ve been told by now, there’s nothing preventing you from just calling it directly yourself. But something you may not know though is that you can actually configure JSX to replace XML tags with a different function.

The way you do this is with the JSX Pragma . Although the original intent was probably to allow other React-like frameworks to take advantage of JSX, you can use whatever function you’d like. Let’s start by just trying console.log and observing the results:

/* @jsx console.log */  // Now this is the same as console.log("div", { "id":"my-element"}, "hi"); <div id = "my-element" >hi</div> 

So here we’re transforming XML tags to logging statements, which is why the code above just prints out "div" , the properties object, and the string "hi" . Try changing /* @jsx console.log */ in the comment above to /* @jsx Array */ and re-running to see what happens.

Generic JSX

Now that we know how to modify what JSX will become, let’s attempt to make it as generic as possible. Instead of choosing one hard-coded function for every tag to be replaced with, what if we could instead make it dynamically use the tag’s name as a function? In other words, we’d like <tag/> to become tag() .

This is a little tricky since we can’t simply write it into the JSX Pragma anymore, and at runtime we only have the name of the function as a string. Luckily, we can construct a function expression that turns the string into the function using eval (I did say hack):

/* @jsx ((a,b)=>(eval(a)(b))) */  function func(properties) {     console.log("success! you passed in " + JSON.stringify(properties)); }  // JSX "function call" <func key = "value"/>; 

Pretty cool right? We just turned JSX tags into an alternate function call syntax (think .apply or .call ), but with named parameters! This gets even cooler when you combine it with ES6 destructring :

/* @jsx ((a,b)=>(eval(a)(b))) */  // Destructuring let's us name our parameters function reduce({ array, initial, result = initial, func }) {     for (let item of array)         result = func(result, item)      return result; }  // Plain JavaScript with options object reduce({ array: [1,2,3,4,5], initial: 0, func: (a,b) => a + b });  // JSX "function call" <reduce array = { [1,2,3,4,5] } initial = { 0 } func = { (a,b) => a + b }/>; 

This is the core of the idea we’ll be playing around with: ES6 made declaring functions with named parameters very natural, and our custom JSX will make calling such functions equally natural. As it stands right now, this syntax doesn’t seem to really offer us much beyond what JavaScript is already capable of by just using an options object (line 13 above). In order to do something really special, we’ll have to digress shortly to a different topic…

Currying

If you’ve done any functional programming in the past, you are probably familiar with the concept of currying. In a nutshell, currying allows you “partially” call a function by only supplying a few arguments to it (and leaving out the rest). In many ways, it is simply a shorthand to just making a wrapper function that takes fewer parameters. I’ve included below one of the first examples that really made this stick for me:

// reduce takes three params: a function, an initial value, and an array. var reduce = require("ramda").reduce;  // If you think about it, the "sum" of an array is just a  // specific case of reduce. Here we show this WITHOUT currying, // and instead wrapping reduce in the sum function: var sum = array => reduce((a,b) => a + b, 0, array);  // This library's reduce function supports currying however, // which allows us to more expressively show this by just  // applying the first two parameters, and leaving the third out: var sum_curry = reduce((a,b) => a + b, 0);  // Both functions are equivalent: sum([1,2,3,4,5]) === sum_curry([1,2,3,4,5]); 

Unfortunately, I’ve found currying to be lacking in many languages. To quickly summarize:

  1. Currying makes the position of your parameters incredibly important . Just from the example above you can see that it is crucial that the iteration function come first in reduce (which is backwards from most implementations), since it is the one most likely to be curried. If the array came first, currying wouldn’t be very useful to use since we’d just have a very specific reduce tied to one particular array. Sometimes deciding the order of the parameters is not trivial, and it leads to lots of unfortunate necessities like flip .
  2. No default or optional parameters . Since missing parameters trigger a curry, you can’t use a missing argument to signify a default. Similarly, you can’t have a variable list of arguments.
  3. Visually, its hard to tell when a function is “done” applying . reduce(a,b) may be complete or may be only partial. It depends on the definition.
  4. You can’t “re-curry” parameters . Once they’re in the function, they’re in for good.
  5. And of course, currying doesn’t work well with the JavaScript convention of having an options argument at the end which coallesces many parameters.

All in all, the resulting effect in my exprience is that currying makes writing code far more pleasurable, but often makes reading code somewhat confusing.

Currying Named Parameters

As we’ll see shortly, many of these problems go away in languages that have named parameters, like OCaml and now our new custom JSX. But to deliver on this, we are going to have to slightly change our generic JSX transformation and introduce a new way of thinking about function calls.

In most languages, arguments are passed into a function at call time. JavaScript follows this very convention, as parentheses are used to both all a function as well as pass arguments to it. But this doesn’t have to be the case. With JSX, we have new syntax to play with that we just happened to make equivalent to function calling. But what if instead we defined JSX tags to mean the binding of arguments (kind of like how .bind binds the this object), and kept parentheses for function calling:

/* @jsx ((a,b)=>(bind(eval(a),b))) */  function bind(aFunction, args) {     return function()     {         return aFunction(args);     } }  function reduce({ array, initial, result = initial, func }) {     for (let item of array)         result = func(result, item)      return result; }  // Almost the same as before, only now we need parentheses: <reduce array = { [1,2,3,4,5]} initial = { 0 } func = { (a,b) => a + b } />(); 

At first glance this seems like a worse system: all we’ve done is turn an already verbose paradigm into a more verbose paradigm. But the beauty of such a system where argument binding is decoupled from function calling is that nothing prevents us from binding multiple times. And if we can bind multiple times, then on each bind we could do a shallow merge of the passed in arguments, effectively giving you named parameter currying :

/* @jsx ((a,b)=>curry(eval(a),b)) */  function curry(aFunction, args) {     return function(additional = {})     {         return aFunction(Object.assign({}, args, additional));     } }  function reduce({ array, initial, result = initial, func }) {     for (let item of array)         result = func(result, item)      return result; }  // Here JSX tags mean "curry". Notice we can specify our parameters // in any order we please, and leave any out: var sum = <reduce initial = { 0 } func = { (a,b) => a + b } />;  // This is equiavelent to the following: var sum_no_jsx = curry(reduce, { initial: 0, func: (a,b) => a + b });  // We do one final "curry", but then actually call the function with parentheses. <sum array = { [1,2,3,4,5] }/>() 

So, here we curried reduce into sum just as in our original example, but with some interesting implications:

  1. The order we bound our arguments didn’t matter. Since parameters are named, we can curry in any order we want .
  2. This form of currying doesn’t break down with optional parameters . Since we manually specify when the function is called, there is no confusion about when the function has sufficient arguments to trigger an implied call.
  3. Visually it is clear when you are currying vs. when you are calling the function . Any time you see a function in a JSX tag, you know it is a curry , any time you see parentheses, you know it is a call.
  4. Since you can curry multiple times with the same named parameter, you can actually override (or re-curry) parameters.

Handling Positional Arguments

Of course, there are some downsides to named parameters too. Namely, since we are no longer using the positions of parameters as a de-facto convention, named parameters rely heavily on everyone agreeing to the same “name” for key parameters. Callbacks are particularly prone to this problem: the callback taker and callback giver must agree on parameter names. Sometimes this is a non-issue, but other times the names won’t match up.

So it seems that it would be nice to define an easy way to “map” named parameters, either to a different name (if a caller expects something different), or a positional parameter (if the caller wants to pass in traditional arguments). Below we’ve done just that. From this point forward we’ll simply be require -ing the underlying implementation (available here ), since it is less important than the concepts we’ll be covering. This version introduces the from function which allows you to map parameters during a curry. We continue our reduce / sum example by making sum ’s API a little friendlier by renaming “array” to “numbers”:

/* @jsx (curry(_=>eval(_))) */ var { curry, from } = require("generic-jsx"); var reduce = require("generic-jsx/reduce");  // Here we are currying "initial' and "func", and RENAMING "array" to "numbers". var sum = <reduce array = { from("numbers") } initial = { 0 } func = { (a,b) => a + b } />;  // Now sum has a more natural API: <sum numbers = { [1,2,3,4,5] }/>(); 

We can use this same ability to map to positional arguments, and thus interface with traditional JavaScript functions:

/* @jsx (curry(_=>eval(_))) */  var { curry, from } = require("generic-jsx");  var divide = ({numerator, denominator}) => numerator / denominator; var half = <divide denominator = { 2 }/>;  var halves = [1,2,3,4,5].map(<half numerator = { from(0) }/>); 

So even though half takes named parameters, it was able to serve as the callback to Array.map which passes in positional parameters . This is because we mapped numerator to the 0th position using from . The nice thing about the above code is that it is rather explicit for anyone coming later to read it. It’s clear that we are currying divide ’s denominator parameter, and that we expect the numerator to be passed into the 0th position. If we were to want to flip this, it would be equally clear.

Data Structures

So far there’s been a pretty glaring omission from our custom JSX: none of our examples have made use of the ability for JSX tags to have child tags. If we turn our attention away from functions and to data structures, we’ll see that child element can provide an incredibly powerful out-of-the-box literal syntax. In the example below, I’ve defined a generic BinaryTree class using ES6 classes, and then used it to represent a math expression:

/* @jsx (curry(_=>eval(_))) */ var { curry } = require("generic-jsx")  class BinaryTree {     constructor({ value, children: [left, right]})     {         this.value = value;                  if (left)             this.left = typeof left === "function" ? left() : left;          if (right)             this.right = typeof right === "function" ? right() : right;     } }  // This represents 5 / ( 4 + 6) <BinaryTree value = "/">     <BinaryTree value = { 5 } />     <BinaryTree value = "+">         <BinaryTree value = { 4 } />         <BinaryTree value = { 6 } />     </BinaryTree> </BinaryTree>() 

Thanks to JSX, our binary tree constructor actually looks like a binary tree: and without any modifications to our general purpose JSX. This shows the power of having chosen functions as our basic building block over objects like React has. With functions, we get object for free. Moreover, our currying can play a very powerful role as well:

/* @jsx (curry(_=>eval(_))) */ var { curry, from } = require("generic-jsx"); var BinaryTree = require("generic-jsx/binary-tree");  var Division = <BinaryTree value = "/"/>; var Addition = <BinaryTree value = "+"/>; var Number = <BinaryTree value = { from(0) }/>;   <Division>     { Number(5) }     <Addition>         { Number(4) }         { Number(6) }     </Addition> </Division>() 

Here we’ve curried constructors in order to create something akin to a special purpose BinaryTree node. Our curried constructors are very re-usable and make our code cleaner, without having to subclass. Again, with no new features we’ve gotten something that allows a great deal of expressivity in a new area of the language. I don’t want you to think that this is solely useful for trees though. In the example below I’ve wrapped Facebook’s immutable JS library to show a more natural way of writing a Map literal:

/* @jsx (curry(_=>eval(_))) */ var { curry, from } = require("generic-jsx"); var Map = require("generic-jsx/immutable-map");   <Map>     <Map.Entry key = { 1 } value = "1" />     <Map.Entry key = "1" value = "2" />     <Map.Entry key = { { } } value = "3" /> </Map>() + "" 

Much like JSON, JSX allows object creation to appear much more declarative, except it works just as well with your custom classes!

Back to React

So up to this point we’ve used JSX syntax as a general purpose function calling extension that’s expanded the language with named parameters, currying, re-currying, and given us out-of-the-box custom data structure literals to boot. But what would really prove our JSX’s flexibility is if you could still produce the same HTML features as React. From the BinaryTree example above, it should be pretty clear that we can:

/* @jsx (curry(_=>eval(_))) */ var { curry } = require("generic-jsx"); var { CSSToString } = require("generic-jsx/react");  function render(element) {     if (typeof element === "string")         return element;          return render(element()); }  var div = ({style, children}) => `<div ${style?' style='+CSSToString(style):''}>` + children.map(render).join("") + "</div>";  render( <div style = { { border: "1px solid black;" } }>             <div style =  { { color: "blue" } }>                 hello there!             </div>             normal         </div>); 

This is of course just a toy version of React, but it does give us an idea of how we could write it with our custom JSX, and it is quite powerful as we’ll see in a moment. The key difference is that in our React, the elements are simply functions, not objects, and rendered elements are simply the result of exhaustively calling those functions . And since these are just functions, we can of course use currying:

/* @jsx (curry(_=>eval(_))) */ var { curry } = require("generic-jsx"); var { div, p, h1, render } = require("generic-jsx/react");  var P = <p style = { { "font-size": "14px" } } />; var H1 = <h1 style = { { "font-size": "24px" } } />;   render( <div>             <H1>Hacking JSX</H1>             <P>JSX is just a source transformation...</P>         </div>); 

Again, here we are creating “new” elements by simply currying existing ones.

More importantly, any operation we can perform on a function we can now perform on a React element, and vice versa. This is an incredibly important insight that merits repeating: in this version of React there is nothing special about the virtual DOM. We have unified the concept of unapplied functions with that of unrendered DOM . Tricks you come up with for either can actually be applied to both.

This is an interesting result I have come across a few times before: sometimes you can gain more insight into the very concrete and practical usage of ideas by first considering them in the abstract. I have wanted to be able to “curry” React elements for a long time now, and similarly to be able to pass “lamda” elements directly. By reconsidering JSX entirely from the bottom up, I think we have built a more solid foundation for this idea than had we only tried to make it work for the peculiarities of DOM rendering.

All the examples above were made with the Tonic project I work on. If you’d like to add working examples to your posts, check out Tonic embeds .

Discuss On Hacker News

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Generalizing JSX: Delivering on the Dream of Curried Named Parameters

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮