Advanced features
Understanding value types
Every value has a type in Cottle, even if you usually don’t have to worry about it (see Value type for details). Functions that expect arguments of specific types will try to cast them silently and fallback to undefined values when they can’t. However in some rare cases you may have to force a cast yourself to get desired result, for example when accessing values from a map:
{set map to ["first", "second", "third"]}
{set key to "1"}
{dump map[key]}
<void>
You could have expected this template to display “second”, but Cottle actually searches for the map value associated to key "1" (as a string), not key 1 (as a number). These are two different values and storing two different values for keys "1" and 1 in a map is valid, hence no automatic cast can be performed for you.
In this example, you can explicitly change the type of key to a number by using built-in function cast(value, type). Also remember you can use the Debug: dump command to troubleshoot variable types in your templates.
Variables immutability
All variables in Cottle are immutable, meaning it’s not possible to replace a section within a string or change the value associated to some key in a map. If you want to append, replace or erase a value in a map you’ll have to rebuild a new one where you inject, filter out or replace desired value. There are a few built-in functions you may find handy to achieve such tasks:
cat(a, b, …) and union(map1, map2, …) can merge strings (
catonly) or maps ;slice(subject, index[, count]) can extract part of a string or a map ;
except(map1, map2, …) can extract the intersection between two maps.
Here are a few examples about how to use them:
{set my_string to "Modify me if you can"}
{set my_string to cat("I", slice(my_string, 16), ".")}
{dump my_string}
{set my_array to [4, 8, 50, 90, 23, 42]}
{set my_array to cat(slice(my_array, 0, 2), slice(my_array, 4))}
{set my_array to cat(slice(my_array, 0, 2), [15, 16], slice(my_array, 2))}
{dump my_array}
{set my_hash to ["delete_me": "TODO: delete this value", "let_me": "I shouldn't be touched"]}
{set my_hash to union(my_hash, ["append_me": "I'm here!"])}
{set my_hash to except(my_hash, ["delete_me": 0])}
{dump my_hash}
"I can."
[4, 8, 15, 16, 23, 42]
["let_me": "I shouldn't be touched", "append_me": "I'm here!"]
Function declaration
Cottle allows you to declare functions directly in your template code so you can reuse code as you would do with any other programming language. To declare a function and assign it to a variable, use the same set command you used for regular values assignments (see section Assignments: set) with a slightly different syntax. Function arguments must be specified between parenthesis right after the variable name that should receive the function, and the to keyword must be followed by a “:” (semicolon) character, then function body declaration as a Cottle template.
Functions can return a value that can be used in any expression or stored in a variable. To make a function halt and return a value, use the return command within its body:
{set factorial(n) to:{
if n > 1:{
return n * factorial(n - 1)
}|
else:{
return 1
}}
}
Factorial 1 = {factorial(1)}
Factorial 3 = {factorial(3)}
Factorial 8 = {factorial(8)}
{set hanoi_recursive(n, from, by, to) to:{
if n > 0:
{hanoi_recursive(n - 1, from, to, by)}
Move one disk from {from} to {to}
{hanoi_recursive(n - 1, by, from, to)}
}}
{set hanoi(n) to:{
hanoi_recursive(n, "A", "B", "C")
}}
{hanoi(3)}
Factorial 1 = 1
Factorial 3 = 6
Factorial 8 = 40320
Move one disk from A to C
Move one disk from A to B
Move one disk from C to B
Move one disk from A to C
Move one disk from B to A
Move one disk from B to C
Move one disk from A to C
You can see in this example that returning a value and printing text are two very different things. Plain text within function body is printed each time the function is called, or more precisely each time its enclosing block is executed (that means it won’t print if contained in an if command that fails to pass, for example).
The value returned by the function won’t be printed unless you explicitly require it by using the echo command (e.g. something like {factorial(8)}). If a function doesn’t use any return command it returns an undefined value, that’s why the call to {hanoi(3)} in the sample above does not print anything more than the plain text blocks it contains.
Variable scope
When writing complex templates using nested or recursive functions, you may have to take care of variable scopes to avoid potential issues. A scope is the local evaluation context of any function or command having a body. When assigning a value to a variable (see section Assignments: set for details) all variables belong to the same global scope. Consider this template:
{set depth(item) to:{
set res to 0 |
for child in item:{
set res_child to depth(child) + 1 |
set res to max(res, res_child)
}|
return res
}}
{depth([["1.1", "1.2", ["1.3.1", "1.3.2"]], "2", "3", ["4.1", "4.2"]])}
The depth function is expected to return the level of the deepest element in a value that contains nested maps. Of course it could be written in a more efficient way without using non-necessary temporary variables, but it would hide the problem we want to illustrate. If you try to execute this code you’ll notice it returns 2 where 3 would have been expected.
Here is the explanation: when using the set method to assign a value to variable res it always uses the same res instance. The depth function recursively calls itself but overwrite the unique res variable each time it tries to store a value in it, and therefore fails to store the actual deepest level as it should.
To solve this issue, the res variable needs to be local to function depth so that each invocation uses its own res instance. This can be achieved by using the declare command that creates a variable in current scope. Our previous example can then be fixed by declaring a new res variable inside body of function depth, so that every subsequent reference to res resolves to our local instance:
{set depth(item) to:{
declare res |
set res to 0 |
for child in item:{
set res_child to depth(child) + 1 |
set res to max(res, res_child)
}|
return res
}}
{depth([["1.1", "1.2", ["1.3.1", "1.3.2"]], "2", "3", ["4.1", "4.2"]])}
You could even optimize the first set command away by assigning a value to res during declaration ; the declare command actually supports the exact same syntax than set, the only difference being than “to” should be replaced by “as”:
{declare res as 0}
The same command can also be used to declare functions:
{declare square(n) as:{
return n * n
}}
Note that the set command can also be used without argument, and assigns variable an undefined value (which is equivalent to reset it to an undefined state).
Native .NET functions
If you need new features or improved performance, you can assign your own .NET methods to template variables so they’re available as Cottle functions. That’s actually what Cottle does when you use Context.CreateBuiltin method: a set of Cottle methods is added to your context, and you can have a look at the source code to see how these methods work.
To pass a function in a context, use one of the creation methods from Function class, then pass it to Value.FromFunction method to wrap it into a value you can add to a context:
Testing custom "repeat" function:
{repeat("a", 15)}
{repeat("oh", 7)}
{repeat("!", 10)}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["repeat"] = Value.CreateFunction(Function.CreatePure2((state, subject, count) =>
{
var builder = new StringBuilder();
for (var i = 0; i < count; ++i)
builder.Append(subject);
return builder.ToString();
}))
});
Testing custom "repeat" function:
aaaaaaaaaaaaaaa
ohohohohohohoh
!!!!!!!!!!
Static class Function supports multiple methods to create Cottle functions. Each method expects a .NET callback that contains the code to be executed when the method is invoked, and some of them also ask for the accepted number of parameters for the function being defined. Methods from Function are defined across a combination of 2 criteria:
- Whether they’re having side effects or not:
Methods
Function.CreateNative*(e.g.Function.CreateNativeExact) are allowed to perform side effects but will be excluded from most optimizations. Their callbacks receive aIRuntimeinstance to access runtime information such as global variables, and a IO.TextWriter instance so they can write any text contents to it.Methods
Function.CreatePure*(e.g.Function.CreatePureVariadic) must be pure functions having no side effect and not relying on anything but their arguments. This assumption is used by Cottle to perform optimizations in your templates.
- How many arguments they accept:
Methods
Function.Create*Exact(e.g.Function.CreateNativeExact) expect a fixed number of arguments when invoked or return an undefined value otherwise.Methods
Function.Create*MinMax(e.g.Function.CreatePureMinMax) accept betweenminandmaxarguments or return an undefined value otherwise.Methods
Function.Create*N(e.g.Function.CreateNativeN) only accept exactlyNarguments or return an undefined value otherwise.Methods
Function.Create*Variadic(e.g.Function.CreatePureVariadic) accept any number of arguments, it is the responsibility of provided callback to validate this number.
Here are the arguments received by the callback you’ll pass to these methods:
First argument is either a
IRuntimeinstance (for non-pure functions) or an opaque state (for pure ones) that must be forwarded to any nested function call ;Next arguments are either a list of values (for functions accepting variable number of arguments) or separate scalar values (for functions accepting a fixed number of arguments) received as arguments when invoking the function ;
Last argument, for non-pure functions only, is a IO.TextWriter instance open to current document output.
Lazy value evaluation
In some cases, you may want to inject to your template big and/or complex values that may or may not be needed at rendering, depending on other parameters. In such configurations, it may be better to avoid injecting the entire value in your context if there is chances it won’t be used, and use lazy evaluation instead.
Lazy evaluation allows you to inject a value with a resolver callback which will be called only the first time value is accessed, or not called at all if value is not used for rendering. Lazy values can be created through implicit conversion from any Func<Value> instance or by using Value.FromLazy construction method:
{if is_admin:
Administration log: {log}
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["is_admin"] = user.IsAdmin,
["log"] = () => log.BuildComplexLogValue() // Implicit conversion to lazy value
});
document.Render(context, Console.Out);
In this example, method log.BuildComplexLogValue won’t be called unless is_admin value is true.
Reflection values
Instead of converting complex object hierarchies to Cottle values, you can have the library do it for you by using .NET reflection. This approach is somehow slower than creating Cottle values manually but as it’s a lazy mechanism it may be a good choice if you have complex objects and don’t know in advance which fields might be used in your templates.
To use reflection, invoke Value.FromReflection method on any .NET object instance and specify binding flags to indicate which members should be made visible to Cottle. Fields and properties resolved on the object will be accessible like if it were a Cottle map. Instances of types that implement IDictionary<TKey, TValue>, IReadOnyDictionary<TKey, TValue> or IEnumerable<TElement> will have their key/value or index/element pairs transformed into Cottle maps.
Current culture is {culture.DisplayName} with keyboard layout ID {culture.KeyboardLayoutId}.
{for key, value in culture:
{if cast(value, 's'):
{key} = {value}
}
}
var context = Context.CreateBuiltin(new Dictionary<Value, Value>
{
["culture"] = Value.FromReflection(CultureInfo.InvariantCulture, BindingFlags.Instance | BindingFlags.Public)
});
Current culture is Invariant Language (Invariant Country) with keyboard layout ID 127.
LCID = 127
KeyboardLayoutId = 127
DisplayName = Invariant Language (Invariant Country)
NativeName = Invariant Language (Invariant Country)
EnglishName = Invariant Language (Invariant Country)
TwoLetterISOLanguageName = iv
ThreeLetterISOLanguageName = ivl
ThreeLetterWindowsLanguageName = IVL
IsReadOnly = true
Warning
Using reflection values has a negative impact on execution performance compared to regular values. Prefer explicit conversions to Value instances unless performance is not relevant for your application.
Spying values
Added in version 2.0.5
If you’re working with many templates you may lose track of what variables are used and how. This is where the spying feature can come handy: it allows gathering information on each variable referenced in a template and their associated values. To use this feature, start by wrapping a context within a spying context using the Context.CreateSpy method and use it for rendering your documents:
var spyContext = Context.CreateSpy(regularContext);
var output = document.Render(spyContext);
var someVariable = spyContext.SpyVariable("one"); // Access information about variable "one"
var someVariableField = someVariable.SpyField("field"); // Access map value field information
var allVariables = spyContext.SpyVariables(); // Access information about all template variables
Read about interface ISpyContext for more information about how to use spying context methods.
Warning
Spying context has a negative impact on both performance and memory usage. You may want to apply some sampling strategy if you need to enable this feature in production.