| Crates.io | blots |
| lib.rs | blots |
| version | 0.13.1 |
| created_at | 2025-09-12 20:02:37.121391+00 |
| updated_at | 2026-01-17 05:04:26.257325+00 |
| description | A small, simple, expression-oriented programming language. |
| homepage | https://github.com/paul-russo/blots-lang |
| repository | https://github.com/paul-russo/blots-lang |
| max_upload_size | |
| id | 1836551 |
| size | 134,210 |
Blots is a small, dynamic, functional, expression-oriented programming language designed to be quick to learn, easy to use, and to produce code that's readable yet reasonably compact. Blots is intended for quick calculations and data transformation.
brew install paul-russo/tap/blots
cargoIf you don't have Rust installed, you can use rustup to install the latest stable version of Rust, including the
cargotool.
cargo install blots
_ separators, and 0b/0x prefixes for binary/hex literals (e.g., 1_000_000, 3.14e-2, 0b1010, 0xFF)'hello', "world"); concatenate with +true, false; operators: and/&&, or/||, not/![1, 2, 3]; access with list[index] (0-based, or negative to count from end: list[-1] is the last element); spread with [...list1, ...list2]{a: 1, "hello there": "hi"}; key shorthand {foo}; access with record.a or record["key"]x => x+1, (x,y?) => x + (y ?? 0), (f, ...rest) => map(rest,f)null+ - * / % ^ !== != < <= > >= (with broadcasting).== .!= .< .<= .> .>= (use these to compare entire lists as whole values)&& || ! or and or not?? (null-coalesce), ... (spread)if cond then expr else exprThere are no mutable variables in Blots. Instead, values are bound to a name. Once created, a binding cannot be mutated; it's the same for the life of the program. This property makes Blots code more "pure": it is difficult to construct an expression in Blots that can return a different result for the same inputs. This also means that functions can be losslessly serialized and output from programs.
Arithmetic and comparison operations automatically "broadcast" over lists, meaning they apply to each element:
[1, 2, 3] * 10 // [10, 20, 30]
[10, 20, 30] + 2 // [12, 22, 32]
[4, 5, 6] > 3 // [true, true, true]
[1, 2] == [2, 2] // [false, true]
Sometimes you want to compare whole values without broadcasting. The dot-prefixed comparison operators (.==, .!=, .<, .<=, .>, .>=) disable broadcasting and perform direct value comparisons:
// Regular == with broadcasting
[10, 5, 10] == 10 // [true, false, true] (equality is evaluated for each element)
// Dot operator without broadcasting
[10, 5, 10] .== 10 // false (list isn't the same type as `10`)
[10, 5, 10] .== [10, 5, 10] // true (lists are identical)
For ordering operators (.<, .<=, .>, .>=), lists are compared lexicographically (like dictionary ordering):
// Element-by-element comparison
[1, 2, 3] .< [1, 2, 4] // true (first two elements equal, 3 < 4)
[1, 2, 3] .< [1, 3, 0] // true (first element equal, 2 < 3)
[2, 0, 0] .> [1, 9, 9] // true (first element decides: 2 > 1)
// Length comparison when elements are equal
[1, 2] .< [1, 2, 3] // true (all common elements equal, shorter is less)
[] .< [1] // true (empty list is less than any non-empty list)
[1, 2, 3] .== [1, 2] // false (different lengths)
// Nested lists work recursively
[[1, 2], [3]] .< [[1, 2], [3, 4]] // true ([3] < [3, 4])
[[2]] .> [[1, 9]] // true ([2] > [1, 9] because 2 > 1)
For .== and .!=, lists must be exactly equal in both structure and values:
// Deep equality check
[1, 2, 3] .== [1, 2, 3] // true (same values, same order)
[[1, 2], [3, 4]] .== [[1, 2], [3, 4]] // true (nested equality)
// Any difference makes them unequal
[1, 2, 3] .!= [1, 2, 4] // true (different values)
[1, 2, 3] .!= [1, 2] // true (different lengths)
[1, 2, 3] .!= 123 // true (different types)
When comparing different types:
==, !=, .==, .!=) work across all types, returning false or true respectively when types differ<, <=, >, >= and their dot variants) error when types cannot be compared"hello" .== [1, 2, 3] // false (string != list)
"abc" .< "def" // true (strings compare lexicographically)
5 .< [1, 2, 3] // ERROR: cannot compare number with list
null > 0 // ERROR: cannot compare null with number
via and intoThe via operator takes a value and sends it through a function, applying the function to each element if the value is a list. For example:
'hello' via uppercase // 'HELLO' (because uppercase('hello') = 'HELLO')
['hello', 'world'] via uppercase // ['HELLO', 'WORLD'] (because [uppercase('hello') = 'HELLO', uppercase('world') = 'WORLD'])
into works exactly the same as via, except there is no broadcasting. This means that you can "reduce" a list into a single value (though you could also produce another list). Example:
'hello' into head // 'h' (because head('hello') = 'h')
['hello', 'world'] via head // ['h', 'w'] (because [head('hello') = 'h', head('world') = 'w'])
['hello', 'world'] into head // 'hello' (because head(['hello', 'world']) = 'hello')
whereThe where operator filters a list based on a predicate function. It's analogous to filter in the same way that via is analogous to map. The predicate function must return a boolean value, and only elements for which the predicate returns true are included in the result. For example:
[1, 2, 3, 4, 5] where x => x > 3 // [4, 5]
[1, 2, 3, 4, 5] where x => x % 2 == 0 // [2, 4]
["apple", "banana", "cherry"] where s => s == "banana" // ["banana"]
The predicate function can accept either one argument (the element) or two arguments (the element and its index):
[10, 20, 30, 40] where (val, idx) => idx > 0 // [20, 30, 40] (filters out the first element)
[10, 20, 30, 40] where (val, idx) => idx % 2 == 0 // [10, 30] (keeps elements at even indices)
Important notes about where:
where with a scalar will result in an error)where operator only works with lists, unlike via which also works with scalarsvia, into, and whereThese operators can be naturally chained together:
// Chaining works intuitively at the top level
[1,2,3] via x => x * 2 where y => y > 2 // [4, 6]
// Chain as many operations as you want
[1,2,3,4,5,6] via x => x * 2 where y => y > 5 via z => z + 1 // [7, 9, 11, 13]
// Or assign intermediate results for clarity
doubled = [1,2,3] via x => x * 2
doubled where x => x > 2 // [4, 6]
Note: If you need to use via, into, or where inside a lambda body (not at the top level), you must wrap the expression in parentheses:
// ❌ This won't parse (via/into/where not allowed in lambda bodies without parens)
[[1,2,3], [4,5,6]] via list => list via x => x * 2
// ✅ Use parentheses to enable via/into/where inside lambda bodies
[[1,2,3], [4,5,6]] via list => (list via x => x * 2) // [[2,4,6], [8,10,12]]
do BlocksBlots is an expression-oriented language, in the sense that every statement in a Blots program should evaluate to a useful value. This works well with a functional approach, where you compose functions to compute values. However, sometimes it's more intuitive to represent a computation as a series of discrete steps that happen one after another, instead of composing functions. For these cases, you can use do blocks to create an expression whose final value is the result of imperative code with intermediate variables:
result = do {
y = x * 2
z = -y
return z
}
Some things to note about do blocks:
do block is an expression and needs to evaluate to a single value, it must end with a return statement.do blocks are separated by newlines, just like other statements in Blots. Alternatively, if you want to keep things more compact, you can use semicolons (;) to separate statements on the same line.The Blots CLI accepts JSON values as inputs, either as piped input or via the --input (-i) flag:
blots -i '{ "name": "Paul" }'
All input values are merged together and made available via the inputs record:
output greeting = "Hey " + inputs.name // "Hey Paul"
For convenience, you can use the # character as shorthand for inputs.:
// These are equivalent:
output greeting = "Hey " + #name
output greeting = "Hey " + inputs.name
// Useful with the coalesce operator for default values:
principal = #principal ?? 1000
years = #years ?? 10
The #field syntax works everywhere inputs.field works and returns null for missing fields (making it compatible with the ?? operator).
JSON arrays and primitive values (numbers, strings, booleans, and null) can be passed directly as inputs as well:
blots -i '[1,2,3]'
These unnamed inputs are named like value_{1-based index}:
output total = sum(...inputs.value_1) // 6
Multiple inputs:
# Combine multiple JSON inputs
blots -i '{"x": 10}' -i '{"y": 20}' "output total = inputs.x + inputs.y"
# Output: {"total": 30}
Piped input:
# Pipe JSON data into Blots
echo '{"items": [1,2,3,4,5]}' | blots -e "output average = avg(...inputs.items)"
# Output: {"average": 3}
# Process command output
curl -s "https://api.example.com/data.json" | blots -e "output count = len(inputs.results)"
# Output: { "count": 20 }
Use the output keyword to include bound values in the outputs record. This record will be sent to stdout as a JSON object when your Blots program successfully executes (or when you close an interactive Blots session). The output keyword can be used in two ways:
// For new bindings
output one = 1
// For existing bindings
answer = 42
output answer
The above example would yield this output:
{ "one": 1, "answer": 42 }
Multiple outputs:
// Calculate statistics from input data
data = inputs.values
output mean = avg(...data)
output min_val = min(...data)
output max_val = max(...data)
output std_dev = sqrt(avg(...map(data, x => (x - mean)^2)))
Structured outputs:
// Return nested data structures
output result = {
summary: {
total: sum(...inputs.items),
count: len(inputs.items)
},
processed: map(inputs.items, x => x * 2)
}
Function output:
Functions are serialized as JSON objects with a special __blots_function key containing the function source code. Simple functions are output as-is:
output double = x => x * 2
{ "double": { "__blots_function": "(x) => x * 2" } }
Closure values are inlined into the function body, making functions self-contained and losslessly serializable:
multiplier = 10
output scale = x => x * multiplier
{ "scale": { "__blots_function": "(x) => x * 10" } }
Functions can be passed as inputs to other Blots programs, enabling function composition across programs.
Using outputs with other tools:
# Format output with jq
blots -i '[1,2,3,4,5]' "output stats = {minimum: min(...inputs.value_1), maximum: max(...inputs.value_1)}" | jq
# Save output to file
blots "output data = range(1, 11) via (x => x^2)" -o squares.json
# Or:
blots "output data = range(1, 11) via (x => x^2)" > squares.json
# Chain Blots programs
blots "output nums = range(1, 6)" | blots "output squares = inputs.nums via x => x^2"
Comments start with //, and run until the end of the line:
// This is a comment
x = 42 // This is also a comment
sqrt(x) - returns the square root of xsin(x) - returns the sine of x (in radians)cos(x) - returns the cosine of x (in radians)tan(x) - returns the tangent of x (in radians)asin(x) - returns the arcsine of x (in radians)acos(x) - returns the arccosine of x (in radians)atan(x) - returns the arctangent of x (in radians)log(x) - returns the natural logarithm of xlog10(x) - returns the base-10 logarithm of xexp(x) - returns e raised to the power of xabs(x) - returns the absolute value of xfloor(x) - returns the largest integer less than or equal to x (e.g. 2.7 becomes 2 and -2.7 becomes -3)ceil(x) - returns the smallest integer greater than or equal to x (e.g. 2.1 becomes 3 and -4.5 becomes -4)round(x) - returns x rounded to the nearest integer (e.g. 2.7 becomes 3)trunc(x) - returns the integer part of x (removes fractional part) (e.g. both 2.7 and 2.1 become 2, and both -2.7 and -2.1 become -2)random(seed) - returns a pseudo-random number in the range [0, 1) based on the given seed. The same seed always produces the same result, making it deterministic and reproducible. Use different seeds to generate different random numbers (e.g., random(42) always returns the same value, while [1, 2, 3, 4, 5] via random generates five different random numbers)min(list) or min(...args) - returns the minimum given value from a listmax(list) or max(...args) - returns the maximum value from a listavg(list) or avg(...args) - returns the average (mean) of values in a listsum(list) or sum(...args) - returns the sum of all values in a listprod(list) or prod(...args) - returns the product of all values in a listmedian(list) or median(...args) - returns the median value from a listpercentile(list, p) - returns the percentile value at position p (0-100) from a listrange(n) - returns [0, 1, ..., n-1]range(start, end) - returns [start, start+1, ..., end-1]len(list) - returns the length of a listhead(list) - returns the first element of a listtail(list) - returns all but the first element of a listslice(list, start, end) - returns a sublist from start (inclusive) to end (exclusive) indicesconcat(list1, list2, ...) - concatenates multiple listsdot(list1, list2) - returns the dot product of two listsunique(list) - returns unique elements from a listsort(list) - returns a sorted copy of the list (ascending)sort_by(list, fn) - sorts a list using a comparison functionreverse(list) - returns a reversed copy of the listany(list) - returns true if any element in the list is trueall(list) - returns true if all elements in the list are trueflatten(list) - flattens nested lists by one level (e.g., [[1, 2], [3, 4]] becomes [1, 2, 3, 4])zip(list1, list2, ...) - combines multiple lists into a list of tuples; pads shorter lists with null (e.g., zip([1, 2], ["a", "b"]) returns [[1, "a"], [2, "b"]])chunk(list, n) - splits a list into sublists of size n; the last chunk may be smaller (e.g., chunk([1, 2, 3, 4, 5], 2) returns [[1, 2], [3, 4], [5]])group_by(list, fn) - groups elements by the result of fn (must return a string); returns a record of lists (e.g., group_by(["apple", "banana"], x => slice(x, 0, 1)) returns {"a": ["apple"], "b": ["banana"]})count_by(list, fn) - counts elements by the result of fn (must return a string); returns a record of counts (e.g., count_by(["a", "b", "a"], x => x) returns {"a": 2, "b": 1})map(list, fn) - applies a function to each element of a listreduce(list, fn, initial) - reduces a list to a single value using a functionfilter(list, fn) - returns elements where the function returns trueevery(list, fn) - returns true if all elements satisfy the predicatesome(list, fn) - returns true if any element satisfies the predicatesplit(string, delimiter) - splits a string into a listjoin(list, delimiter) - joins a list into a stringreplace(string, search, replacement) - replaces occurrences in a stringtrim(string) - removes leading and trailing whitespaceuppercase(string) - converts string to uppercaselowercase(string) - converts string to lowercaseincludes(string, substring) - checks if string contains substringformat(string, ...values) - formats a string with placeholder values (e.g. `format("answer: {}", 42))typeof(value) - returns the type of a value ("number", "string", "boolean", "null", "list", "record", "built-in function", or "function")arity(fn) - returns the minimum number of parameters a function expectsto_string(value) - converts a value to its string representationto_number(value) - converts a string or boolean to a number. If value is a string, parses it as a floating-point number. If value is a boolean, returns 1 for true and 0 for false.to_bool(number) - converts a number to a boolean. If the number is 0, then returns false. Otherwise, returns true.keys(record) - returns a list of all keys in a recordvalues(record) - returns a list of all values in a recordentries(record) - returns a list of [key, value] pairs from a recordugt(a, b) - unchecked greater than: returns true if a > b, false otherwise (including when types cannot be compared)ult(a, b) - unchecked less than: returns true if a < b, false otherwiseugte(a, b) - unchecked greater than or equal: returns true if a >= b, false otherwiseulte(a, b) - unchecked less than or equal: returns true if a <= b, false otherwiseconvert(value, from_unit, to_unit) - converts a numeric value from one unit to anotherThe convert function supports 200+ units across 19 categories:
Units can be specified by full name or abbreviation and comparisons default to case-insensitive matching. Metric units support both American ("meter") and British ("metre") spellings. When multiple units share the same lowercase alias (for example mA vs MA), convert returns an ambiguous-unit error; provide the canonical casing shown below to disambiguate.
Examples:
convert(100, "celsius", "fahrenheit") // 212
convert(5, "km", "miles") // 3.1068559611866697
convert(1, "kg", "lbs") // 2.2046226218487757
convert(1024, "bytes", "kibibytes") // 1
convert(180, "degrees", "radians") // 3.141592653589793 (π)
convert(1, "kilowatt", "watts") // 1000
Access mathematical constants via constants.*:
constants.pi: The mathematical constant π.constants.e: The mathematical constant e.constants.max_value: The maximum value that can be represented as a 64-bit floating point number.constants.min_value: The minimum non-zero value that can be represented as a 64-bit floating point number.There's a language support extension for Blots, available on both the VSCode Marketplace and the Open VSX Registry. You should be able to install it from within your editor like other extensions, but you can also download the VSIX file directly from either directory.
For Vim users, there's also a Vim plugin providing syntax highlighting for .blots files.