Getting started

A tour of Raven

Raven is a typed language for .NET. If you know C#, you can bring your knowledge of classes, interfaces, generics, collections, and asynchronous APIs. This tour focuses on Raven's choices for expressing values, modeling outcomes, and composing a workflow.

Each example uses familiar application code as its starting point. The language reference covers the complete syntax and rules; the beginner guide introduces programming concepts from the beginning.

Model each outcome with the data it needs

A quote can be ready or rejected. Give those states their own payloads instead of asking callers to interpret a status alongside potentially missing fields.

import System.Console.*

record Shipment(Id: int, Weight: decimal)

union Quote {
    case Ready(total: decimal)
    case Rejected(reason: string)
}

func quote(shipment: Shipment) -> Quote {
    if shipment.Weight <= 0 {
        return .Rejected("Weight must be positive")
    }
    return .Ready(12.50m + shipment.Weight * 1.75m)
}

let message = quote(Shipment(42, 3.5m)) match {
    .Ready(let total) => "Total: $total"
    .Rejected(let reason) => reason
}

WriteLine(message)

The record and arithmetic provide the input; the union expresses the decision. The caller handles both alternatives in a match expression. Exhaustiveness checking helps keep that handling complete as the model changes.

A leading . uses the target type: the return annotation identifies .Ready, and the matched value identifies its cases. let total introduces a capture. Raven keeps binding explicit so it cannot be confused with a value comparison.

Unions and their rules · Choosing records, classes, and unions

Let a decision produce a value

Use an if block as a value when the branches compute the same result. This lets a calculation stay next to the condition that selects it.

import System.Console.*

func shippingCost(weight: decimal, express: bool) -> decimal {
    let surcharge = if express {
        let handling = 4m
        handling + weight * 0.5m
    } else {
        0m
    }
    12.50m + surcharge
}

WriteLine(shippingCost(3.5m, true))

The last expression supplies each block's value, including the function's result. An explicit return is also available, as in the previous example. Loops, mutation, and early exits remain useful when the operation calls for them.

let prevents reassignment of a binding; var permits it. Neither makes a referenced object deeply immutable. This distinction matters when working with ordinary mutable .NET collections and objects.

Expressions and inference · Bindings and mutability

Keep expected failure in the function's signature

Raven uses Result<T, E> for an expected success or failure, and Optional<T> for presence or absence. The caller can handle those cases or propagate them.

import System.Console.*

func parsePort(text: string) -> Result<int, string> {
    int.Parse(text) match {
        .Ok(let port) when port > 0 && port <= 65535 => .Ok(port)
        .Ok(_) => .Failure("Port must be between 1 and 65535")
        .Failure(_) => .Failure("Port must be a number")
    }
}

func endpoint(text: string) -> Result<string, string> {
    let port = parsePort(text)?
    return .Ok("http://localhost:$port")
}

WriteLine(endpoint("8080"))
WriteLine(endpoint("invalid"))

? yields the success payload or returns the failure from endpoint. It requires a compatible enclosing return type. With Optional, the same operator propagates absence.

There is a deliberate .NET boundary choice here: Raven projects selected framework APIs, including int.Parse(string), into Result. The CLR method has not changed; Raven supplies the source-level view and handling. Likewise, int.TryParse(string) can produce an Optional<int> without an out variable. This is a defined set of projections, not a blanket conversion of every throwing method. Use try expression to capture another throwing call.

Optional, Result, and propagation · Framework interoperability

Compare a value or capture it explicitly

Patterns can test values from the current scope with a comparison operator. A binding keyword introduces a new value instead.

import System.Console.*

func describePort(port: int, preferred: int) -> string {
    port match {
        == preferred => "Preferred port"
        let other => "Alternative: $other"
    }
}

WriteLine(describePort(8080, 8080))
WriteLine(describePort(9000, 8080))

== preferred compares against the parameter using Raven's ordinary equality semantics. let other captures the value. A bare variable name is not a substitute for that comparison. This is worth noticing if you come from a language where bare names introduce pattern bindings.

For nullable values, a pattern can establish a separate non-null binding. Direct is not null narrowing is also available as an opt-in compatibility mode; Raven does not otherwise adopt a general nullable flow engine.

Patterns and bindings · Nullability

Compose with the .NET APIs you already use

Task, HttpClient, and await still serve their familiar roles. Raven's contribution in this example is the error boundary and its composition with propagation:

import System.*
import System.Net.Http.*
import System.Threading.Tasks.*

async func downloadLength(url: string) -> Task<Result<int, Exception>> {
    use http = HttpClient()
    let text = (try await http.GetStringAsync(url))?
    return .Ok(text.Length)
}

try captures the exception as a result; the following ? propagates its error. use scopes resource disposal to the function. The function advertises both its asynchronous work and its possible failure in the return type. Each ? unwraps one carrier layer, so a call that already returns a Result can require another explicit propagation step.

Ordinary .NET exception handling remains available. Choose capture when the operation should expose exceptions as values, and try/catch when you want to handle them in that form.

Async functions · Exceptions and capture

Keep objects, functions, and libraries together

Classes and interfaces remain first-class modeling tools for identity, state, lifetimes, and polymorphism. Standalone functions can live directly in a namespace, and a single-operation dependency can be expressed as a function parameter. Use the shape that describes the dependency.

The same program can use LINQ, NuGet packages, ASP.NET Core, and Raven's domain types. Familiarity does not mean every source rule is identical: for example, Raven uses () for unit rather than void, and ordinary classes are closed to inheritance unless declared open or abstract. The reference explains these differences where they affect the code you write.

Continue with a real program