template strings
Template strings solve a lot of text based feedback problems. Instead of having string builders and concatenations and memory allocation headaches, we can instead create a composite string using a template.
[self make-html: page] [self: Progam, page: Page -> String |
`<html>
<head>
<title>~[page name]</title>
<head>
<body>
~[page contents]
</body>
</html`]
But what if we wanted more control over what happens to the string bits? To solve this we'd not want a "String" but an array of strings. It also tells us that String is an interface rather than a concrete class.
That's no huge surprise - the kind of characters you have in a string dictate some of its capabilities. A byte string can always be indexed, while a utf-8 string needs to be searched.
If we define a TemplateString as an Array of: String then we can implement nice console outputs very easily.
[self print-page-summary: page] [self: Program, page: Page -> String |
`~[page timestamp] ~[page name] ~[page contents]` {
widths: {12, 20, 46),
padding: ' ',
overflow: '...'}].
pages do> [page: Page -> String | self print-page-summary: page]
copy> logger info-channel.
Now we're padding out each field from the TemplateString with spaces so that we get matching columns to make it look like a table. Very little code is necessary to achieve this.
This gives us some hints as to the nature of a TemplateString. Not only is it a set of pointers to other strings, but the compiler will slice and dice it for you and fill in two arrays: strings and fields. The fields are only the parts that were substituted in and the strings are all the bits - in this case 5 bits with the 2nd and 4th being a single space each.
We're also taking advantage of the 'focus' syntax {} which lets you send messages to an existing object just like you would with a new object:
jane: Person = {name: 'Jane'}.
bob := {Person | name: 'Bob'}.
bob {age: 30}.
copy-of-bob := {...bob, age: 31}.
We're also taking advantage of an as-yet described logging system. In almost all previous examples we've sent messages to stdout or stderr. What would be better is an abstract logging class where you specify what kind of event you're recording. Is it information? is it a notice? is it a warning? an error? a panic?
An event gets a timestamp and the logger can be configured to prepend every output with that timestamp. It might also sent the events to a database, or a remote logging server - or most likely to the console.
If the console knows ansi codes it can colourise them. It can also wrap at the timestamp width if the message goes over the console width. These quality of life touches make the environment pleasant to work in if you're used to running code from a terminal.
If we know the width of the console and the TemplateString is fully resolved when it is committed to a console then we can reasonable hope to specify percentages, rather than fixed character widths.
If we wanted to get really fancy we'd make an Array of: TemplateString and allow widths to be computed automatically.
But let's rewind a moment back to the notion of percentages. A percentage is an interpretation of a number, eg: {Percentage | value: 0.5}. It would be nice to have a simple syntax to make a percentage. It would be nice to have a simple syntax to have units too.
We're going to look at units as a core library soon. But for percentages we can either figure out how to make % a unary-message, or use a common abbreviation for percentage - pc.
`~[page timestamp] ~[page name] ~[page contents]` {
widths: {12, [width | width * 33pc - 12], 66pc),
padding: ' ',
overflow: '...'}
Something like that perhaps. It almost feels like CSS3's calc() except we're still writing a general programming language that is C low level. The only costs are the ones we want to pay for.
The only thing that'd make this better is the ability to label the fields we're filling in. So let's do that then. Instead of computing the field immediately we can provide a way for it to be computed later.
We don't yet have the concept of a Symbol in STZ. It's a very fundamental concept in Smalltalk-80. It's used to name methods and as unique values in pattern matching. The syntax for a Symbol is #foobar.
We could delve in to that world and add a syntax for symbols in STZ, but we don't actually need it - the weird thing about STZ is how a bunch of the program can be resolved at compile time. The part that matters most here is the type we're returning from the block. The type is resolved at compile time:
FancyTemplateString :: TemplateString options:
{widths: {'timestamp': 12}}.
output: FancyTemplateString =
`~[ -> Field name: 'timestamp' | page timestamp]`
Both TemplateString and Field subtype themselves and use their meta information to customise the code that writes the final output to the logger or any other io destination.