Templates are functions
Over the last decade or so of web development, templates have developed from a side show to a major pillar of developer religion. No matter what your web-stack faith, some form of templating ideology exists. Despite the wide range of belief there is a conceptual kernel they all share: A template is a function of some context that returns an HTML document.
Map<String, Object> → HTMLDoc
These functions are typically bound by convention (and often implementation language restriction) to avoid side effecting operations such as database queries or updates. This restriction makes templates an instance of pure functions.
Historical and organisational concerns have caused templating systems to develop common traits:
- No business logic.
- Simplified/restricted syntax and semantics.
- Some notion of declarative programming.
The rest of this article looks at those traits in the context of pure functions.
The cottage industry of producing new templating systems has tended to be an iterative process, slowly refining the concept. One commonality is that the lion's share of the developers of these systems have learnt from the mistakes of PHP and want to avoid the spaghetti nightmare of business code and template being intermingled. The other commonality is that these languages have arisen in the context of the dominant paradigm of the industry: procedural and object oriented code. The result is a greenspunning of functional-esq declarative languages.
It is my opinion that if templating systems for procedural languagesi were built around defining templates as pure functions, templating would have greater reuse, and allow more expressive, declarative templatesii without sacrificing reasonabilityiii.
Returning to pure functions, let's examine what they provide:
- A restriction on the extent of any executed code. A pure function only ever returns something. Anytime a pure function call appears with a given set of arguments, you can replace it with the result of that function call and it will be correct (this is known as referential transparency). This satisfies our requirement that business logic is kept out of templates (at least as much so as for instance in Django's templates where object methods, template tags and filters can all poke holes in the layering).
- A clear unit of code: the function. The unit of code in many existing templating systems is the template file. Common methods of reuse and composition are import/include and inheritance. Files are a very coarse grained unit and, worse, many templating systems suffer from unbounded context. In other words, when you include a template in another template the context of the included template is whatever the context is at the point of inclusion. Django solves this (for instance) with inclusion tags which require the addition of a Python function to bind context and create a template scoped function. This limiting of the bounds of template context is vital to reasoning about a template.
These two properties allow us to describe chunks of our template and reuse them without fear: no side effects will occur, no violation of our system's areas of responsibility will take place, and we can always reason about the templates.
What about declarative templates? The canonical example in functional programming is map
and filter
:
function inc (x) {
return x + 1;
}
function is_odd (x) {
return x % 2 == 1;
}
map(inc, [1, 2, 3]); //=> [2, 3, 4]
filter(is_odd, [1, 2, 3]) //=> [1, 3]
map(inc, filter(is_odd, [1, 2, 3])) //=> [2, 4]
The important aspect here is that functions are passed to other functions, allowing the mechanics
of how an array is processed to be internal to map
and filter
, and the caller only describe what to do.
To extend this to templating, we would be passing templates to other templates/functions. For example, the Jinja2 templating languageiv for Python has a macro
tag that allows you to define first class templates in your template file. These first class templates output whatever their body evaluates to. For example, map above could be written as:
from jinja2 import Template
t = Template("""
{% macro inc(x) %}
{{ x + 1}}
{% endmacro %}
{% macro map(t, l) %}
{% for i in l %}
{{ t(i) }}
{% endfor %}
{% endmacro %}
{{ map(inc, [1, 2, 3]) }}
""")
print t.render()
# 2
# 3
# 4
In this example, map
and inc
are both first class templates, and map
is paramaterized with inc
. While the example trivialises this, in practise it can be very powerful. This kind of paramaterization allows you to dramatically reduce repeated code.
However, because these macros output their body, the effective return type is always approximately string
. As a result, we cannot create a pipeline of map
and filter
as seen in the javascript example above. This implies that while first class templates are a powerful addition, we are still operating in a space that is more restricted than full pure functions.
What would you gain by allowing any pure function be defined within a template? Template authors could trivially create declarative operations to support the domain of their site, rather than either
- limiting themselves to the primitive declarations provided by the system authors
- stepping outside the templates to write new declarations in a non-declarative host language, and provide the appropriate boilerplate for them to be available within the template language.
A simple hypothetical here would be taking a set of common filtering operations and a composition function to create a single filter that is then used throughout the codebase. This might look something like:
{% define summarize=compose(safe, trim_words_html(30)) %}
…
{{ post.body|summarize }}
The argument against this is “Simplicity”. However, this argument favours a surface level simplicity that is anti-abstraction. Being able to define domain specific abstractions is the cornerstone of software development; why are we excluding this from templating!
Templating systems in an Procedural / OO environment are a functional microcosm. Pure functions should be the basis for any templating system; all other concerns are a side-show and in general fall out of using the functional model. Starting with a well understood and coherent model allows greater reuse.
Any time a templating system makes a decision with its templates that is less expressive than composition and application of pure functions, I believe it is worth evaluating whether that trade off is worth it. Everyone has different requirements, but I want templates that encourage abstraction, reuse, and composition over artificial simplicity and feeding the sacred cows.
- Users of functional languages should consider if they really need templates to look like those of procedural languages: If you already have pure functions, why reinvent them. If you are familiar with Clojure, you will know that the two most popular templating systems – Hiccup and Enlive – are dramatically different to what you might find in (for example) Python.
- Anecdotal observation: declarative APIs in OO often allow use a predetermined set of declarations to describe your problem, but its not uncommon to encounter difficulty creating new declarations. In functional programming (a great example is parser combinators) you can define new declarations easily, often as a composition of existing declarations.
- I think of reasonability as an informally term meaning that the code is easy to reason about. You don’t need to execute the code (with or without a debugger) to understand it.
- If you use Django, and have not yet explored Jinja2 as an alternative to Django’s templates, I would urge you to do so. In my opinion it is a great improvement over Django’s defaults, while maintaining a consistent flavour.