In our previous examples, we manually selected a single user from an array with ["get", ..., "/users/0"]
. This is fine for demonstration, but real-world tasks require processing every item in a collection. You might need to transform a list of products, summarize a series of log entries, or, in our case, process a list of users.
This is where Computo's array operators come into play. We'll start with the most fundamental one: map
.
map
OperatorThe map
operator iterates over an input array and applies a transformation to each item, producing a new array of the transformed items. The original array is not changed.
Its syntax introduces a new concept: the lambda
(or anonymous function).
["map", <array_expression>, ["lambda", ["<item_variable>"], <transform_expression>]]
Let's break that down:
* <array_expression>
: An expression that must evaluate to a JSON array.
* ["lambda", ... ]
: A special expression that defines an operation to be performed on each item.
* <item_variable>
: The name you choose for the variable that will hold the current item during each iteration.
* <transform_expression>
: The expression that transforms the item. Inside this expression, you can access the current item using ["$", "/<item_variable>"]
.
map
: Extracting UsernamesLet's use the same users_input.json
from the last chapter. Our goal is to produce a simple JSON array containing only the names of all the users.
users_input.json
(for reference):
{
"users": [
{ "name": "Alice", "active": true, "plan": "premium" },
{ "name": "Bob", "active": false, "plan": "basic" },
{ "name": "Charlie", "active": true, "plan": "basic" }
]
}
Create a script named get_names.json
.
json
["map",
["get", ["$input"], "/users"],
["lambda", ["user"], ["get", ["$", "/user"], "/name"]]
]
Here is what this script tells the engine:
a. "Map over the array found at the /users
path in the input."
b. "For each item, create a temporary variable named user
."
c. "As the transformation, get
the /name
property from the user
variable."
Run the script:
bash
computo get_names.json users_input.json
The output is a new array containing just the transformed items:
[
"Alice",
"Bob",
"Charlie"
]
{"array": [...]}
Before we continue, we need to address a crucial piece of syntax. In Computo, a JSON array [...]
is always treated as an operator call. So how do you represent a literal array of data? You use a special object wrapper:
{"array": [item1, item2, ...]}
When the interpreter sees {"array": ...}
, it knows you mean "this is a literal array of data," not an action to perform.
For example, to map over a hardcoded array, you would write:
["map",
{"array": [10, 20, 30]},
["lambda", ["n"], ["+", ["$", "/n"], 5]]
]
This would produce [15, 25, 35]
. This syntax prevents any ambiguity between operator calls and literal array data.
map
The real power of map
comes from transforming each item into a new object structure. You can use obj
or permuto.apply
right inside the lambda
.
Let's transform our user list into a new list of simpler objects, each containing just a name
and plan
.
transform_users.json
:
["map",
["get", ["$input"], "/users"],
["lambda", ["u"],
["obj",
["name", ["get", ["$", "/u"], "/name"]],
["plan", ["get", ["$", "/u"], "/plan"]]
]
]
]
Running this gives us a completely new array of objects:
[
{
"name": "Alice",
"plan": "premium"
},
{
"name": "Bob",
"plan": "basic"
},
{
"name": "Charlie",
"plan": "basic"
}
]
map
and if
You can use any Computo operator inside a lambda
, including if
. This allows for powerful, item-specific conditional logic.
Let's generate a status message for each user. Active users get a welcome message; inactive users get a notice.
user_statuses.json
:
["map",
["get", ["$input"], "/users"],
["lambda", ["user"],
["if",
["get", ["$", "/user"], "/active"],
["permuto.apply",
{"message": "User ${/name} is active."},
["$", "/user"]
],
["permuto.apply",
{"message": "User ${/name} is INACTIVE."},
["$", "/user"]
]
]
]
]
Run this with the --interpolation
flag:
computo --interpolation user_statuses.json users_input.json
The output is an array of conditionally generated objects:
[
{
"message": "User Alice is active."
},
{
"message": "User Bob is INACTIVE."
},
{
"message": "User Charlie is active."
}
]
As your transformations become more complex, you'll often find yourself writing the same lambda expressions multiple times. Computo provides a powerful feature called lambda variable resolution that allows you to store lambda functions in variables using let
bindings and reuse them throughout your script.
You can store a lambda function in a variable and then reference it using the $
operator:
["let", [["function_name", ["lambda", ["param"], <transformation>]]],
["map", <array>, ["$", "/function_name"]]
]
Let's start with a basic example that doubles numbers. Instead of writing the same lambda twice, we'll store it once and reuse it:
reusable_double.json
:
["let", [["double", ["lambda", ["x"], ["*", ["$", "/x"], 2]]]],
["obj",
["list1", ["map", {"array": [1, 2, 3]}, ["$", "/double"]]],
["list2", ["map", {"array": [4, 5, 6]}, ["$", "/double"]]]
]
]
Output:
{
"list1": [2, 4, 6],
"list2": [8, 10, 12]
}
The double
function is defined once and used twice, making the code more maintainable and readable.
You can define multiple lambda functions in the same let
binding and use them together:
multiple_functions.json
:
["let", [
["increment", ["lambda", ["x"], ["+", ["$", "/x"], 1]]],
["is_large", ["lambda", ["x"], [">", ["$", "/x"], 3]]]
],
["map",
["filter", {"array": [1, 2, 3, 4, 5]}, ["$", "/is_large"]],
["$", "/increment"]
]
]
Output:
[5, 6]
This example:
1. Defines two functions: increment
(adds 1) and is_large
(checks if > 3)
2. First filters the array to keep only large numbers: [4, 5]
3. Then increments each remaining number: [5, 6]
Let's apply this to our user data with more complex, reusable transformations:
user_pipeline_with_functions.json
:
["let", [
["is_active", ["lambda", ["user"], ["get", ["$", "/user"], "/active"]]],
["is_premium", ["lambda", ["user"], ["==", ["get", ["$", "/user"], "/plan"], "premium"]]],
["extract_name", ["lambda", ["user"], ["get", ["$", "/user"], "/name"]]],
["format_user_summary", ["lambda", ["user"],
["obj",
["name", ["get", ["$", "/user"], "/name"]],
["status", ["if",
["get", ["$", "/user"], "/active"],
"ACTIVE",
"INACTIVE"
]],
["plan_type", ["get", ["$", "/user"], "/plan"]]
]
]]
],
["obj",
["active_users", ["filter", ["get", ["$input"], "/users"], ["$", "/is_active"]]],
["premium_users", ["filter", ["get", ["$input"], "/users"], ["$", "/is_premium"]]],
["active_names", ["map",
["filter", ["get", ["$input"], "/users"], ["$", "/is_active"]],
["$", "/extract_name"]
]],
["user_summaries", ["map",
["get", ["$input"], "/users"],
["$", "/format_user_summary"]
]]
]
]
Using our familiar users_input.json
, this produces:
Output:
{
"active_users": [
{ "name": "Alice", "active": true, "plan": "premium" },
{ "name": "Charlie", "active": true, "plan": "basic" }
],
"premium_users": [
{ "name": "Alice", "active": true, "plan": "premium" }
],
"active_names": ["Alice", "Charlie"],
"user_summaries": [
{ "name": "Alice", "status": "ACTIVE", "plan_type": "premium" },
{ "name": "Bob", "status": "INACTIVE", "plan_type": "basic" },
{ "name": "Charlie", "status": "ACTIVE", "plan_type": "basic" }
]
}
map
, filter
, etc.)let
scoping rules["$", "/parameter_name"]
syntaxThis feature transforms Computo from a simple transformation tool into a powerful functional programming environment where you can build libraries of reusable transformation functions.
str_concat
When working with array transformations, you'll often need to build strings from multiple values. The str_concat
operator allows you to concatenate (join together) multiple strings into a single string.
["str_concat", <string1>, <string2>, <string3>, ...]
The str_concat
operator accepts any number of arguments and converts them to strings before joining them. This makes it perfect for building formatted messages, labels, or identifiers within your array transformations.
Simple example:
["str_concat", "Hello, ", "World", "!"]
Result: "Hello, World!"
Let's apply this to our user data to create formatted user descriptions:
user_descriptions.json
:
["map",
["get", ["$input"], "/users"],
["lambda", ["user"],
["str_concat",
["get", ["$", "/user"], "/name"],
" (",
["get", ["$", "/user"], "/plan"],
" plan) - ",
["if",
["get", ["$", "/user"], "/active"],
"ACTIVE",
"INACTIVE"
]
]
]
]
Using our familiar users_input.json
, this produces:
Output:
[
"Alice (premium plan) - ACTIVE",
"Bob (basic plan) - INACTIVE",
"Charlie (basic plan) - ACTIVE"
]
str_concat
The str_concat
operator automatically converts non-string values to strings, making it useful for combining different data types:
format_with_numbers.json
:
["let", [
["format_item", ["lambda", ["item"],
["str_concat",
"Item #",
["get", ["$", "/item"], "/id"],
": ",
["get", ["$", "/item"], "/name"],
" ($",
["get", ["$", "/item"], "/price"],
")"
]
]]
],
["map",
{
"array": [
{"id": 1, "name": "Widget", "price": 25.99},
{"id": 2, "name": "Gadget", "price": 15.50}
]
},
["$", "/format_item"]
]
]
Output:
[
"Item #1: Widget ($25.99)",
"Item #2: Gadget ($15.50)"
]
String concatenation is particularly useful for building URLs or paths:
build_user_urls.json
:
["map",
["get", ["$input"], "/users"],
["lambda", ["user"],
["obj",
["name", ["get", ["$", "/user"], "/name"]],
["profile_url", ["str_concat",
"/users/",
["get", ["$", "/user"], "/name"],
"/profile"
]],
["api_endpoint", ["str_concat",
"https://api.example.com/v1/users/",
["get", ["$", "/user"], "/name"],
"?plan=",
["get", ["$", "/user"], "/plan"]
]]
]
]
]
Output:
[
{
"name": "Alice",
"profile_url": "/users/Alice/profile",
"api_endpoint": "https://api.example.com/v1/users/Alice?plan=premium"
},
{
"name": "Bob",
"profile_url": "/users/Bob/profile",
"api_endpoint": "https://api.example.com/v1/users/Bob?plan=basic"
},
{
"name": "Charlie",
"profile_url": "/users/Charlie/profile",
"api_endpoint": "https://api.example.com/v1/users/Charlie?plan=basic"
}
]
String concatenation works well with lambda variable resolution for creating reusable formatting functions:
reusable_formatters.json
:
["let", [
["full_name_formatter", ["lambda", ["person"],
["str_concat",
["get", ["$", "/person"], "/first_name"],
" ",
["get", ["$", "/person"], "/last_name"]
]
]],
["email_formatter", ["lambda", ["person"],
["str_concat",
["get", ["$", "/person"], "/first_name"],
".",
["get", ["$", "/person"], "/last_name"],
"@company.com"
]
]]
],
["map",
{
"array": [
{"first_name": "John", "last_name": "Doe"},
{"first_name": "Jane", "last_name": "Smith"}
]
},
["lambda", ["person"],
["obj",
["full_name", ["$", "/full_name_formatter"]], // This applies the lambda stored in the variable
["email", ["$", "/email_formatter"]] // This applies the lambda stored in the variable
]
]
]
]
Note: In this example, the lambda variables contain lambda functions that will be applied to the current person
item in the map operation.
You've added array processing to your skillset. You have learned:
* How to iterate over an array and transform each item using the map
operator.
* The syntax for lambda
expressions to define the per-item transformation.
* The special {"array": [...]}
syntax for representing literal arrays.
* How to combine map
with obj
, if
, and permuto.apply
for complex list transformations.
* Lambda variable resolution for storing and reusing lambda functions in let
bindings.
* How to build complex, maintainable data processing pipelines with named, reusable functions.
* The str_concat
operator for combining multiple strings and building formatted text within transformations.
map
is the first of several array operators. In the next chapters, we will explore others like filter
and reduce
to further refine our data pipelines.