banner



What Is A Dsl Template

Introduction

In the last episode we introduced a new Swift EDSL for constructing HTML documents. It didn't take much work to go information technology going, and it showed a lot of hope. We were able to construct some circuitous documents very hands, and even perform transformations on those documents in the same way we might transform an array or whatever data structure in Swift.

Nonetheless, nosotros did not do a skillful chore of comparison the HTML DSL to perhaps the nearly popular mode of rendering HTML views: templating languages. Virtually every web framework in existence today uses templating languages to render HTML views. Definitely the most popular style of rendering HTML. Just we don't think it'south the best manner to go the job washed. Nosotros think that templating languages hide all types of fun compositions which tin make our views more reusable and easier to understand.

In this episode we will define what a templating language is, show off some popular ones, both outside the Swift community and inside, so hopefully prove the viewer that our tiny HTML DSL library that nosotros created last fourth dimension has a lot of benefits over the templating languages.

Templating languages

A templating language is an all new programming language that embeds itself within a plainly text document and so that it tin can emulate other languages. They are DSLs in their own style because they are highly tuned languages for outputting apparently text documents. The templating language will provide ways of using certain tokens for interpolating values into the document or adding logical constructs.

Let's demo ane such templating linguistic communication. There are a lot of templating languages out there, such equally Mustache, Handlebars, ERB, Haml, Stencil, Leaf, and more than. The ane we will be looking at is chosen Stencil. We chose this one because information technology's built in Swift and it fifty-fifty runs in playgrounds, but all of the other languages are quite like.

Nosotros'll start by importing Stencil and build a template using a string:

          import Stencil  let template = Template.init(stringLiteral: """ <header>   <h1>Hello!</h1> </header> """)        

Correction

We autocompleted the init(stringLiteral:) initializer, which probably shouldn't be invoked direct. Template comes with a more suitable init(templateString:) initializer.

And to utilize this template, we tin can call its return method.

          print(try template.render(null)) // <header> //   <h1>Hello!</h1> // </header>        

It consists of ii phases. You build the template, which is just a large ole string, and then you lot render the template by providing a dictionary of values that you want to interpolate. Currently we aren't using any interpolated values, so allow's add some.

          let template = Template.init(stringLiteral: """ <header>   <h1>Hello {{ proper name }}!</h1> </header> """)  impress(endeavor template.render(["name": "Blob"])) // <header> //   <h1>Hello Blob!</h1> // </header>        

So you see here that we use the token {{ name }} to signal something that tin be interpolated in at runtime. And this is how nosotros add customization to our templates.

Already, in that location is a small problem. This unabridged value exchange API is stringly-typed. A small-scale typo will crusade information technology to not piece of work how y'all look. If we misspell the interpolated value:

          let template = Template.init(stringLiteral: """ <header>   <h1>Howdy {{ nam }}!</h1> </header> """)  print(endeavour template.return(["proper noun": "Blob"])) // <header> //   <h1>Hello !</h1> // </header>        

We become a string back with no name. We get no indication that something went wrong. Not even a runtime error.

Misspelling a key in the return part does the same.

          let template = Template.init(stringLiteral: """ <header>   <h1>Howdy {{ name }}!</h1> </header> """)  print(endeavor template.return(["nam": "Hulk"])) // <header> //   <h1>Howdy !</h1> // </header>        

Again, no indication that something went wrong. It just rendered incorrectly.

That'southward not great. That makes refactoring with confidence nearly impossible, as you have no static guarantees that strings are updated properly.

Moving on, a feature that many templating languages offer are a concept unremarkably referred to every bit "filters", thought I'm not exactly sure why. They are basically transformations yous can perform on your interpolated values

For case, we can add together | uppercase to point that we want to uppercase the name when information technology is interpolated.

          allow template = Template.init(stringLiteral: """ <header>   <h1>Hullo {{ proper name | uppercase }}!</h1> </header> """)  print(try template.render(["name": "Blob"])) // <header> //   <h1>Hello BLOB!</h1> // </header>        

This is another stringly-typed API. Notation that we didn't go whatever autocomplete aid from Xcode, and that also means if we misspell it nosotros only find out at runtime. For example, we may accidentally type uppercased, which matches Swift'due south String API:

          let template = Template.init(stringLiteral: """ <header>   <h1>How-do-you-do {{ name | uppercased }}!</h1> </header> """)  impress(try template.render(["name": "Blob"])) // An error was thrown and was not caught: // - Unknown filter 'uppercased'. Plant similar filters: 'uppercase'        

The render call now threw an error at runtime, though at to the lowest degree it provided a helpful mistake message. This is a pretty big bummer, though. We are all used to our IDE catching simple errors like this for us earlier we are fifty-fifty allowed to run our app. This is one of the biggest problems with templating languages. Information technology's a whole new programming language, even so oftentimes without all the niceties that nosotros come up to expect when dealing with a fully fledged language, 1 that supports syntax highlighting, autocompletion, and static analysis.

One of the oft-touted pros of templating languages is that they are "logicless". However, that is basically never true.

Well-nigh every templating language provides many logic constructs for treatment control flow. For example, Stencil offers {% if %} tags:

          let template = Template.init(stringLiteral: """ {% if showName %} <header>   <h1>Hello {{ name | capital letter }}!</h1> </header> {% stop %} """)  print(try template.render(["name": "Blob"])) // An error was thrown and was not caught: // - Unknown template tag 'cease'.        

Whoops, we get a runtime error! And this time no helpful suggestions. Turns out that the end tags in Stencil friction match up with the opening tags, and so we wanted endif:

          let template = Template.init(stringLiteral: """ {% if showName %} <header>   <h1>Hullo {{ name | uppercase }}!</h1> </header> {% endif %} """)  impress(try template.render(["name": "Blob"]))        

Now it prints an empty string, which is expected, because nosotros never set showName to truthful.

We just need to set up showName to true in our dictionary.

          let template = Template.init(stringLiteral: """ {% if showName %} <header>   <h1>Hello {{ name | capital }}!</h1> </header> {% endif %} """)  impress(try template.render(["name": "Hulk", "showName": truthful])) // <header> //   <h1>Hi Blob!</h1> // </header>        

So there nosotros take information technology. Templates practice typically have logic, and we see once once more that this syntax is another opportunity for a typo-based runtime error.

Templating languages too have looping. Allow's say we wanted to output an HTML listing of user names:

          let template = Template.init(stringLiteral: """ <ul>   {% for user in users %}     <li>{{ user }}</li>   {% endfor %} </ul> """)  print(try template.render(["proper name": "Blob", "showName": true])) // <ul> // // </ul>        

Well, showtime nosotros need to pass a listing of users.

          let template = Template.init(stringLiteral: """ <ul>   {% for user in users %}     <li>{{ user }}</li>   {% endfor %} </ul> """)  print(try template.render(["users": ["Blob", "Hulk Jr.", "Blob Sr."]]) // <ul> // //    <li>Hulk</li> // //    <li>Blob Jr.</li> // //    <li>Blob Sr.</li> // // </ul>        

Everything rendered, but kind of strangely. All of the extra newlines and spacing are kinda gross, but there's really no nice manner to get rid of them because they are a part of the template.

We could play around with the template in an try to brand rendering nicer, but then the template itself becomes much more than difficult to read.

          let template = Template.init(stringLiteral: """ <ul>{% for user in users %}     <li>{{ user }}</li>{% endfor %} </ul> """)  print(effort template.render(["users": ["Blob", "Hulk Jr.", "Blob Sr."]]) // <ul> //    <li>Blob</li> //    <li>Blob Jr.</li> //    <li>Blob Sr.</li> // </ul>        

This unfortunately optimizes for rendering templates over reading them. We demand to maintain this code, and so this mayhap optimizes for the wrong thing.

This problem is common enough in templating languages, that some, like ERB, provide special tag annotations for truncating leading or trailing whitespace. Fifty-fifty then, this is a lot of extra mental piece of work to try to brand templates render nicely. We've both written a lot of ERB and couldn't accurately describe how this truncation behavior works.

Stencil also supports dot-syntax to reference fields in a larger structure. So if users contained structs or dictionaries, we could reference fields in those structures, say like user.name:

          let template = Template.init(stringLiteral: """ <ul>   {% for user in users %}     <li>{{ user.name }}</li>   {% endfor %} </ul> """)  print(try template.return(["users": ["Blob", "Blob Jr.", "Blob Sr."]]) // 🛑 error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION        

Ok, well this is very concerning. Nosotros now have a runtime crash. Non merely an error being thrown which we can grab. This ways that it could take down our server. We haven't withal updated the data being fed into the template, so when it tries to access name on the strings we're passing through, it presumably can't reconcile trying to find the proper name field of a cord, and something in the library is causing a crash.

Correction

We opened a GitHub issue nigh this crash when we showtime released this episode, and a fix was merged merely a few days later.

We can set this, merely it's still pretty scary to think a crash could creep upwards so easily:

          permit template = Template.init(stringLiteral: """ <ul>   {% for user in users %}     <li>{{ user.name }}</li>   {% endfor %} </ul>  print(effort template.render(   [     "users": [       ["name": "Hulk"],       ["name": "Blob Jr."],       ["name": "Blob Sr."]     ]   ] )) // <ul> // //    <li>Blob</li> // //    <li>Blob Jr.</li> // //    <li>Blob Sr.</li> // // </ul>        

We're seeing, aslope another feature of templating languages, some other precipitous border.

We've now seen a few features of templating languages: interpolation, logic, loops, and handling complex data structures. Each feature had its caveats of increasing business organization: all runtime errors.

DSLs

Given the problems with templating languages, what is an alternative? Well, of course DSLs! Last episode we sketched the beginning of an HTML DSL, and even open up sourced a total library for the DSL. Let's see how this embedded DSL solves a lot of the problems that templating languages take.

Allow's start easy. How do nosotros interpolate values into the DSL and then that we can dynamically build HTML? Well, since the DSL is just basic Swift types, there's nothing stopping united states from just sticking a value, whether it be a literal or variable, directly into a DSL value:

          let name = "Blob" impress(   render(     header([       h1([.text(name)])       ])   ) ) // <header ><h1 >Hulk</h1></header>        

What's cool hither is that this is only Swift code, and Swift code has to exist compiled. If we introduce a typo in our template, nosotros get an error at compile time.

          let proper name = "Blob" impress(   render(     header([       h1([.text(name)])       // 🛑 Use of unresolved identifier 'nam'; did you mean 'name'?       ])   ) )        

And if we misspell our data, we get a similar compiler error.

          let nam = "Hulk" impress(   render(     header([       h1([.text(name)])       // 🛑 Use of unresolved identifier 'name'; did you mean 'nam'?       ])   ) )        

The mere fact that our DSL is written in Swift has already solved this problem that templating languages have.

I matter I kinda liked about the template style of doing this, even so, was that y'all treated the template kinda like a function in which we fed in all the data the template needed. We love functions here on Point-Free, so allow'south enhance this lil flake of HTML to be a function:

          func greet(name: String) -> Node {   return header([     h1([.text(name)])     ]) }        

And generating an HTML node from this role is a simple as invoking it:

          greet(name: "Blob") // el("header", [], [el("h1", [], [text("Blob")])])        

This is starting to look more similar that template style. We provide a function that acts as a template, and later we become to substitute in values by calling the part with data.

A feature that virtually templating languages take is "filters", which permit you to transform interpolated values before they are put into the template. Well, we don't need special support for that considering we can immediately utilise any function or method in Swift direct on our values.

          func greet(name: String) -> Node {   return header([     h1([.text(name.uppercased())])     ]) }  impress(render(greet(proper noun: "Blob"))) // <header ><h1 >Blob</h1></header>        

Over again there is no fashion to misspell this method: nosotros go a compiler fault if we do so. And because nosotros're in Xcode, we also accept autocomplete and instant access to method documentation.

Even better, in the template world, if you want a transformation that is not one of the default filters provided by the language, you have to go through a lengthy process to "register" your filter through a plug-in system. This becomes tiresome when all y'all wanna exercise is pass a value to a function! In a DSL we can just work inside Swift and define methods and functions as we cull.

What about logical constructs and control flow? Again, Swift beingness a fully featured language we get instant admission to everything Swift has to offer and can enhance our HTML views.

Here's a common state of affairs: nosotros take some HTML that should be rendered only if some status is met. Possibly its a view that takes a user, but only renders its contents when the user is an admin:

          struct User {   permit name: String   let isAdmin: Bool }  func adminDetail(user: User) -> Node {   guard user.isAdmin else { ??? }   return header([     h1([.text("Welcome admin: \(user.proper noun)")])     ]) }        

We can use the guard construct in Swift, but what should we render in the else case? We could make the return blazon optional Node?, or nosotros could even alter our DSL to business relationship for the concept of "empty" HTML. However, call back that our DSLs element case took in an array of nodes, an assortment has a very natural "empty" state…it's merely the empty assortment! Then let's upgrade this view to return an assortment of nodes:

          func adminDetail(user: User) -> [Node] {   guard user.isAdmin else { render [] }   return [     header([       h1([.text("Welcome admin: \(user.name)")])       ])   ] }        

What's cool most this is that we could utilise a baby-sit statement, which forces us to return in the else block. This is a nice control menses construction that Swift has, and we become to utilise it for complimentary. I'm not even aware of any templating languages that have such a feature.

Let'due south besides plug some users into this part:

          let user = User(name: "Blob Jr.", isAdmin: imitation) return(adminDetail(user: user)) // 🛑 Cannot convert value of blazon '[Node]' to expected argument type 'Node'        

Alright, render expects a node, just nosotros're returning an assortment of nodes. It seems very natural to have an overload of render that takes an array of nodes. And because we're working in Swift, we can add this functionality ourselves.

          func render(_ nodes: [Node]) -> String {   return nodes.map(render).joined() }        

We just map over our array with the original render role before joining our rendered nodes into a single string.

          print(render(adminDetail(user: User(name: "Blob Jr.", isAdmin: faux))))        

We get an empty string here, which makes sense because the user isn't an admin.

What nearly for an admin user?

          print(return(adminDetail(user: User(name: "Blob Sr.", isAdmin: true)))) // <header ><h1 >Welcome admin: Blob Sr.</h1></header>        

It'south pretty amazing that nosotros're able to work inside the language we're already comfortable in, Swift. We're free of the friction and problems nosotros were seeing in templating languages. When we run our code it either works or the compiler guides united states through fixes.

We likewise saw that templates typically take looping constructs, and that can be nice for building up large, complex documents. Well, Swift has tons of looping constructs.

Permit's define a office that returns a list of users, like our earlier template. Near of our looping can be achieved with map.

          func users(_ names: [String]) -> Node {   return ul(names.map { name in li([.text(proper name)]) }) }        
          impress(render(users(["Blob", "Blob Jr.", "Blob Sr."]))) // <ul ><li >Hulk</li><li >Blob Jr.</li><li >Blob Sr.</li></ul>        

We're just living in the world of Swift and we're able to render more and more than complex information hands by just writing and calling functions.

We're all familiar with Swift and nosotros can leverage all that cognition to write HTML.

Considering we're working with Swift code, nosotros can besides see things that might exist more than difficult to see in templating languages: code reuse.

Permit's extract out that lil bit of view logic in the map into its own dedicated view part:

          func users(_ names: [Cord]) -> Node {   return ul(users.map { proper noun in userItem(name) }) }  func userItem(_ name: String) -> Node {   return li([name]) }  print(render(users(["Blob", "Blob Jr."]))) // <ul ><li >Blob</li><li >Hulk Jr.</li><li >Blob Sr.</li></ul>        

It builds and works exactly the aforementioned, only nosotros're now working with a reusable unit of code that we extracted out of the original office.

On Indicate-Free we like to work with the [point-free way of programming], which allows us to fully get rid of mentioning the name from the names nosotros're mapping over and supply the userItem function directly to the map.

          func users(_ names: [Cord]) -> Node {   render ul(users.map(userItem)) }        

And this is super declarative. We've too stumbled upon an example of reusability in our HTML views. Nosotros tin just excerpt any lil subview to its own function, and then invoke that function to go all of its DOM.

We haven't yet seen this kind of code reuse with templating languages. Information technology's technically possible, but it's very convoluted. We aren't going to give all the details, but essentially you can refer to another template using the include tag.

          let template = Template.init(stringLiteral: """ <ul>   {% for user in users %}     {% include "userItem" user %}   {% endfor %} </ul> """)        

Here nosotros used include "userItem" user to bespeak that we want to include the contents of the userItem template, and laissez passer along the user value to that template.

And if we call the template.

          impress(try template1.render(["users": ["Blob", "Blob Jr."]])) // An error was thrown and was not caught: // Template named `userItem` does not be. No loaders found        

Now, this is currently error-ing because it doesn't know what we hateful past referencing "userItem".

To prepare this we need the concept of a "loader" then that it knows how to load external templates. There are many kinds of loaders, including those that load things from disk or from memory. We tin can provide a unproblematic in-memory template loader similar then:

          class MemoryTemplateLoader: Loader {   func loadTemplate(name: Cord, surroundings: Environment) throws -> Template {     if name == "userItem" {       render Template(templateString: """ <li>{{ user }}</li> """, environment: environs)     }      throw TemplateDoesNotExist(templateNames: [proper noun], loader: self)   } }        

And so, in social club to employ this template loader we must create an environs with the loader specified:

          let environment = Environs(loader: MemoryTemplateLoader())        

And finally, we use this environment to render our template, instead of the template itself. Information technology doesn't appear to be possible to specify an environment when calling return on a template directly:

          permit template1 = """ <ul>   {% for user in users %}     {% include "userItem" user %}   {% endfor %} </ul> """  print(   try environment.renderTemplate(     cord: template,     context: ["users": ["Hulk", "Hulk Jr."]]   ) )        

We got it working, but with a lot of ceremony. And while typically y'all wouldn't practice this much work because the library you are using does some of it for yous, like Vapor, Kitura, or Sourcery, but it still speaks to the complexity of this approach. Virtually of the complexity is coming from the fact that none of the templates lawmaking lives in Swift, and and so we are constantly having to invent new solutions to bug that popular up.

And template loaders are yet another feature with a potential runtime error, similar if we were to misspell a template proper noun or provide an invalid value. Role application doesn't take these problems.

And in fact, include statements in Stencil are aught more than convoluted role application. And every templating language has this concept. Rails calls it "partials", Django calls it "fragments", etc. All of those are just instances of function awarding, just have the same problems.

What's the point?

This has been informative, but information technology'due south probably a good time to ask: "what's the point?" Why are we even talking most templating languages? This is a video series on functional programming later all!

Well, the manner we await at it, server-side Swift is going to continue to abound in popularity, and nosotros feel that information technology's important to question some of the long-held best practices equally Swift rises to prominence. Templating languages are definitely an accustomed all-time practice, even with all their faults. They probably became popular in the early on days of web development because they are infinitely flexible, albeit complex, and well-nigh languages do not have the kinds of features that make DSLs really nice, so it was like shooting fish in a barrel to miss.

Withal, Swift has proper algebraic data types, which are an important ingredient for DSLs, and we can take inspiration from functional programming to try to re-envision solutions to some of the spider web's bug. Past doing this nosotros were able to accomplish most of what templating languages offer, while solving a lot of their problems, and nosotros even get to do things that templating languages take no answer for.

For case, since our DSL is just made of elementary Swift data types, nosotros tin can transform information technology just like we would an array or a dictionary in Swift. Let's testify this off by cooking upwardly a really simple transformation.

Let's cook up a function that transforms an HTML certificate past replacing all of its text nodes with redacted versions of the text.

          func redacted(_ node: Node) -> Node {   switch node {   case permit .el(tag, attrs, children):     return .el(tag, attrs, children.map(redacted))   case let .text(cord):     return .text(       string         .split up(separator: " ")         .map { String(repeating: "█", count: $0.count) }         .joined(separator: " ")     )   } }        

Allow'due south take an example from last time and take a await at information technology in a live view.

          import WebKit import PlaygroundSupport  let md = header([   h1(["Indicate-Complimentary"]),   p([id("blurb")], [     "Functional programming in Swift. ",     a([href("/about")], ["Acquire more"]),     "!"     ]),   img([src("https://pbs.twimg.com/profile_images/907799692339269634/wQEf0_2N_400x400.jpg"), width(64), height(64)])   ])  allow webView = WKWebView(frame: .init(10: 0, y: 0, width: 360, pinnacle: 480)) webView.loadHTMLString(render(medico), baseURL: nil) PlaygroundPage.electric current.liveView. = webView        

There's our document! What's it await like to redact it?

          webView.loadHTMLString(return(redacted(doc)), baseURL: nil)        

And there it is, safe from prying eyes. Now what if nosotros want to redact the image? Well, we can pattern match on img elements, strip the src, and render a black background.

          case let .el("img", attrs, children):   render .el(     "img",     attrs.filter { attrName, _ in attr != "src" }       + [("style", "background: black")],     children   )        

And when we render information technology, the prototype is redacted, every bit well! And just to make sure the src has been removed, allow's look at the raw markup.

          print(return(redacted(doc))) // <header ><h1 >██████████</h1><p id="blurb">██████████ ███████████ ██ ██████ <a href="/about">█████ ████</a>█</p><img width="64" height="64" style="background: black"></img></header>        

Templating languages accept no respective story for this kind of transformation. There is no way to concisely traverse over a template certificate considering ultimately it simply gets rendered into a plainly text file. You lose all of the structure. You lot typically need to bring in a whole other library that parses the HTML before you can make any such transformation earlier rendering it dorsum to a string again. Why do all that work when y'all can start with this structure in the first place, manipulate it as much as you lot want, and and then finally, at the last moment, interpret it. And this is the entire indicate of DSLs!

Then, nosotros recollect this is the signal to contrasting DSLs with templating languages. DSLs are simple to understand, like shooting fish in a barrel to build, and provide a lot of benefits over traditional methods of solving the same problems. And it'southward all based on ideas we've seen in functional programming.

At present this isn't the last we have to say almost the HTML DSL. At that place are even more benefits to using DSLs over templating languages, and it has to exercise with how to compose views.

Until next time!

This episode is free for everyone.

Subscribe to Point-Gratuitous

Access all by and future episodes when yous become a subscriber.

What Is A Dsl Template,

Source: https://www.pointfree.co/episodes/ep29-dsls-vs-templating-languages

Posted by: alonsosook1999.blogspot.com

0 Response to "What Is A Dsl Template"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel