A functional language interpreter with text processing, easy embeddable!
| Package | npm |
|---|---|
funcity-cli |
|
funcity |
(Japanese language is here/ζ₯ζ¬θͺγ―γγ‘γ)
This is a lightweight functional language processor implemented in TypeScript, featuring syntax extensions for text processing. It includes a CLI application and a library package containing only the core engine.
funcity can be considered a type of text template processor. For example, entering code like this:
Today is {{if weather.sunny}}nice{{else}}bad{{end}}weather.
Evaluates the value of the weather variable manually bound to the core engine beforehand and generates different text outputs:
Today is bad weather.
The if ... else ... end statements in the text indicate that the script is being executed.
So, you might ask, what makes this a "Functional language"?
Or how is it different from existing text processors?
Let me show you another equivalent example:
Today is {{cond weather.sunny 'nice' 'bad'}} weather.
This is an example of function application,
inserting the result of applying three arguments to the cond function.
The first argument is a conditional expression.
The following code may further interest you:
{{
set printWeather (fun w (cond w.sunny 'nice' 'bad'))
}}
Today is {{printWeather weather}} weather.
fundefines an anonymous lambda function.setperforms a mutable binding in the current scope.
Of course, the beloved Fibonacci sequence can also be computed by defining a recursive function:
{{
set fib (fun n \
(cond (le n 1) \
n \
(add (fib (sub n 1)) (fib (sub n 2)))))
}}
Fibonacci (10) = {{fib 10}}
If you want to try it right now, use Playground page!
Furthermore, you can easily integrate this interpreter into your application:
// Input script
const script = "Today is {{cond weather.sunny βniceβ 'bad'}} weather.";
// Run the interpreter
const variables = buildCandidateVariables();
const logs: FunCityLogEntry[] = [];
const text = await runScriptOnceToText(script, {
variables,
logs,
sourceId: 'hello.fc',
});
// Display the result text
console.log(text);In other words, funcity is a processing system that brings the power of functional programming to text template processors, enabling seamless integration into applications!
- A lightweight functional language processor for handling untyped lambda calculus. Adopted the simplest possible syntax. Additionally, selected the syntax extensions that should be prioritized for text processing.
- All function objects are treated as asynchronous functions. You do not need to be aware that they are asynchronous functions when applying them.
- There is also a CLI using the core engine. The CLI has both REPL mode and text processing mode.
- The core engine includes a tokenizer, parser, and reducer (interpreter).
- The core engine library is highly independent, requiring no dependencies on other libraries or packages. It can be easily integrated into your application.
- Parsers and interpreters support both interpreting pure expressions and interpreting full text-processing syntax. This means that even when an interpreter for a purely functional language is required, it is possible to completely ignore the (somewhat incongruous) syntax of text processing.
- Allows pre-binding of useful standard function implementations.
npm install -D funcity-cliOr, global installation:
npm install -g funcity-clifuncity CLI provides both REPL mode and script execution mode.
REPL mode lets you enter funcity code interactively and execute it. Script execution mode reads a funcity script source and prints the result to stdout.
# Start in REPL mode
$ funcity
$ funcity repl
# Script execution mode (read from stdin)
$ funcity -i -
# Script execution mode (read from a file)
$ funcity -i script.fc
# Predefine variables with -D (C preprocessor style)
$ funcity run -D env=prod -D debug -i script.fc
# Load predefined variables from JSON files
$ funcity run -d vars.base.json -d vars.local.json -i script.fc
# Script execution mode (explicit, read from stdin)
$ funcity run- If you omit
repl/run, it defaults toreplwhen no options are provided. - If
--inputor-iis specified, it is treated asrun. --define/-Dcan be specified multiple times.-D namemeansname=true;-D name=valuesetsvalueas a string.--define-json/-dcan be specified multiple times. Each JSON root must be an object, otherwise an error is reported.- If the same key appears in both
-dand-D,-Dtakes precedence. - On startup, the CLI loads
~/.funcityrconce and executes it before running the REPL or script. Use--no-rcto skip loading this file.
Start with funcity or funcity repl. The prompt is funcity> .
The REPL runs code expressions only and ignores text blocks. This means it behaves like a pure funcity functional language interpreter. Note that template blocks inside string literals are still supported.
Example using the add function and set for variable binding:
$ funcity
funcity> add 1 2
it: 3
funcity> set x 10
it: (undefined)
funcity> add x 5
it: 15Type exit or press Ctrl+D to exit.
The REPL automatically binds the last result to it.
It also binds its to an array of non-undefined results from the last evaluation.
Use this feature to perform consecutive calculations like a calculator:
funcity> add 1 2
it: 3
funcity> mul it 10
it: 30The REPL has a special variable, prompt. Its initial value is defined as 'funcity> ', which is output as the REPL prompt.
As astute users may have noticed, you can change the prompt using set:
funcity> set prompt 'number42> '
it: (undefined)
number42>
Script execution mode reads scripts from a file or standard input and processes them as complete scripts, including text blocks.
For example, store the funcity script in a script.fc file:
{{
set fib (fun n \
(cond (le n 1) \
n \
(add (fib (sub n 1)) (fib (sub n 2)))))
}}
Fibonacci (10) = {{fib 10}}
Execute it as follows:
$ funcity run -i script.fcAlternatively, specifying -i - reads from standard input:
$ echo βHello {{add 1 2}}β | funcity run -i -
Hello 3funcity uses a script syntax that extends the functional language with procedural constructs for text formatting. This section focuses on how to write scripts that interleave text and expressions.
Here is the shortest possible example:
The city is {{'Lisbon'}}.
Running it with funcity produces:
$ echo "The city is {{'Lisbon'}}." | funcity run
The city is Lisbon.Inside the double braces {{...}}, you can write a statement or expression.
Curly braces can be specified for any length as long as they contain two or more characters.
They must be closed with the same number of curly braces as opened (e.g., {{{...}}} / {{{{...}}}}).
Besides strings, you can also insert numbers:
$ echo "This format appeared in {{1965}}." | funcity run
This format appeared in 1965.Because numbers are allowed, you can also compute them:
$ echo "This format appeared in {{add 1960 5}}." | funcity run
This format appeared in 1965.add is one of the standard funcity functions. See the standard functions section for more details.
You can also call JavaScript's Math object:
$ echo "The diagonal of a 1cm square is {{Math.sqrt 2}}cm." | funcity run
The diagonal of a 1cm square is 1.4142135623730951cm.Functions are not limited to numbers. You can pass strings as well:
$ echo "Because it is a {{concat 'sun' 'flower'}}, it is bright." | funcity run
Because it is a sunflower, it is bright.Some functions take multiple arguments. Since arguments are space-separated, nested expressions should use parentheses to avoid ambiguity:
$ echo "We counted about {{add (mul 4 10) 2}} birds." | funcity run
We counted about 42 birds.String literals can embed template blocks with {{...}}.
Inside a quoted string, {{...}} is parsed the same way as a normal template block, so you can nest statements like if/for/end.
$ echo "{{set name 'Alice'}}{{'Hello {{name}}!'}}" | funcity run
Hello Alice!To include literal braces inside a string, escape them as \{ and \}.
funcity also has a somewhat procedural syntax. If you know other text processors, you will quickly see how it works:
$ echo "The label is {{if true}}valid{{else}}invalid{{end}}." | funcity run
The label is valid.The if statement evaluates its argument. If the value is not false, it outputs everything up to else.
Otherwise it outputs everything between else and end.
Use elseif to add additional conditions between if and else.
Try replacing true with false and confirm the behavior.
$ echo "Branch: {{if false}}A{{elseif true}}B{{else}}C{{end}}" | funcity run
Branch: BSince that example hardcodes true, it is not very interesting.
Let's look at for next:
$ echo "We named them {{for i (range 1 5)}}[cat{{i}}]{{end}}." | funcity run
We named them [cat1][cat2][cat3][cat4][cat5].The range function returns a list of numbers from 1 to 5.
for iterates that list, assigns each value to i, and repeats the body until end.
Now nest an if inside the for:
$ echo "Odd labels: {{for i (range 1 5)}}{{if (mod i 2)}}[cat{{i}}]{{end}}{{end}}" | funcity run
Odd labels: [cat1][cat3][cat5]You can nest for and if as needed.
funcity lets you define variables with arbitrary names:
Pi is long, so define a variable named pi.
{{
set pi 3.14159265
}}
Now pi is reusable. A circle with radius 10cm has circumference {{mul 2 pi 10}}cm.
When writing multi-line code, save it to a file (for example, sample.fc) and pass it to funcity:
$ funcity run -i sample.fc
Pi is long, so define a variable named pi.
Now pi is reusable. A circle with radius 10cm has circumference 62.831853cm.Because a newline is emitted at the point where the braces close, you may see blank lines like the one above. If that is a problem for your output, pay attention to where you break lines.
Memo: variables can be overwritten later with set. In programming terms, they are "mutable", which is not always favored in functional languages.
With these pieces, you can implement the usual fizz-buzz:
{{for i (range 1 15)}}{{if (eq (mod i 15) 0)}}FizzBuzz{{elseif (eq (mod i 3) 0)}}Fizz{{elseif (eq (mod i 5) 0)}}Buzz{{else}}{{i}}{{end}}
{{end}}
The line is long only to keep the output layout correct. Executing it yields:
$ funcity run -i sample.fc
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzzwhile is also supported:
Let's eat!
{{
set abort true
while abort
set eat (readline 'Do you want BBQ? [y/n]:')
set abort (not (or (eq eat 'y') (eq eat 'Y')))
end
}}
All full.
readline outputs the argument string as a prompt and waits for input.
The resulting input string is returned and assigned to the eat variable.
abort stores an abort flag, and based on the input obtained via readline, it implements the behavior of βexiting after eating the BBQ.β
Here, we're comparing input using simple βyβ or βYβ, but using the regular expression function match allows for more flexible checks.
String literals in funcity can be wrapped in single quotes ', double quotes ", or backticks `.
You can also wrap strings with three or more of the same quote character (for example '''...''' or """...""", and likewise with backticks).
The opening and closing quote must match. The empty string can be written as '', "", or an empty backtick literal (two quotes are reserved for this).
Other quote characters can be used inside a string without escaping.
To use the same quote as the opener (or \) inside a string, escape it with a backslash.
Supported escape sequences:
\nnewline\ttab\rcarriage return\vvertical tab\fform feed\0NUL\'single quote\"double quote- ````` backtick
\{left brace\}right brace\\backslash
Undefined escape sequences are treated as errors.
Examples:
$ echo "Newline: {{'A\\nB'}}" | funcity run
Newline: A
B$ echo "Quote: {{'I\\'m \\\\ ok'}}" | funcity run
Quote: I'm \ okIn funcity, lists (arrays) are defined with [...]. Elements are separated by spaces.
Passing a list as the second argument to for lets you iterate each element:
$ echo "Iriomote cat IDs: {{for i [1 2 3 4 5]}}[cat{{i}}]{{end}}" | funcity run
Iriomote cat IDs: [cat1][cat2][cat3][cat4][cat5]You can also use map and filter to transform a list and return a new one.
map applies a function to each element, while filter keeps only elements that match a condition:
$ echo "x10: {{map (fun [x] (mul x 10)) [1 2 3 4]}}" | funcity run
x10: [10 20 30 40]$ echo "Odd only: {{filter (fun [x] (mod x 2)) [1 2 3 4 5]}}" | funcity run
Odd only: [1 3 5]Other higher-order functions include flatMap and reduce.
Combined with range, or first, last, at, sort, collect, and reverse,
you can implement common set-like operations easily.
You can separate multiple expressions in three ways:
- Split blocks:
{{...}}{{...}} - Newline inside a block:
{{...\n...}} - Semicolon inside a block:
{{...;...}}
Examples:
{{set a 1}}{{set b 2}}
Sum: {{add a b}}
{{
set a 1
set b 2
}}
Sum: {{add a b}}
{{set a 1; set b 2}}
Sum: {{add a b}}
Note: ; is treated as an expression separator, but it is not allowed inside list expressions ([...]).
Conversely, to break a line within an expression, place \ at the end of the line:
{{
set abort (not \
(or (eq eat 'y') \
(eq eat 'Y')))
}}
Comments are recognized as starting with // and continuing to the end of the line:
{{
// Save the display string
set label (concat name 'is cool')
}}
So far you can already handle most text formatting tasks. Here we focus on funcity as a functional language.
You can define anonymous functions using lambda syntax:
$ echo "Define a function that doubles a number: {{fun x (add x x)}}" | funcity run
Define a function that doubles a number: fun<#1>This only creates the function object. To apply it, call it:
$ echo "Define and apply a doubling function: {{(fun x (add x x)) 21}}" | funcity run
Define and apply a doubling function: 42Inline expressions like this can feel unusual. Instead, bind the function to a variable:
{{
set mul2 (fun x (add x x))
}}
Define and apply a doubling function: {{mul2 21}}
You can define functions with multiple arguments:
{{
set ellipse (fun [a b] (mul Math.PI (mul a b)))
}}
Ellipse area: {{ellipse 2 3}}
$ funcity run -i sample.fc
Ellipse area: 18.84955592153876Define an argument list with [...]. The example above uses JavaScript's Math.PI to turn the ellipse area formula pi * a * b into a function.
Finally, recursion. You can call a bound function from within itself.
The Fibonacci example below binds fib, and fib refers to itself:
{{
set fib (fun n \
(cond (le n 1) \
n \
(add (fib (sub n 1)) (fib (sub n 2)))))
}}
Fibonacci (10) = {{fib 10}}
Because funcity variables are mutable, you can read them freely from inside functions. If a variable is undefined, a runtime error occurs.
Currently, funcity does not perform tail-call optimization, so deep recursion can overflow.
npm install funcityThe core engine of funcity takes a script string as source code, executes it, and returns a string result. This flow follows a typical language-processing workflow like the following:
flowchart LR
Script["Script text"] --> Tokenizer["Tokenizer"]
Tokenizer --> Parser["Parser"]
Parser --> Reducer["Reducer"]
Reducer --> Text["Text output"]
- The tokenizer analyzes the script text and splits it into the words used by funcity.
- The parser analyzes context from the tokens produced by the tokenizer and builds meaningful node structures.
- The interpreter (Reducer) interprets and computes each node. This chain of operations recursively executes the entire script code.
Writing the whole operation in code gives a minimal example like this:
const run = async (
script: string,
sourceId: string,
logs: FunCityLogEntry[] = []
): Promise<string> => {
// Run the tokenizer
const blocks: FunCityToken[] = runTokenizer(script, logs, sourceId);
// Run the parser
const nodes: FunCityBlockNode[] = runParser(blocks, logs);
// Run the reducer
const variables: FunCityVariables = buildCandidateVariables();
const warningLogs: FunCityWarningLogEntry[] = [];
const results: unknown[] = await runReducer(nodes, variables, warningLogs);
logs.push(...warningLogs);
// Concatenate all results as text
const text: string = results.join('');
return text;
};βThe coreβ of the core engine is truly concentrated in this code:
- The reducer's output is raw computational results. Multiple results may also be obtained. Therefore, these are concatenated as strings to produce the final output text.
- If a script does not change once loaded and you want to run only the reducer many times, you can run the tokenizer and parser up front, then execute only the reducer for efficient processing.
- Tokenizer and parser errors and warnings are added to
logs. If you want to terminate early due to errors or warnings, you can check whether there are entries inlogsafter each processing step completes. - Processing can continue to the interpreter even if errors or warnings exist. However, locations where errors occurred may have been replaced with appropriate token nodes. Using this information to run the interpreter will likely cause it to behave incorrectly.
- Interpreter errors are notified via exceptions. Only warnings are logged to the
warningLogsargument. - Depending on the script's content, reducer processing may not finish (e.g., due to infinite loops).
Passing an
AbortSignalas an argument torunReducer()allows external interruption of execution.
Note: This code is exposed as a similar function named runScriptOnce() and runScriptOnceToText().
That it actually converts results to text using convertToString().
The previous section demonstrated how to execute funcity scripts directly. However, you can also parse and execute only the functional language syntax within funcity. This allows you to use funcity purely as a functional language processor when text processing is not required. This corresponds to the CLI's REPL mode:
TODO: Example using runCodeTokenizer, parseExpressions, reduceNodeThe reducer can accept a predefined set of variables as an argument. If you define (bind) variables in advance, you can reference them inside the script:
// buildCandidateVariables() can add arbitrary variables, including standard functions
const variables = buildCandidateVariables(
{
foo: 'ABCDE', // The string can be referenced under the name `foo`
},
);
// ex: `{{foo}}` ---> ['ABCDE']
const results = await runReducer(nodes, variables, logs);Variables can bind not only literal values like strings and numbers, but also arbitrary function objects:
// buildCandidateVariables() can add arbitrary variables, including standard functions
const variables = buildCandidateVariables(
{
bar: async (n: unknown) => Number(n) * 2, // Async function
}
);
// ex: `{{bar 21}}` ---> [42]
const results = await runReducer(nodes, variables, logs);- When specifying function objects, you can pass async functions as shown above. The reducer handles asynchronous continuations internally, so any processing including I/O can be implemented.
- While you can explicitly specify the type of a function's arguments, the interpreter does not check this type.
Therefore, if you write code assuming the specified type, a runtime error may occur when a value of a different type is passed by the script.
As mentioned above, we recommend always receiving it as
unknownand checking within the function.
Using this variable definition feature allows application functionality to be referenced within scripts, enabling users to extend the application and publish a plugin system.
A "funcity function" is not a normal JavaScript function object; it can receive nodes obtained from the parser directly as arguments.
You can define one using makeFunCityFunction().
In the example below, the second argument is reduced (evaluated) and returned only when the value passed to the first argument is truthy. With ordinary functions, all arguments are reduced before they are passed, so a function like this can only be defined as a funcity function:
const variables = buildCandidateVariables(
{
// funcity function
baz: makeFunCityFunction(
async function ( // function keyword required
this: FunCityFunctionContext, // Access reducer features
c: FunCityExpressionNode | undefined, // First-argument node
n: FunCityExpressionNode | undefined) { // Second-argument node
// `c` or `n` is missing
if (!c || !n) {
// Record an error
this.appendError({
type: 'error',
description: 'Required arguments',
range: this.thisNode.range, // Current function-application node
});
// Return undefined due to the error
return undefined;
}
// Only when reduced `c` is truthy
const cr = await this.reduce(c);
if (isConditionalTrue(cr)) {
// Reduce `n` and return it
return await this.reduce(n);
}
// When `c` is falsy
return -1;
}),
}
);
// ex: `{{baz true 5}}` ---> [5]
// ex: `{{baz false 5}}` ---> [-1] (The expression `5` is not reduced)
const results = await runReducer(nodes, variables, logs);FunCityFunctionContextis an interface for using some interpreter features inside funcity functions. It is passed as the JavaScriptthisreference, so you need to use afunctionstatement and definethisas the first argument. Note that you can obtain and operate on this context not only in funcity functions but also in normal functions.this.reduce()evaluates (reduces) the specified argument node. For example, if the node references a variable, the value of that variable is returned. If the node indicates function application, that computation is executed and the result is returned.- As with normal functions, arguments might not be passed (you don't know at runtime whether the required number of arguments was provided),
so you should assume they can be
undefined. - In this example we return
undefinedwhen an error is recorded, but this is not required; you can return any value. If you return a meaningful value, evaluation continues using that value (processing usually continues even if logs are recorded).
Several functions in the standard library are also implemented using funcity functions.
While cond, and, and or are funcity functions, fun and set are also implemented as funcity functions.
Standard functions are implemented using only features that are publicly available from standardVariables or included in the functions returned by buildCandidateVariables(),
and that are standardly usable and independent of external libraries.
For example, there is a length standard function that returns the length of a string or an array (Iterable object):
{{length 'ABC'}}
The following are the standard functions:
| Function/Object | Description |
|---|---|
typeof |
Returns the type name. |
cond |
If the condition in the first argument is true, returns the second argument; otherwise the third (funcity function) |
defaults |
Returns the first argument unless it is null/undefined; otherwise returns the second (funcity function). |
toString |
Converts the arguments to a string. |
toBoolean |
Converts the first argument to a boolean. |
toNumber |
Converts the first argument to a number. |
toBigInt |
Converts the first argument to a bigint. |
add |
Adds all arguments as numbers. |
sub |
Subtracts all arguments as numbers. |
mul |
Multiplies all arguments as numbers. |
div |
Divides the first argument (as a number) by the second argument. |
mod |
Returns the remainder of dividing the first argument (as a number) by the second argument. |
eq |
Performs strict equality (===). |
ne |
Performs strict inequality (!==). |
lt |
Returns true if the first argument is less than the second. |
gt |
Returns true if the first argument is greater than the second. |
le |
Returns true if the first argument is less than or equal to the second. |
ge |
Returns true if the first argument is greater than or equal to the second. |
now |
Returns current date time in Date object. |
random |
Returns a random integer using Math.random() (requires arguments). |
randomf |
Returns a random floating-point number using Math.random(). |
concat |
Concatenates strings and Iterable arguments in order. |
join |
Uses the first argument as a separator and joins strings from the second argument onward. |
trim |
Trims whitespace at both ends of the first argument. |
toUpper |
Uppercases the first argument. |
toLower |
Lowercases the first argument. |
length |
Returns the length of the string/array/Iterable in the first argument. |
and |
ANDs arguments as booleans (funcity function) |
or |
ORs arguments as booleans (funcity function) |
not |
Returns the negation of the first argument as a boolean. |
at |
Uses the first argument as an index to fetch an element from the array/Iterable in the second argument. |
first |
Returns the first element of the array/Iterable in the first argument. |
last |
Returns the last element of the array/Iterable in the first argument. |
range |
Creates a sequential array of the second argument length, starting from the first argument value. |
slice |
Slices an array/Iterable, or a string. |
sort |
Converts an Iterable to an array and sorts with the default order. |
reverse |
Reverses an Iterable into an array. |
map |
Applies the function in the first argument to each element and returns an array. |
flatMap |
Expands and concatenates results of the function in the first argument. |
flatten |
Expands one level of nested Iterable values. |
filter |
Returns only the elements where the result of the function in the first argument is true. |
collect |
Builds an array excluding results that are null/undefined from the function in the first argument. |
distinct |
Returns unique elements from an array/Iterable. |
distinctBy |
Returns unique elements using a key selector function. |
union |
Returns the union of two arrays/Iterables. |
unionBy |
Returns the union using a key selector function. |
intersection |
Returns the intersection of two arrays/Iterables. |
intersectionBy |
Returns the intersection using a key selector function. |
difference |
Returns the difference of two arrays/Iterables (a \\ b). |
differenceBy |
Returns the difference using a key selector function. |
symmetricDifference |
Returns the symmetric difference of two arrays/Iterables. |
symmetricDifferenceBy |
Returns the symmetric difference using a key selector function. |
isSubsetOf |
Returns true if all elements of the first argument are contained in the second. |
isSubsetOfBy |
Returns true if all keys of the first argument are contained in the second. |
isSupersetOf |
Returns true if all elements of the second argument are contained in the first. |
isSupersetOfBy |
Returns true if all keys of the second argument are contained in the first. |
isDisjointFrom |
Returns true if two arrays/Iterables have no common elements. |
isDisjointFromBy |
Returns true if two arrays/Iterables have no common keys. |
reduce |
Folds using the initial value in the first argument and the function in the second argument. |
match |
For the second argument, returns an array of matches using the regex in the first argument. |
replace |
For the third argument, replaces matches of the regex in the first argument with the second argument. |
regex |
Creates a regex object from the pattern in the first argument and the options in the second argument. |
bind |
Partially applies the arguments after the first to the function in the first argument. |
url |
Creates a URL object from the first argument and optional base in the second argument. |
fetch |
Performs a fetch using the fetch API. |
fetchText |
Fetches and returns response.text(). |
fetchJson |
Fetches and returns response.json(). |
fetchBlob |
Fetches and returns response.blob(). |
include |
Includes and evaluates an external script. Provided via createIncludeFunction(). |
tryInclude |
Same as include, but can ignore missing sources via options. Provided via createIncludeFunction(). |
delay |
Resolves after the specified milliseconds. |
console |
Console output object. |
Additionally, while not strictly functions, the following elements are also included in the standard functions. These are the minimum definitions required for functionality in funcity, and the reducer can operate even without access to them, though practical code would be nearly impossible to write:
| Element | Description |
|---|---|
true |
JavaScript true value |
false |
JavaScript false value |
undefined |
JavaScript undefined value |
null |
JavaScript null value |
fun |
Anonymous function defined function (funcity function) |
set |
Variable binding function (funcity function) |
typeof returns the type name of the first argument's instance.
This function is similar to JavaScript's typeof, but also performs the following additional checks:
| Type | Type Name |
|---|---|
| Null | null |
| Array | array |
| Iterable | iterable |
The cond function returns either the value of the second argument or the value of the third argument, depending on the truth value of the first argument:
{{cond true βOKβ 'NG'}}
In regular functions, all arguments are evaluated. However, this function is special ("funcity function"): only one of the second and third arguments is evaluated, depending on the result of the first argument.
defaults returns the first argument unless it is null/undefined; otherwise it returns the second argument:
{{defaults user.nickname? 'Guest'}}
This function is also a "funcity function", so the second argument is evaluated only when needed.
Values like 0, false, and '' are kept as-is.
These functions convert the first argument to a string, boolean, number, or bigint.
{{toString 123 'ABC'}}
{{toBoolean 0}}
{{toNumber '123'}}
{{toBigInt '9007199254740993'}}
toStringconverts each argument group into a string and concatenates them with commas. Stringification follows its own pretty-printing rules.toBooleanfollows funcity's conditional semantics:null/undefinedare false,0/0nare false, and all other values are true.toNumberuses JavaScript'sNumber(...), andtoBigIntusesBigInt(...).- When
toBigIntreceives a non-primitive type, it performs the same string conversion astoStringbefore converting toBigInt.
These functions can take multiple arguments:
{{add 2 5 4 3 9}}
For and and or, at least one argument is required.
They evaluate left-to-right and stop once the result is determined (first false for and, first true for or. They are "funcity function").
random generates a random integer using JavaScript's Math.random():
{{random 5}}
{{random 5 7}}
- With one argument, it returns an integer in
0 .. <n. - With two arguments, it returns an integer in
base .. <base+span.
randomf is the floating-point version with the same ranges:
{{randomf ()}}
{{randomf 5}}
{{randomf 5 7}}
Extract elements from an array/Iterable:
{{at 1 [12 34 56]}}
{{first [12 34 56]}}
{{last [12 34 56]}}
The index for at is zero-based.
Creates a sequential array with a start value and count:
{{range 3 5}}
The result is an array like [3 4 5 6 7].
Slices an array/Iterable or string:
{{slice 1 3 [10 11 12 13]}}
{{slice -2 [10 11 12 13]}}
{{slice 1 3 'ABCDE'}}
The results are [11 12], [12 13], and 'BC'.
If the last argument is a string, slice behaves like String.prototype.slice and returns a string.
Otherwise it treats the last argument as an Iterable, converts it to an array, and returns a sliced array.
The end argument can be omitted.
Pass a function that takes one argument as the first argument. It can be a lambda or a bound variable.
By passing an array-like Iterable as the second argument, it runs sequential processing:
{{map (fun [x] (mul x 10)) [12 34 56]}}
{{flatMap (fun [x] [(mul x 10) (add x 1)]) [1 2]}}
{{filter (fun [x] (mod x 2)) [1 2 3 4]}}
Use flatten to unwrap one level of nested Iterable values without passing a function:
{{flatten [[1 2] [3] [4 5]]}}
Filters out null/undefined and returns an array:
{{collect [1 undefined 3 null 4]}}
The result is an array like [1 3 4].
Returns unique elements while preserving the first occurrence order:
{{distinct [1 2 2 3]}}
{{distinctBy (fun [x] (mod x 2)) [2 4 1 3]}}
distinctBy uses the key selector in the first argument to determine uniqueness.
Set operations between two arrays/Iterables:
{{union [1 2 2 3] [3 4 1]}}
{{intersection [1 2 3] [2 3 4]}}
{{difference [1 2 3] [2]}}
{{symmetricDifference [1 2 3] [3 4]}}
Each operation returns a unique array.
The *By variants accept a key selector function as the first argument.
Relationship checks between two arrays/Iterables:
{{isSubsetOf [1 2] [1 2 3]}}
{{isSupersetOf [1 2 3] [2 3]}}
{{isDisjointFrom [1 2] [3 4]}}
The *By variants compare keys produced by the selector function.
Specify the initial value as the first argument.
Specify a fold function that takes two arguments as the second argument.
Pass an array-like Iterable as the third argument to run the fold:
{{reduce 'A' (fun [acc v] (concat acc v)) ['B' 'C' 'D']}}
The result is 'ABCD'.
Specify the regex pattern string as the first argument.
Specify regex options (e.g. 'g' or 'i') as the second argument.
{{set r (regex '[A-Z]' 'gi')}}
The regex in the first argument can be a string or a regex object.
Specify the string to test as the second argument:
{{match (regex '[A-Z]' 'gi') 'Hello World'}}
The result is an array like ['H' 'e' 'l' 'l' 'o' 'W' 'o' 'r' 'l' 'd'].
The regex in the first argument can be a string or a regex object:
{{replace 'dog' 'ferret' 'dog is cute'}}
The result is 'ferret is cute'.
Returns a function with partially applied arguments:
{{(bind add 123) 100}}
The result is 223.
Be aware that this is different from variable binding, despite the similar-sounding name.
This function is nearly equivalent to bind() for function objects in JavaScript.
Creates a URL object using the first argument and an optional base URL in the second argument:
{{set u (url '/path' 'https://example.com/base/')}}
The result is a URL object that represents https://example.com/base/path.
Resolves after the specified milliseconds (optional second argument is returned):
{{delay 200}}
include evaluates external scripts and inserts the result.
tryInclude behaves similarly but ignores the script if it cannot be found.
In the CLI, it is defined as follows:
- REPL: The base path is relative to the current directory.
- Script execution: The base path is relative to the script's directory or the current directory.
- Variable scope is always considered the same (no child scope is created).
{{include 'foo.fc'}}
{{tryInclude 'optional.fc'}}
Both functions throw when a parse error is detected in the included script.
To use these functions programmatically, create them with createIncludeFunction() and inject them into a variable:
const logs: FunCityLogEntry[] = [];
const { include, tryInclude } = createIncludeFunction({
resolve: async (request) => {
if (request === 'foo.fc') {
return "{{set a 1}}";
}
return undefined;
},
logs,
mode: 'template',
scope: 'same',
});
const variables = buildCandidateVariables({ include, tryInclude });resolve must return one of the following:
- A
stringscript. ThesourceIdwill be the request string. - An
{ sourceId, script }object. undefinedto indicate a missing source (handled byincludeMissing/tryIncludeMissing).
mode controls parsing:
template: parse full templates (usesrunTokenizer+runParser).code: parse code-only scripts (usesrunCodeTokenizer+parseExpressions).
scope controls evaluation:
same: evaluate in the caller scope (sosetaffects the caller).child: evaluate in a child scope (no variable leakage).
objectVariables exposes JavaScript built-in objects for binding:
| Object | Description |
|---|---|
Object |
Object object. |
Function |
Function object. |
Array |
Array object. |
String |
String object. |
Number |
Number object. |
BigInt |
BigInt object. |
Boolean |
Boolean object. |
Symbol |
Symbol object. |
Math |
Math object. |
ArrayBuffer |
ArrayBuffer object. |
Date |
Date object. |
Intl |
Intl object. |
JSON |
JSON object. |
Map |
Map object. |
Set |
Set object. |
Promise |
Promise object. |
RegExp |
RegExp object. |
WeakMap |
WeakMap object. |
WeakSet |
WeakSet object. |
DaReflectte |
Reflect object. |
Error |
Error object. |
import { buildCandidateVariables, objectVariables } from 'funcity';
const variables = buildCandidateVariables(objectVariables);For example:
{{Math.sqrt 2}}
{{Date '2025/2/23'}}
Note: As an important restriction of funcity, if an object has a constructor, you cannot call the object as a function object.
For example, the following expressions distinguished in JavaScript: new Date(β2025/2/23β), Date(β2025/2/23β),
are always interpreted as new Date(β2025/2/23β) when written as Date β2025/2/23β in funcity syntax.
CLI includes objectVariables by default.
fetchVariables exposes the JavaScript fetch API for binding:
| Function | Description |
|---|---|
fetch |
Accesses a web server using the fetch API. |
fetchText |
Returns the result of response.text(). |
fetchJson |
Returns the result of response.json(). |
fetchBlob |
Returns the result of response.blob(). |
import { buildCandidateVariables, fetchVariables } from βfuncityβ;
// Enable the fetch API
const variables = buildCandidateVariables(fetchVariables);
// ...fetch returns a response object using the global fetch. fetchText, fetchJson and fetchBlob are convenience wrappers:
{{fetchText 'data:text/plain,hello'}}
{{fetchJson 'data:application/json,%7B%22ok%22%3Atrue%7D'}}
CLI includes fetchVariables by default.
nodeJsVariables exposes a readline function for reading a single line from
stdin (optional prompt). Import it from the Node-only entry to avoid pulling
Node built-ins into projects that do not use them:
import { buildCandidateVariables } from 'funcity';
import { nodeJsVariables } from 'funcity/node';
const variables = buildCandidateVariables(nodeJsVariables);
// ...For example:
{{set persons (toNumber (readline 'How many people? '))}}
Additionally, createRequireFunction generates a Node.js require function that resolves modules relative to a specified directory.
Making this function available allows scripts to dynamically load NPM modules.
When acceptModules argument is specified, module access is restricted to that allowlist.
Modules must still be either Node.js default modules or located within the node_modules/ directory of the specified directory:
import { buildCandidateVariables } from 'funcity';
import { createRequireFunction } from 'funcity/node';
const _require = createRequireFunction(
'/path/to/script/dir', // If not specified, use `process.cwd()`
['fs', 'lodash'] // `acceptModules`
);
const variables = {
require: _require,
};
// ...For example:
{{
set fs (require 'fs/promises')
fs.readFile '/foo/bar/text' 'utf-8'
}}
CLI includes both readline and require by default.
If you understand this far, you should also understand what the following code achieves in the browser. Therefore, please exercise caution when exposing functionality like the following. At the same time, you should see how funcity protects the host environment from scripts. Simply put, you just need to prevent dangerous definitions from being accessible.
const candidateVariables = buildCandidateVariables(
{
// Endless expansion and perilous adventures
window,
document,
}
);funcity was separated from the document site generator mark-the-ripper during its design phase, as it seemed better suited to function as an independent scripting engine.
Therefore, mark-the-ripper can leverage the power of funcity's functional language.
Under MIT.

