# 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](https://rustup.rs/) to install the latest stable version of Rust, including the `cargo` tool.
```
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 with `list[index]` (0-based); spread with `[...list1, ...list2]`
- **Record**: JSON compatible. Key-value pairs `{a: 1, "hello there": "hi"}`; key shorthand `{foo}`; access with `record.a` or `record["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**: `&& || !` or `and 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:
```blots
[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:
```blots
// 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):
1. **Element-by-element comparison**: Lists are compared element by element from left to right
2. **First difference decides**: The first non-equal pair of elements determines the result
3. **Length as tiebreaker**: If all compared elements are equal, the shorter list is considered less than the longer list
```blots
// 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:
```blots
// 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 return `false` and `true` respectively for different types
- Ordering operators (`.<`, etc.) return `false` when types can't be ordered
```blots
"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:
```blots
'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:
```blots
'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')
```
### `where`
The `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:
```blots
[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):
```blots
[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`:
- The left side must be a list (using `where` with a scalar will result in an error)
- The right side must be a function that returns a boolean value
- If the predicate returns a non-boolean value, the program will error
- The `where` operator only works with lists, unlike `via` which also works with scalars
### Chaining `via`, `into`, and `where`
These operators can be naturally chained together:
```blots
// 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:
```blots
// ❌ 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` 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:
```blots
result = do {
y = x * 2
z = -y
return z
}
```
Some things to note about `do` blocks:
- Since each `do` block is an expression and needs to evaluate to a single value, it must end with a `return` statement.
- Statements in `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.
### Inputs and Outputs
#### Inputs
The Blots CLI accepts JSON values as inputs, either as piped input or via the `--input` (`-i`) flag:
```bash
blots -i '{ "name": "Paul" }'
```
All input values are merged together and made available via the `inputs` record:
```blots
output greeting = "Hey " + inputs.name // "Hey Paul"
```
##### Input Shorthand Syntax
For convenience, you can use the `#` character as shorthand for `inputs.`:
```blots
// 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:
```bash
blots -i '[1,2,3]'
```
These unnamed inputs are named like `value_{1-based index}`:
```blots
output total = sum(...inputs.value_1) // 6
```
##### More Input Examples
**Multiple inputs:**
```bash
# Combine multiple JSON inputs
blots -i '{"x": 10}' -i '{"y": 20}' "output total = inputs.x + inputs.y"
# Output: {"total": 30}
```
**Piped input:**
```bash
# 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:
```blots
// For new bindings
output one = 1
// For existing bindings
answer = 42
output answer
```
The above example would yield this output:
```json
{ "one": 1, "answer": 42 }
```
##### More Output Examples
**Multiple outputs:**
```blots
// 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:**
```blots
// 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:**
```bash
# 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:
```blots
// This is a comment
x = 42 // This is also a comment
```
### Built-in Functions
#### Math Functions
- `sqrt(x)` - returns the square root of x
- `sin(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 x
- `log10(x)` - returns the base-10 logarithm of x
- `exp(x)` - returns _e_ raised to the power of x
- `abs(x)` - returns the absolute value of x
- `floor(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`)
- `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. `2.7` becomes `2` and `-2.7` becomes `-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)
#### Aggregate Functions
- `min(list)` - returns the minimum given value from a list
- `max(list)` - returns the maximum value from a list
- `avg(list)` - returns the average (mean) of values in a list
- `sum(list)` - returns the sum of all values in a list
- `prod(list)` - returns the product of all values in a list
- `median(list)` - returns the median value from a list
- `percentile(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 list
- `head(list)` - returns the first element of a list
- `tail(list)` - returns all but the first element of a list
- `slice(list, start, end)` - returns a sublist from start (inclusive) to end (exclusive) indices
- `concat(list1, list2, ...)` - concatenates multiple lists
- `dot(list1, list2)` - returns the dot product of two lists
- `unique(list)` - returns unique elements from a list
- `sort(list)` - returns a sorted copy of the list (ascending)
- `sort_by(list, fn)` - sorts a list using a comparison function
- `reverse(list)` - returns a reversed copy of the list
- `any(list)` - returns true if *any* element in the list is `true`
- `all(list)` - returns true if *all* elements in the list are `true`
#### Higher-Order Functions
- `map(list, fn)` - applies a function to each element of a list
- `reduce(list, fn, initial)` - reduces a list to a single value using a function
- `filter(list, fn)` - returns elements where the function returns true
- `every(list, fn)` - returns true if all elements satisfy the predicate
- `some(list, fn)` - returns true if any element satisfies the predicate
#### String Functions
- `split(string, delimiter)` - splits a string into a list
- `join(list, delimiter)` - joins a list into a string
- `replace(string, search, replacement)` - replaces occurrences in a string
- `trim(string)` - removes leading and trailing whitespace
- `uppercase(string)` - converts string to uppercase
- `lowercase(string)` - converts string to lowercase
- `includes(string, substring)` - checks if string contains substring
- `format(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 expects
- `to_string(value)` - converts a value to its string representation
- `to_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`.
#### Record Functions
- `keys(record)` - returns a list of all keys in a record
- `values(record)` - returns a list of all values in a record
- `entries(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 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:
```blots
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](https://github.com/paul-russo/blots-syntax) for Blots, available on both the [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=Blots.blots-syntax) and the [Open VSX Registry](https://open-vsx.org/extension/blots/blots-syntax). 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.