Cannelle
Goal
The goal of Cannelle is to provide a well-reflected and performant templating solution.
Objectives
The CNL project objectives are:
- Many popular templating formats support to recycle existing content,
- Introduces a format optimized for Fuddle and Daniell,
- Provides an efficient serialization method that supports all templating formats and enables caching and faster execution than source-based templates,
- Provides an execution model and an engine that implements it that can be used in all other projects of FUDD ecosystem .
Reflecting about Templates
Templating is an old process to produce new items out of sketches of exiting items. A few thousand years ago, one equipped with a good collection of clay cylinders could pump up contracts on papyrus by inking and One could say that those almighty large language models we all praise and fear these days are just another kind of template, in which we drop a set of tokens in the slots and get back a statistical-driven assembly of matching tokens out.
In the early 90s HTML shows up, and Perl scripts provide the first step in assembling content as a sequence of function calls. Sadly, that's the model that React uses today. The httpd and later Apache servers URL routing is directory-based, so effectively the structure of a website is its on-disk directory structure.
WebObjects
But very quickly WebObjects appears; it is a completely different approach to content assembly. Backed by NeXT experience in advanced development tools, prototyping, object-oriented models for UI and database, it came from day one with the following features:
- Component-based architecture, with components being composable, containing both logic and content fragments,
- Separation of concerns, reducing the complexity of tracking logic effects between business and presentation layers,
- Template binary representation as a stream of serialized objects, for high performance loading and execution,
- Dynamic content binding that could reassign sources and sinks in different ways during content rendering, increasing component reusability and simplifying logic,
- Automatic creation of layouts from metadata descriptions (Direct-to-Web), eliminating all together the need for building templates,
- Serialization of component states at runtime, implicitely providing session management and client=server workflow synchronization,
- Distributed computing to spread workload and separate processing centers for quick operations and long jobs,
- Internationallization and adaptation to user preferences,
- Build/test/deploy integrated tools with high-end UI.
NeXT considered WebObjects to be an entreprise product and was selling it an entry price of 50K usd, and later Apple considered it mission-critical for its online store and counter-productive for its App concept so it simply stopped distributing it. Unfortunately very few people ever found out what the future of web application development was going to look like. Most of the features introduced by WebObjects eventually found their way in popular frameworks like Rails, React, NextJS and Phoenix. But still today no framework delivers such an integrated set of features as advanced as WebObjects was doing 30 years ago.
PHP
Instead PHP became the most popular way to build websites! For good reasons too: it was easily accessible both in terms of distribution and familiarity with scripting languages, one could quickly write a few echo
calls and get the feeling the entire website was going to be finished in a few minutes, and the MySQL and Apache support meant it was a no-brainer way to implement a data-driven site. Like so many other, I used PHP in early 2000s to implement my startup online retail presence that grew up to the million of requests per day, it was getting the job done.
PHP runs on the simplest template concept possible: there's is some HTML content, then some logic that may or may not create additional content, some more HTML, some more logic and so on. Almost nothing is implicit: one has to write the code load sub-components, to extract request parameters, to build a data context (careful about the mysql timeouts!), to add content to the output stream, to build up HTTP headers (careful about the ordering!), to define route structures within the content, and the list goes on. But it was interpreted and immediately activated by a browser's request on the web server, giving that amazing instant gratification of seeing code edits be reflected into a web page with just a simple refresh. It takes much longer to work that way, but it feels fast.
Eventually some blogging platform were written in PHP and eventually one of them really took off and today over 40% of the websites in operations are running on WordPress. One of the reason for WordPress popularity was that it essentially removed all templating activity for casual users but for one operation: supply the content, and WordPress will put it in the middle of a webpage, and organize many pages automatically. But on the other side, WordPress opened the door to themes, which effectively are hierarchies of templates. Creating a clear separation and market infrastructure for content and theme providers is probably the essence of WordPress phenomenal success, on which it also grew a software market with its plugin architecture.
React
While PHP's approach is to be an assembly language for websites , Ruby on Rails got some traction by introducing more high-level concepts. But it was still a server-side interpreted and dynamic rendering approach, which had the same issues of performance as PHP.
Around 2010 the JavaScript engine in browsers was fast enough to run client-side content creation logic, leading to the development of SPA frameworks like Angular, Elm, React and Vue. While Angular and Vue aimed for a more traditional PHP-in-the-browser approach to templating, Elm focused on high-level software engineering concepts such as MVC, state management and security through strong typing. React went simply for components, and that turned out to be the winning approach. The significant shortage of skilled software developers meant React implemented the solution to a latent market: an exchange of specialized components between higher-skilled developers that can write them and lower-skilled content creators that can aggregate them into online applications they can sell to businesses. Angular and Vue required too much software skills, Elm was for even more advanced developers... Like for WordPress, React was simplistic enough to be adopted by the majority of people producing web applications and then push the entire industry to work with it.
NextJS
React offers nothing else of templating systems; it is simply a library that executes functions doing DOM construction/editing in the browser. This limited feature set and performance issues started to be painful enough that demand for a better solution started to be loud and clear. The buzz for a return to fast static content, for the routing management to be simpler (implicit!), for easy deployable sites and so on found its champion in NextJS. NextJS still use React at its core, and then adds support for themes, for directory-and-files-as-routes, for server-side static content rendering, etc. The success of Vercel and its NextJS framework talks for itself, it is the way to go these days for building a static website or interactive web app.
NextJS isn't perfect though; it is still a React-based system which runs in a Javascript engine. Writing templates means writing Typescript code, merging lots of npm modules, and being creative to assess the end-result is what was hoped for. React was created for client-side use, and from time to time that blows in the face of a server-side script. There's no high-level site editor, nor a WordPress-like overall solution. Components are just functions and thus lack higher-level structures that could provide NextJS with more context on routing, content merging strategies or caching. Javascript for server-side execution isn't the best technology available for performance, security or productivity, but that's the only one that runs React (and thus NextJS sites/apps). The distributed processing capabilities of Javascript are simplistic. You get the idea...
It's possible to do better!
Building something good enough for the FUDD ecosystem
The Cannelle template system can study a lot of experience to implement its objectives. We know from WebObjects how a very advanced templating system can work, or the popularity of themes and separation of content and layouts from WordPress, or the strenght of the component model from React. We know we want to keep it simple to support the non-technical content providers, and we also want to convey high-level computational concepts to be able to derive automatically as much information as possible on the template to optimize its performance and consistency automatically rather than relying on developers to solve these issues.
First Cannelle has to support existing templating systems that have traction. That means being able to read a template in PHP, Jekyll, Jinja, Hugo, Gatsby and NextJS. It means to be able to understand the embedded logic of each of those template format to eventually execute it. The parsing is achieved with a mix of technologies: the Jinja templates are supported with the Ginger package, while the PHP, Jekyll, Hugo and React-based templates are first parsed by the TreeSitter grammars and then converted into an internal AST.
Then Cannelle has to be able to store templates in post-parsed/compiled format for fast retrival and activation. The serialized template format has to provide as much information as possible to the runtime system, such as external resources used, events listened for and routes exposed, context parameters expected and static vs dynamic fragments of content. This information is extracted from a descent into internal AST representation of the template. The serialized format is derived from the Java Virtual Machine (JVM) class file binary format; it uses a constant pool as its central data repository where verbatim strings are compressed, multiple tables that gives quick introspection capability to the template, and then blocks of bytecodes that express the template logic.
Then there's the engine! Cannelle defines a virtual machine (VM) that is also inspired by the JVM, but with a focus on efficient stream concatenation and a low-level data model and instruction set that supports both object-oriented and functional programming specifics. The VM supports also native function calls to use performant implementations of support libraries for the different template formats. Unlike the JVM, the Cannelle uses 32 bits as it base size data word, which simplifies Unicode support. The VM reuses the Haskell garbage collection and threading models, optimized over decades.
Equiped with an internal AST for each template format, a compiled template format and a VM for runtime, the last piece of the equation is to transform a template's logic into bytecode. For this Cannelle has a generic compiler and assembler for the VM bytecode, and a specialization of the compiler for each language in which the template formats express their logic. This compilation capability of each template format into the same executable model enables Cannelle to mix and match different template formats together at runtime. So for example a partial template written for Hugo can be used in a PHP template, and vice-versa, as long as the data required from the different templates is compatible.
Interestingly, Cannelle internal AST representation provides a node identification and serialization feature. This identification support means that nodes can be annotated or trees can be compared to do logic analysis and build automatic code conversion. EasyWordy uses that feature to do gradual conversion of PHP to Haskell/Fuddle code.
The Cannelle template system is a strong base for recycling existing content and integrating it into new projects. But that is just the starting point for the FUDD ecosystem.
Extending Cannelle capabilities
A modern and powerful templating system should be everything WebObjects was able to provide 30 years ago, and then some! To get there Cannelle needs to be much more than just compatible with popular template formats. It needs to support implicit routing definitions, distributed computing, adaptable client- and server-side computing depending on operating targets, internationalization and personalization. This is where the Fuddle extensions come to play.
Fuddle is an fork of the Elm language and runtime framework. Elm code is compiled to JavaScript for execution, and the Elm runtime expects for the most to run in a browser, ie Elm applications are client-side applications. Elm is a great piece of technology and well respected amongst the development community, having in fact inspired many other frameworks, React included... but it is rarely used for a variety of reasons. The most cited issues with Elm are that the project hasn't evolved since 2019 and that Elm rigourous rules for bug-free code is too opiniated for casual applications. Fuddle brings in some needed modernization to Elm, and removes some constraints so that many more situations can be tackled with Fuddle code. It also harmonizes a few details in Elm's syntax that lead to frustrations when switching between Haskell and Elm; Fuddle code can be copy-pasted into Haskell code and run.
Cannelle brings in all the previously mentioned features to Fuddle templates. Amongst other things, it means that Fuddle code can run in the Cannelle VM rather than as JavaScript. That frees Fuddle templates from requiring a JavaScript engine to execute.
There are 3 kinds of Fuddle templates. The first kind is the classic embedded logic one like in PHP, Hugo and Jekkyl templates, using the {{
and }}
syntax to switch between verbatim HTML and executable content.
The second kind is a much simplified approach to writing HTML based on Elm Html and Attributes libraries. In a conventional Elm application the content description is in fact a module with multiple function calls like in React. This it will be executed at runtime to create a DOM. In the second kind of Fuddle templating format, the VM does an immediate execution of all the HTML functions that are used in a static context to create the fragments of text that will be stored in the template binary format. This implements server-side rendering in a very effective and problem-free manner given the underlying advanced typing, immutability and decidability of Fuddle. So a Fuddle template in this format will look like:
body [ class "text-primary-700 dark:text-primary-300" ] [
div [ class "p-4" ] [
i [] [ text "Some text in italics." ]
]
]
As this contains no dynamic code, it will just become a single compact block of text in the compiled template. But even when using components in the template code, if Fuddle can detec that the components aren't using dynamic code, it will still convert the logic into a single block of text, as when the defaultFormats
is used, a function that doesn't depend on any runtime context:
body defaultFormats [
div [ class "p-4" ] [
i [] [ text "Some text in italics." ]
]
]
When a runtime dependant context is detected, the static logic before and after the dynamic function will be transformed into compact blocks of text so that only the dynamic code is actually executed at runtime and efficiently concatenated into the result stream. For example:
body defaultFormats [
div [ class "p-4" ] [
i [] [ someContextualMessage ]
]
]
Assuming that the function someContextualMessage
is relying on a dynamic value, the Fuddle template will result at runtime in the concatenation of a compact block of text that ends with the italic elment, the result of calling the someContextualMessage
funciton and another compact block of text that starts with the closing of the italic element.
The third Fuddle template format uses a short-hand notation to augment content with simple logic, such as variable dereferencing or simple if/then/else control on blocks of verbatim content. This is mostly used in code templates, such as Haskell project templates that are in fact a hierarchy of directories with code blocks that get assembled from a few configurations, like the stack new
feature. Cannelle only supports a single template parsing and rendering, Daniell is the tool that takes care of understanding a full hierarchy and processing it logically to obtain a complex final result.