Raven
Raven is a pragmatic, typed application language for .NET that makes functional composition, algebraic modeling, and object-oriented design complementary parts of one toolset, with direct access to the .NET runtime and ecosystem.
It is built around a few clear ideas: expression-oriented code where values help, statement forms where effects are clearer, explicit mutability, explicit pattern bindings, and pragmatic interop with the .NET ecosystem.
It takes visible inspiration from Swift, Rust, and F#, but Raven is not trying to imitate any one of them exactly. Its current shape is defined more by consistency than by novelty:
- expression-oriented code with statement forms where control flow is clearer
- explicit lexical mutability with
letandvar - explicit pattern bindings, including deconstruction and control-flow patterns
Option<T>andResult<T, E>as ordinary control-flow shapes- records, primary constructors, unions, and structural patterns
- direct use of .NET libraries, collections, async APIs, and IL-based tooling
Raven has no
void; it usesunit, written as().
Start without ceremony
Raven introduces behavior with functions because a function is often the most direct way to name a calculation, validation rule, or workflow step. Functions can be composed, stored in values, and passed as dependencies without first creating a class or one-method service interface.
Even the program itself does not need a class wrapper. Top-level statements can
form a small application directly, and a named entry point is a plain Main
function rather than a method that must live on a Program class.
The same is true for reusable behavior: parsing, validation, formatting, and workflow functions can live directly in a namespace. They do not need to be wrapped in a static utility class just to give them somewhere to live.
This low-ceremony starting point does not make Raven a purely functional language. Object-oriented programming is a first-class part of Raven's toolset, not merely a compatibility layer for .NET. Classes, interfaces, methods, and mutable state are available where identity, encapsulation, lifecycle, or polymorphism are part of the model. Records and unions cover other useful domain shapes without forcing everything into an object hierarchy.
record struct Temperature(Value: decimal)
func IsTooHot(limit: Temperature, reading: Temperature) -> bool {
return reading.Value > limit.Value
}
func Monitor(read: () -> Temperature, report: (Temperature) -> ()) -> () {
let reading = read()
report(reading)
}
Here Monitor receives behavior through function parameters. A production
program can pass device and telemetry functions; a test can pass deterministic
ones. No service wrapper is required. When a stateful device connection really
has a lifecycle, it can still be modeled as a class and expose one of its methods
as the same function-shaped dependency.
If these choices are unfamiliar from C#, the Raven for C# developers guide compares common C# structures with their Raven counterparts.
Hello, World
import System.Console.*
WriteLine("Hello, World!")
Top-level statements are supported. Top-level type declarations are hoisted for binding, so scripts can place helper types near the file-scope code that uses them.
Function and block bodies may also declare local helper class, struct,
record, and enum types. These declarations are scoped to the containing
body and are intended for types that are only used locally.
A quick taste
import System.*
import System.Console.*
func Main() -> () {
let inputs = ["10", "3", "abc", "42"]
let message = match ProcessNumbers(inputs) {
Ok(_) => "All numbers processed"
Error(let err) => "Failed: $err"
}
WriteLine(message)
}
func ProcessNumbers(inputs: string[]) -> Result<(), string> {
for text in inputs {
let no = ParseInt(text)?
let line = match no {
10 => "Ten exactly!"
let value => "Parsed: $value"
}
WriteLine(line)
}
return Ok
}
func ParseInt(text: string) -> Result<int, string> {
return int.Parse(text) match {
Ok(let v) => Ok(v)
Error(_) => Error("\"$text\" is not a number")
}
}
That example shows the current Raven style in miniature:
matchis a normal expressionResultvalues are handled directly instead of being wrapped in exception-heavy flow- pattern arms use explicit binding keywords where they introduce new values
- selected .NET APIs are projected into Raven's
OptionandResultvocabulary; hereint.Parse(string)returns aResultwhose error channel is the union of the framework exceptions that the projection captures
Target-typed shorthand
When the surrounding context supplies a type, Raven lets a leading . stand in
for that target. Named member bindings such as .Active resolve static members,
enum values, and union cases on the target type. An omitted member name,
.(...), constructs the target type.
union UserStatus {
case Active
case Suspended(reason: string)
}
record class User(
val Id: int,
val Name: string,
val Role: Option<string>,
val Status: UserStatus)
let users: User[] = [
.(1, "Ada", Some("compiler engineer"), .Active),
.(2, "Bo", None, .Suspended("email bounced"))
]
The same rule works in assignment, return, argument, and collection-element
positions whenever the target type is known. Use it where that target is clear
from an annotation, an API parameter, or the surrounding name. Prefer the
explicit form (User(...), UserStatus.Active) when the shorthand would make a
reader hunt for the target type.
Bindings and mutability
letintroduces an immutable lexical binding.varintroduces a mutable lexical binding.valandvardescribe read-only and writable properties and signature-like declarations.
let name = "Raven"
var count = 0
count = count + 1
let a = 1, b = 2
var left = 10, right = 20
Raven also accepts val as an alternative spelling for an immutable lexical
binding. The language's standard style and generated examples use let, giving
lexical code the familiar let/var pairing while reserving the val/var
pairing for properties and signatures. Choosing val for a local does not
produce a diagnostic unless a project explicitly enables the optional
PreferLetInsteadOfValAnalyzer style policy.
let and val make the binding read-only; they do not make the referenced
object deeply immutable. A read-only binding cannot be reassigned, but a
mutable object reached through it may still change. Semantic tooling describes
a local introduced with let as val, because val is the resulting binding's
mutability rather than its lexical introduction syntax.
Expressions and matching
if and match are commonly used in expression position, but Raven is not expression-only. Loops, disposal, mutation, and early returns remain ordinary statement forms when that keeps intent clearer.
let label = match value {
0 => "zero"
_ => "non-zero"
}
let message = if isAnonymous {
"Welcome"
} else {
"Welcome back"
}
Raven also leans heavily on patterns as a control-flow surface, not only as a match feature:
if let Person(1, name, _) = person {
WriteLine(name)
}
while let Some(item) = stream.Next() {
WriteLine(item)
}
for let [first, ...rest] in rows {
WriteLine(first)
}
loop {
if work.IsComplete {
break
}
work.Step()
}
let label = match input {
let Some((x, y)) => "($x, $y)"
_ => "none"
}
The important rule is that pattern bindings stay explicit. In inline and freestanding
patterns, a capture uses a binding keyword. When Raven offers an outer shorthand
form such as let (...) = expr, if let pattern = expr,
while let pattern = expr, for let pattern in values, or
match { let pattern => ... }, that outer keyword supplies the binding mode for
otherwise bare captures inside the pattern. Those implicit captures may still
carry inline type annotations, as in let (key: string, value: int) = entry.
There is also an important surface distinction:
is,match,if let pattern = expr,while let pattern = expr, andfor ... inpattern targets use the general pattern language.- deconstruction assignment/declaration (
let (...) = expr,(...) = expr,let [...] = expr,[...] = expr) use the deconstruction subset rather than every match-only pattern form.
Result and Option
Raven treats recoverable flow as data. In domain code, Option<T> is preferred for absence and Result<T, E> for expected failures.
In the standard library, both carriers are defined as plain union types, which
means they use the default struct carrier shape aligned with the .NET union
contract.
func Divide(a: int, b: int) -> Result<int, string> {
if b == 0 {
return Error("Division by zero")
}
return Ok(a / b)
}
let value = Divide(10, 2)?
func FindFirstEven(values: int[]) -> Option<int> {
for value in values {
if value % 2 == 0 {
return Some(value)
}
}
return None
}
Propagation expressions (?)
Use ? to forward failure/absence and keep the happy path linear.
func BuildLabel(values: int[]) -> Result<string, string> {
let firstEven = FindFirstEven(values)?
let quotient = Divide(100, firstEven)?
return Ok("Result: $quotient")
}
Railroad-style flow with carrier methods
Raven also supports pipeline-friendly methods on Result and Option so transformations and fallbacks stay in one straight line:
import System.Linq.*
import System.Collections.Generic.*
let plans = List<RatePlan> {
RatePlan("NorthStar", 500, 120)
RatePlan("Oceanic", 450, 150)
}
let requests = List<ShipmentRequest> {
ShipmentRequest("REQ-1001", "NorthStar", 10, Some("SAVE5"))
ShipmentRequest("REQ-1002", "Oceanic", 3, None)
}
let summary = match BuildQuoteSummary(requests, plans) {
Ok(let message) => message
Error(let err) => "Quote failed: $err"
}
func BuildQuoteSummary(requests: IEnumerable<ShipmentRequest>, plans: IEnumerable<RatePlan>) -> Result<string, string> {
let request = requests
.FirstOrError(r => r.Id == "REQ-1002", () => "Request not found")?
let total = plans
.FirstOrError(p => p.Carrier == request.Carrier, () => "Rate plan not found")
.Map(plan => plan.BaseCents + (request.WeightKg * plan.PerKgCents))?
let promoDiscount = PromoCents(request.PromoCode).UnwrapOr(0)
return Ok("Quote ${request.Id}: ${total - promoDiscount} cents")
}
func PromoCents(code: Option<string>) -> Option<int> {
let raw = code?
let normalized = raw.Trim().ToUpperInvariant()
match normalized {
"SAVE5" => Some(500)
_ => None
}
}
record class ShipmentRequest(val Id: string, val Carrier: string, val WeightKg: int, val PromoCode: Option<string>)
record class RatePlan(val Carrier: string, val BaseCents: int, val PerKgCents: int)
In Raven, null still exists for .NET interop, but it is not the preferred domain-modeling tool. The intended direction is:
- use
Option<T>for absence in Raven code - use
Result<T, E>for expected failure - use nullable types primarily where .NET APIs already speak in those terms
Functions
func Add(a: int, b: int) -> int => a + b
let addA = func (x: int) => x + 42
let addB = func (x: int) {
x + 42
}
let result = addA(1)
Raven treats declarations and expressions as one function concept:
- Named declarations can appear as top-level functions in namespaces, as methods on types, or as local functions inside blocks.
- Function expressions produce function values:
func (...) => ...,func (...) { ... }, or shorthand(...) => ....
In practice, the difference is binding shape:
- Named declarations introduce top-level, member, or local function symbols.
- Function expressions produce a function value that you bind to
let/varor pass as an argument.
Function type signatures use arrow notation:
let f: (int, int) -> int
let g = func (a: int, b: int) => a + b
Function expressions may omit func as shorthand. This is mainly a convenience for higher-order call sites such as LINQ-style APIs:
let projected = [1, 2, 3].Where(x => x > 1).Select(x => x * 2)
Function expressions also support async, static, and static async modifier forms:
let add = static func (a: int, b: int) {
a + b
}
let delayedAdd = async func (a: int, b: int) {
await Task.Delay(2)
return a + b
}
static function expressions cannot capture outer locals or parameters.
Function signatures, function expressions, and tuple shapes intentionally reuse the same arrow-and-parentheses vocabulary so higher-order code stays readable instead of introducing a separate syntax family.
Data shapes and patterns
Raven treats structural data inspection as a core language tool.
union Token {
case Identifier(text: string)
case Number(value: int)
case End
func IsTerminal() -> bool {
return self is End
}
}
func Describe(token: Token) -> string {
match token {
.Identifier(let text) => "id: $text"
.Number(let value) => "number: $value"
.End => "end"
}
}
The same general pattern model works across:
matchif value is patternif let pattern = exprwhile let pattern = exprfor ... inwith pattern targets- deconstruction assignment and declaration
This reuse is intentional. Raven wants one pattern system that scales across value inspection, branching, iteration, and deconstruction instead of splitting those features into unrelated syntax families.
Union bodies are ordinary member bodies, so case declarations and authored
members live together in the same union:
union Result<T, E> {
case Ok(value: T)
case Error(error: E)
func IsSuccess() -> bool {
return self is Ok
}
}
At the same time, deconstruction forms stay intentionally narrower than general pattern-matching forms. They are designed for extraction, not for every kind of conditional match. That means Raven reuses one pattern model conceptually while still distinguishing between:
- general matching surfaces such as
is,match,if let pattern = expr,while let pattern = expr, andfor let pattern in values - deconstruction surfaces such as
let (a, b) = exprand[head, ..tail] = values
When deconstruction uses a Deconstruct shape, Raven also supports named
elements so existing code can stay stable as new properties are added. Both
matching and declaration/assignment deconstruction may spell element names in
any order, for example Person(Items: let items, Name: let name, Age: 42) or
let (Items: items, Name: name, Age: age) = person. Named elements bind by the
selected Deconstruct parameter name; unknown names report the same
member-not-found diagnostic Raven uses for property patterns.
Typed captures inside named elements use the explicit nested-binding spelling
Name: let name: string; the ambiguous shorthand Name: name: string is
diagnosed.
Extensions
extension StringExt for string {
func ToSlug() -> string =>
self.Trim().ToLowerInvariant().Replace(" ", "-")
}
import MyApp.StringExt.*
let slug = "Hello World".ToSlug()
Records and primary constructors
record class ShipmentRequest(
val Id: string,
val Carrier: string,
val WeightKg: int,
)
For classes and structs, primary-constructor parameters become properties only when declared with val/var.
For records, positional parameters are promoted by default and define the record's complete value shape. Record bodies can add computed members and static storage, but not extra instance fields, storage properties, or secondary constructors.
Accessibility defaults
Raven members are public by default. Use access modifiers (private, internal, protected) when you intentionally narrow visibility.
For type-like declarations, Raven also supports fileprivate when a helper should only be visible inside the current source file. File-scoped declarations keep their source name for same-file lookup, but the compiler mangles the emitted type name.
class Counter(private var count: int = 0) {
func Increment() -> () {
count += 1
}
val Count: int => count
}
fileprivate class CounterState {
var value: int = 0
}
Extensions and traits are part of Raven’s “interop without surrender” story: the core language stays compact, while everyday ergonomics can still grow through normal library surfaces instead of requiring a special compiler feature for every convenience.
Async and await
import System.Net.Http.*
import System.Threading.Tasks.*
async func DownloadLength(url: string) -> Task<int> {
use http = HttpClient()
let text = await http.GetStringAsync(url)
return text.Length
}
.NET interop
import System.Console.*
import System.Collections.Generic.*
let numbers: List<int> = [1, 2, 3]
WriteLine(numbers.Count)
Raven is designed to work with the .NET ecosystem directly, including BCL APIs, LINQ-style pipelines, tasks, and normal CLR types.
Where to go next
- Getting started
- Language spec
- Classes and members
samples/cases/
This is a living document and may evolve with the language.