30 releases
Uses new Rust 2024
| new 0.9.0 | Nov 9, 2025 |
|---|---|
| 0.8.11 | Nov 7, 2025 |
| 0.8.9 | Oct 30, 2025 |
| 0.8.0 | Sep 28, 2025 |
| 0.4.13 | Sep 16, 2025 |
#92 in Text processing
921 downloads per month
440KB
10K
SLoC
Blots
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.
Installation
Installing a Prebuilt Binary
Homebrew
brew install paul-russo/tap/blots
Building from Source, Using cargo
If you don't have Rust installed, you can use rustup to install the latest stable version of Rust, including the
cargotool.
cargo install blots
The Blots Language
Core Types
- Number: 64-bit float with decimal/sci-notation support and
_separators (e.g.,1_000_000,3.14e-2) - String: Single or double quotes (
'hello',"world"); concatenate with+ - Boolean:
true,false; operators:and/&&,or/||,not/! - List: Ordered collection
[1, 2, 3]; access withlist[index](0-based); spread with[...list1, ...list2] - Record: JSON compatible. Key-value pairs
{a: 1, "hello there": "hi"}; key shorthand{foo}; access withrecord.aorrecord["key"] - Function:
x => x+1,(x,y?) => x + (y ?? 0),(f, ...rest) => map(rest,f) - Null:
null
Operators & Control Flow
- Arithmetic:
+ - * / % ^ ! - Comparison:
== != < <= > >=(with broadcasting) - Non-broadcasting comparison:
.== .!= .< .<= .> .>=(use these to compare entire lists as whole values) - Logic:
&& || !orand or not - Special:
??(null-coalesce),...(spread) - Conditional:
if cond then expr else expr
Bindings
There 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.
Broadcasting
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]
Dot-Prefixed Comparison Operators
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)
List Comparison Algorithm
For ordering operators (.<, .<=, .>, .>=), lists are compared lexicographically (like dictionary ordering):
- Element-by-element comparison: Lists are compared element by element from left to right
- First difference decides: The first non-equal pair of elements determines the result
- Length as tiebreaker: If all compared elements are equal, the shorter list is considered less than the longer list
// 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)
Equality Comparisons
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)
Mixed Type Comparisons
When comparing different types with dot operators:
.==and.!=always returnfalseandtruerespectively for different types- Ordering operators (
.<, etc.) returnfalsewhen types can't be ordered
"hello" .== [1, 2, 3] // false (string != list)
5 .< [1, 2, 3] // false (number and list have no natural ordering)
"abc" .< "def" // true (strings compare lexicographically)
via and into
The 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')
do Blocks
Blots 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:
- Since each
doblock is an expression and needs to evaluate to a single value, it must end with areturnstatement. - Statements in
doblocks 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.
Inputs and Outputs
Inputs
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"
Input Shorthand Syntax
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
More Input Examples
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 }
Outputs
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 }
More Output Examples
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)
}
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
Comments start with //, and run until the end of the line:
// This is a comment
x = 42 // This is also a comment
Built-in Functions
Math Functions
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.7becomes2and-2.7becomes3)ceil(x)- returns the smallest integer greater than or equal to x (e.g.2.1becomes3)round(x)- returns x rounded to the nearest integer (e.g.2.7becomes3)trunc(x)- returns the integer part of x (removes fractional part) (e.g.2.7becomes2and-2.7becomes-2)
Aggregate Functions
min(list)- returns the minimum given value from a listmax(list)- returns the maximum value from a listavg(list)- returns the average (mean) of values in a listsum(list)- returns the sum of all values in a listprod(list)- returns the product of all values in a listmedian(list)- returns the median value from a listpercentile(list, p)- returns the percentile value at position p (0-100) from a list
List Functions
range(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 istrueall(list)- returns true if all elements in the list aretrue
Higher-Order Functions
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 predicate
String Functions
split(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))
Type Functions
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 fortrueand 0 forfalse.to_bool(number)- converts a number to a boolean. If the number is 0, then returnsfalse. Otherwise, returnstrue.
Record Functions
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 record
Unit Conversion
convert(value, from_unit, to_unit)- converts a numeric value from one unit to another
The convert function supports 200+ units across 19 categories:
- Angle: degrees, radians, gradians, revolutions, arc minutes, arc seconds
- Area: square meters/kilometers/miles/feet/etc., acres, hectares
- Concentration of Mass: grams per liter, milligrams per deciliter
- Duration: seconds, minutes, hours, days, weeks, milliseconds, etc.
- Electric Charge: coulombs, ampere hours (with metric prefixes)
- Electric Current: amperes (with metric prefixes)
- Electric Potential Difference: volts (with metric prefixes)
- Electric Resistance: ohms (with metric prefixes)
- Energy: joules, calories, kilocalories, kilowatt hours
- Frequency: hertz (with metric prefixes)
- Fuel Efficiency: liters per 100km, miles per gallon
- Information Storage: bits, bytes, kilobytes, kibibytes, megabytes, mebibytes, etc.
- Length: meters, kilometers, miles, feet, inches, nautical miles, light years, etc.
- Mass: kilograms, grams, pounds, ounces, tons, stones, etc.
- Power: watts (with metric prefixes), horsepower
- Pressure: pascals, bars, atmospheres, psi, mmHg, inHg
- Speed: meters per second, kilometers per hour, miles per hour, knots
- Temperature: Celsius, Fahrenheit, Kelvin
- Volume: liters, gallons, cubic meters, cups, pints, quarts, etc.
Units can be specified by full name or abbreviation and are case-insensitive. Metric units support both American ("meter") and British ("metre") spellings.
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
Constants
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.
Tools
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.
Dependencies
~11–24MB
~299K SLoC