pzavolinsky / elmx

A tiny precompiler that takes an Elm program with embedded HTML and desugars the HTML into elm-html syntax. Elmx is to Elm what React's JSX is to Javascript
MIT License
351 stars 11 forks source link

List interpolation #2

Closed rymohr closed 8 years ago

rymohr commented 8 years ago

Nice work! Can you expand on why the : operator is required for lists? Seems like you would always interpolate if the list was a elmx fragment. Requiring = for string interpolation definitely makes sense.

pzavolinsky commented 8 years ago

Hi @rymohr thanks!

The idea behind {:list} is that we need to support two scenarios:

wrap : Html -> Html
wrap item = <li><label>The item is:</label>{item}</li>

and

listOf : [Html] -> Html
listOf items = <ul><li>First hardcoded item</li>{:items}</ul>

In the fist scenario, the <label> and item would end up in the same list, as children of <li>, namely:

wrap : Html -> Html
wrap item = Html.li
  []
  [ Html.label [] [Html.text "The item is:"]
  , item
  ]

while in the second scenario the hardcoded <li> and {:items} are two lists that get concatenated into <ul>'s children, that is:

listOf : [Html] -> Html
listOf items = Html.ul
  []
  ([Html.li [] [Html.text "First hardcoded item"]] ++ items)

You can check these two examples and play around with them in the live cheatsheet (they are "Nested Html" and "Nested list of Html").

Cheers!

rymohr commented 8 years ago

Thanks for the examples. I guess my follow up question then is, when would you ever not want siblings concatenated if they both fall under the same parent?

And what is unique to elmx that prevents it from using an identical syntax to react's jsx?

pzavolinsky commented 8 years ago

Hi @rymohr these are both good questions.

when would you ever not want siblings concatenated if they both fall under the same parent?

You always want the siblings concatenated, but sometimes you want to concat a static list of siblings to a dynamic one:

numbers : Bool -> Html
numbers addExtra =
  let
     items = if addExtra 
             then[ <li>Two</li><li>Many</li> ]
             else []
  in
     <ul><li>One</li>{:items}</ul>

and sometimes you just want to append a single element to the static list of siblings:

numbers : String -> Html
numbers more =
let
   item = <li>{=more}</li>
in
   <ul><li>One</li>{item}</ul>

which brings us to the second question:

And what is unique to elmx that prevents it from using an identical syntax to react's jsx?

In other words, why cannot {:items} above be just {item}?

The answer is twofold:

Lets look at React in more detail!

When you write this:

const list = (items) => <ul><li>First</li>{items}</ul>

The resulting JS looks like this:

var list = function (items) {
  return React.createElement(
    'ul',
    null,
    React.createElement(
      'li',
      null,
      'First'
    ),
    items
  );
};

The first argument to React.createElement is the tag name, the second the attributes, and from that point on, the rest are either elements (like 'First') or lists (like items). If we had to implement this createElement function ourselves, we could probably go for something like:

function ourCreateElement(tag, attrs, ...children) {
  // normalizedChildren :: [ [Element] ]
  const normalizedChildren = children.map(
    c => (isArray(c) ? c : [c])
  );

  // do something with normalizedChildren, knowing is always a list of lists of
  // elements (e.g. flatten and render)
}

const isArray = o => o.length !== undefined // or something more fancy

In Elm, using elm-html similar code would look like:

list : List Html -> Html
list items =
  Html.ul [] ([ Html.li [] [ Html.text "First" ] ] ++ items)

Note the type of Html.ul:

ul : List Attribute -> List Html -> Html

The second argument must be a list of Html, cannot be a single Html nor a list of mixed stuff (Htmls and lists of Html).

So lets think about this in a different way...

Given the following piece of JS:

const Comp = (i) => <div>{i}</div>

can you tell during compilation time (i.e. when translating JSX into JS) whether i is a single element or a list?

The answer is "no", because there could be two instances of the Comp component one passing multiple elements in i and another passing a single one:

const A = () => {
  const i = <span>Hi</span>;
  return <Comp i={i} />;
}
const B = () => {
  const i = [1,2,3].map(v => <span key={v}>{v}</span>);
  return <Comp i={i} />;
}

Since the code for Comp is generated only once and must work for both scenarios above, it follows that Comp works for both single elements and lists (exploiting React.createElement's flexibility).

If React.createElement was statically typed (as it would be if it was implemented in Elm), then the signature would probably be similar to that of Elm's ul:

fakeCreateElement : String -> List Attribute -> List Html -> Html
fakeCreateElement tag attrs children =
  ...

And then the magic of JSX allowing both single elements and lists would not work.

So, long story short, in JSX everything works because JS is dynamic and a function can take a list one day an element the next day and a hobbit some time later.

In Elm you need to make things explicit (and that is a good thing) so we cannot have the magic {x} work for anything (or, at least I cannot think of a way or making this work).

rymohr commented 8 years ago

Isn't that exactly what union types are for?

pzavolinsky commented 8 years ago

Yes, unfortunately (for this particular use case, but luckily for everyone's mental sanity) Html is not a union type:

type alias Html = VirtualDom.Node

(from Html.elm)