Closures are small inline functions. They are generally used in two ways:
By binding them with
LETas a parameter for reuse,Passed directly into methods like
.map(),.filter(), or.chain().
Parameters passed into closures are contained within ||, after which the body of the function is written. They are not a replacement for DEFINE FUNCTION when you need a named, reusable routine with permissions.
Basic examples
Basic function definitions
-- Define an anonymous function that doubles a number
LET $double = |$n: number| $n * 2;
RETURN $double(2); -- Returns 4
-- Define a function that concatenates two strings
LET $concat = |$a: string, $b: string| $a + $b;
RETURN $concat("Hello, ", "World!"); -- Returns "Hello, World!"Error handling and type enforcement
Like regular functions, you can also enforce type constraints within your functions to prevent type mismatches.
-- Define a function that greets a person, returning a string
LET $greet = |$name: string| -> string { "Hello, " + $name + "!" };
RETURN $greet("Alice"); -- Returns "Hello, Alice!"
-- Define a function with a return type
LET $to_upper = |$text: string| -> string { string::uppercase($text)
};
RETURN $to_upper("hello"); -- Returns "HELLO"
RETURN $to_upper(123); -- Error: type mismatch
-- Define a function that accepts only numbers
LET $square = |$num: number| $num * $num;
RETURN $square(4); -- Returns 16
RETURN $square("4"); -- Error: type mismatchClosures in functions
Many of SurrealDB's functions require a closure to be passed in, making it easy to use complex logic on a value or the elements of an array.
The chain function which performs an operation on a value before passing it on:
"Two"
.replace("Two", "2")
.chain(|$num| <number>$num * 1000);2000We can see that the input to the .chain() method is indeed a closure by creating our own that is assigned to a parameter. This closure can be passed into .chain(), returning the same output as above.
LET $my_func = |$num| <number>$num * 1000;
"Two"
.replace("Two", "2")
.chain($my_func);The following example shows a chain of array functions used to remove useless data, followed by a check to see if all items in the array match a certain condition, and then a cast into another type. The array::filter call in the middle ensures that the string::len function that follows is being called on string values.
[NONE, NONE, "good data", "Also good", "important", NULL]
.filter(|$v| $v.is_string())
.all(|$s| $s.len() > 5)
.chain(|$v| <string>$v);'true'To fail the query when the pipeline does not meet your assumptions (for example while debugging), end the chain with .expect() instead of casting to a string:
[NONE, NONE, "good data", "Also good", "important", NULL]
.filter(|$v| $v.is_string())
.all(|$s| $s.len() > 5)
.expect(|$ok| $ok,
"Expected at least one string longer than 5 characters");Capturing parameters
Another aspect of closures is that they are able to capture parameters in their environment. This is in fact where the name "closure" comes from, as they are anonymous functions that are able to "enclose" such parameters without needing to pass them through the || parallel bars that hold its input arguments.
LET $okay_nums = [1,2,3];
[1,5,6,7,0].filter(|$n| $n IN $okay_nums);Closure limitations
Closures work inside a read-only context, and cannot be used to modify database resources.
-- 1. Create a test table and function
DEFINE TABLE test_table SCHEMAFULL;
DEFINE FIELD name ON test_table TYPE string;
DEFINE FUNCTION fn::test_create($name: string) -> object {
CREATE test_table CONTENT { name: $name };
{ created: true, name: $name };
};
-- 2. Call the function directly - works
fn::test_create("direct_call");
-- 3. Call the function inside .map() - fails
LET $names = ["Alice", "Bob", "Charlie"];
$names.map(|$n| fn::test_create($n));
-- Error: "Couldn't write to a read only transaction"In many cases, a closure can be substituted by another operation such as a FOR loop or a regular SELECT statement.
DEFINE TABLE test_table SCHEMAFULL;
DEFINE FIELD name ON test_table TYPE string;
DEFINE FUNCTION fn::test_create($name: string) -> object {
CREATE test_table CONTENT { name: $name };
{ created: true, name: $name };
};
-- Function to create a record called for each name
SELECT VALUE fn::test_create($this) FROM ["Alice", "Bob", "Charlie"];Conclusion
These anonymous functions provide a flexible way to define small, reusable pieces of logic that can be used throughout your queries. By leveraging them, you can write more modular and maintainable SurrealQL code.