Implement syntactic sugar for rendering trees à la JSX

EDIT: I have updated the example to be more complete, but I'm still not sure if that works how I'm imagining it. I'll first try to implement a working version of the UI library before coming back with more details.

Hello everyone!

The idea is to enable a thin layer of syntactic sugar to render component trees. Why? It presents them in a really intuitive and visually easing structure.

One strong use case for this feature: It would allow to create some nifty libraries for rendering declarative layout. (Basically a Swift version of React.)

As far as I can see, this would not introduce any backward-incompatible changes.

Basic example:

Given

protocol Properties {}

protocol Renderable {
    init(properties: Properties, children: [RenderTree]?)
    func render() -> [RenderTree]?
}

class RenderTree {
    typealias Factory = (Properties, [RenderTree]?) -> Renderable
    var factory: Factory
    var properties: Properties
    var children: [RenderTree]?
    
    init(factory: @escaping Factory, properties: Properties, _ children: [RenderTree]? = nil) {
        self.factory = factory
        self.properties = properties
        self.children = children
    }
}

class Scroll: Renderable {
    struct ScrollProperties: Properties {}
    
    var children: [RenderTree]?
    
    static func createElement(_ children: [RenderTree]? = nil) -> RenderTree {
        print("create \(type(of: self))")
        
        return RenderTree(
            factory: Scroll.init,
            properties: ScrollProperties(),
            children
        )
    }
    
    required init(properties _: Properties, children: [RenderTree]?) {
        self.children = children
    }
    
    func render() -> [RenderTree]? {
        print("Hello \(type(of: self))")
        
        return nil
    }
}

class Section: Renderable {
    struct SectionProperties: Properties {
        let heading: String
    }
    
    var properties: SectionProperties
    var children: [RenderTree]?
    
    static func createElement(heading: String, _ children: [RenderTree]? = nil) -> RenderTree {
        print("create \(type(of: self))")
        
        return RenderTree(
            factory: Section.init,
            properties: SectionProperties(heading: heading),
            children
        )
    }
    
    required init(properties: Properties, children: [RenderTree]?) {
        self.properties = properties as! SectionProperties
        self.children = children
    }
    
    func render() -> [RenderTree]? {
        print("Hello \(type(of: self)) \(properties.heading)")
        
        return nil
    }
}

class Label: Renderable {
    struct LabelProperties: Properties {
        let text: String
    }
    
    var properties: LabelProperties
    var children: [RenderTree]?
    
    static func createElement(text: String, _ children: [RenderTree]? = nil) -> RenderTree {
        print("create \(type(of: self))")
        
        return RenderTree(
            factory: Label.init,
            properties: LabelProperties(text: text),
            children
        )
    }
    
    required init(properties: Properties, children _: [RenderTree]?) {
        self.properties = properties as! LabelProperties
    }
    
    func render() -> [RenderTree]? {
        print("Hello \(type(of: self)) \(properties.text)")
        
        return nil
    }
}

func render(_ root: RenderTree) {
    root.factory(root.properties, root.children).render()
    
    render(root.children)
}

func render(_ children: [RenderTree]?) {
    if let children = children {
        for child in children {
            for (property, value) in Mirror(reflecting: child.properties).children {
                print("\(property!): \(value)")
            }
            
            child.factory(child.properties, child.children).render()
            
            render(child.children)
        }
    }
}

evaluate

let foo = "Some"
let bar = "labels"
let baz = "Some"
let bat = "more"

let tree = (
    <Scroll>
        <Section heading="Section one">
            <Label text{foo}/>
            <Label text{bar}/>
        </Section>
        <Section heading="Section two">
            <Label text{baz}/>
            <Label text{bat}/>
        </Section>
        <Section heading="Section tree"/>
    </Scroll>
)

render(tree)

the same as

let foo = "Some"
let bar = "labels"
let baz = "Some"
let bat = "more"

let tree = (
    Scroll.createElement([
        Section.createElement(heading: "Section one", [
            Label.createElement(text: foo),
            Label.createElement(text: bar),
        ]),
        Section.createElement(heading: "Section two", [
            Label.createElement(text: baz),
            Label.createElement(text: bat),
        ]),
        Section.createElement(heading: "Section tree"),
    ])
)

render(tree)

See JSX and https://reactjs.org/docs/jsx-in-depth.html for further examples and the rationale to why one would want to implement this, considering one could fake it with template literals.

Interested to hear your thoughts on it!

1 Like

I don't really see how this is syntactic sugar. It's not simplifying anything, it's just uses different symbols.

BTW you can use a varadiac argument in your constructor, which can let you omit the [ ] brackets around the array of children.

Sorry, I omitted crucial details for this thing to work. Updated the post accordingly. Thanks for pointing me to variadic arguments.

This still needs some work, though, as I am not sure how to design the "properties" type.

You might wanna take a look at swift-web, which embeds a domain specific language for HTML.

1 Like

That solves a different problem, though (creating HTML markup).

This library should be able to declaratively render views, compare with GitHub - nicklockwood/layout: A declarative UI framework for iOS. However, it would be desirable to statically interpolate with expressions.

Both solve the problem of declarative layout, just for different domains, no? The same declarative style could equally apply to creating a UIKit EDSL (which I and others have toyed with in the past).

To some extent, yes. The library you linked produces the markup in one single render pass, though, which simplifies the problem enormously.

A usable UI library would need to be able to update properties, compare the next render tree to the previous one, rerender only the sub trees that changed and execute the operations that are needed to reflect the change in the UI.

Definitely. Diffing and updating are an additional layer, but that layer shouldn't really impact our ability to model things directly in Swift :smiley:

A lot of folks have been playing around with the Elm Architecture, which models these problems and doesn't require an additional language preprocessor (unless you consider Elm itself a JavaScript preprocessor :wink:). I played around with it almost 2 years ago: https://twitter.com/stephencelis/status/757588606814912512

And there's been a lot of additional experimentation since:

I think it's good to consider our ability to make use of existing Swift features, especially when plain Swift functions and types seem to read nicely.

2 Likes

An operator based DSL is an alternative to mimicking XML syntaxes.

1 Like

IMO if there is syntatic sugar for such cases, it should be minimal. You should see what can be done today in code and maybe suggest some improvements. For example something like this is already possible:

Scroll(
    Section(heading: "Section one", children:
        Label(text: foo),
        Label(text: bar)
    ),
    Section(heading: "Section one", children:
        Label(text: foo),
        Label(text: bar)
    ),
    Section(heading: "Section three")
)

But implementing init methods for all these elements would be super annoying, so that's one are for improvement.

Thanks for those resources, they look awesome. Especially the one you showcased on twitter. Did you continue working on that or use it in any real world application?

Glad to see much tinkering on declarative layout. There doesn't seem to be any project yet that shows large traction, though.

I think the best strategy would be to come up with something that is a light abstraction, easily extensible and doesn't include any “magic“.

I wouldn't lock-in into a library that tries to do funky stuff with operator overloading and I don't think those will be maintained for much longer once the main developers abandon the project.

Fully agree with you.

I had the same thought and prototyped that. The class could provide an init method that is a tree node and contains the factory, properties and children (all used for diffing the tree and determining which components need to be rendered, mounted, unmounted). The factory could be provided by an inner class (the actual component) that contains an init method, lifecycle hooks and the render method.

That syntax would be pretty usable already.

An operator based DSL qualifies for all the conditions: it can be light, it is extensible and it is no more magical than any new syntactic sugar. Most importantly, it is already in the Swift language. The only thing it cannot do is being JSX alike. But does it have to be?

The reference to that particular framework was meant to illustrate the possibility of a tree-building DSL based on operators, not that any of us has to stick with the implementation of that particular framework.

Note this is not what React does. React delegates this to a factory object, not a static method on the referenced object. Once you start getting into React contexts it also starts to resemble a dependency injection framework.

However, such interfaces would be made more difficult since Javascript does not have early bound static typing.

IMHO the 'swifter' way to do this would be via code generation, a la partials with XAML or ASP.Net in CLR languages like C#. In this way you could also start implementing this for a web or application framework and gain some real world usage before evaluating first class language support.