Table of Contents

Collection expressions

Collection expressions create arrays, lists, dictionaries, spans, and other collection types with a compact bracket syntax.

let numbers = [1, 2, 3]
let names: List<string> = ["Ada", "Lin", "Grace"]
let scores = ["Ada": 10, "Lin": 12]

Elements are evaluated from left to right. A trailing comma is allowed, and elements can be separated by commas, semicolons, or newlines:

let colors = [
    "red"
    "green"
    "blue"
]

Immutable, mutable, and array forms

When no surrounding type determines the collection kind, Raven uses the literal's opening syntax to choose a default:

Form Default type
[a, b] ImmutableList<T>
![a, b] List<T>
[|a, b|] CLR array T[N] or T[]
let immutable = [1, 2, 3]       // ImmutableList<int>
let mutable = ![1, 2, 3]        // List<int>
let array = [|1, 2, 3|]         // int[3]

The same defaults apply when elements use newline or semicolon separators. The ! marker makes mutable collection creation intentional; an explicit target type can choose a different mutable or immutable collection without it:

let queue: Queue<int> = [1, 2, 3]
let list: List<int> = [1, 2, 3]

Spreading existing collections

Prefix an element with ... to enumerate another collection and insert all of its values at that position:

let middle = [2, 3, 4]
let combined = [1, ...middle, 5]
let array = [|0, ...middle, 6|]

The spread source must implement System.Collections.IEnumerable, either directly or through a generic interface such as IEnumerable<T>. Its elements must be convertible to the destination element type. A source that is not enumerable reports RAV2022.

A bare range element expands in the same way:

let inclusive = [1..3]       // 1, 2, 3
let halfOpen = [1..<4]       // 1, 2, 3
let surrounded = [0, 1..3, 4]

Here .. includes the upper bound and ..< excludes it. The range contributes its values rather than a single System.Range object.

Collection comprehensions

A comprehension produces one collection element for each value from a source:

let squares = [for n in numbers => n * n]
let evenSquares = [for n in numbers if n % 2 == 0 => n * n]
let rangeSquares = [for n in 1..10 => n * n]

The optional if filters values before the selector runs. The source may be a range or any value accepted by for.

The iteration target can be a pattern:

let entries = [(1, "Ada"), (2, "Bob")]
let selected = [for let (2, name) in entries => name]

Pattern targets follow the same rules as pattern-targeted for statements. Elements that do not match are skipped. The filter runs only after the pattern has matched, and the outer let, val, or var supplies the binding mode for otherwise bare captures.

Comprehensions are implemented as collection-building loops and use the same target typing and element conversions as ordinary collection elements.

Dictionary expressions

Write key: value to create a dictionary entry:

let byName = ["Ada": 10, "Lin": 12]
let mutableByName = !["Ada": 10, "Lin": 12]

Without a target type, the bare form produces ImmutableDictionary<TKey, TValue> and the ! form produces Dictionary<TKey, TValue>.

Dictionary expressions support spreads and comprehensions:

let more = ["Grace": 14]
let merged = [..."Ada": 10, ...more, "Lin": 12]

let lengths = [for key in [|"a", "bb"|] => key: key.Length]
let doubled = [for let (key, value) in [("a", 1), ("b", 2)] =>
    key: value * 2]

...key: value inserts one entry in spread position. A dictionary spread must provide values compatible with IEnumerable<KeyValuePair<TKey, TValue>> after key and value conversions.

If any element has dictionary shape, the whole expression is dictionary-shaped. It cannot mix dictionary entries with positional elements, range elements, or a comprehension that produces only values.

Target typing

The surrounding context can determine both the collection type and its element type:

let numbers: int[] = [1, 2, 3]
let names: List<string> = ["Ada", "Lin"]
let lookup: IReadOnlyDictionary<string, int> = ["a": 1, "b": 2]
let span: System.Span<int> = [1, 2, 3]
let readOnly: System.ReadOnlySpan<int> = [1, 2, 3]

Array targets

For a target T[], Raven allocates a one-dimensional array and converts every element to T. A spread must enumerate values convertible to T.

For a fixed-length target T[N], Raven checks the element count whenever it can prove it statically. Ordinary elements contribute one item, and a spread from a fixed-length T[M] contributes M. A proven mismatch is a compile-time error.

Multidimensional arrays such as T[,] are not created by collection-expression syntax. Create them through a runtime API or another existing value, then use normal indexing and assignment.

Span targets use array-backed storage, so the resulting Span<T> or ReadOnlySpan<T> remains valid for its normal scope. Both ordinary and spread elements are supported.

Collection targets

A non-array target must have an accessible parameterless constructor and an instance Add method. Raven constructs the target and calls Add for each element. The method's parameter type determines element conversions.

A dictionary target uses the same builder model but requires an accessible Add(key, value) method. Keys and values are converted to its two parameter types.

Inference without a target

Without an expected type, Raven finds the best common element type. Spreads and ranges contribute their element types. Numeric conversions can produce a common type:

let values = [1, 2.0] // ImmutableList<double>

Inference does not fall back to object, System.ValueType, or an interface merely to make unrelated elements fit. Supply an explicit target when that heterogeneous shape is intentional:

let values: object[] = [1, true]

Dictionary expressions infer key and value types separately.

Collection element types also participate in generic method inference before overload resolution supplies a final collection target. For example, Task.WhenAll([Task.FromResult(1)]) infers the result type int instead of first widening the element to the non-generic Task type.

An empty collection expression has no element type to infer and therefore requires a target:

let names: string[] = []
let queue: Queue<int> = []
let span: System.Span<byte> = []

With a target, Raven creates the corresponding empty array, initialized collection, or empty span.

Element access

Use brackets after a collection value to read an element through its array or indexer behavior:

let values = [1, 42, 3]
let answer = values[1]

Index and range access are described under Index, range, and bitwise operators.