Raven for absolute beginners
This guide is for people who are new to programming. You do not need to know C#, .NET, functional programming, or object-oriented programming.
A program is a set of instructions for a computer. Programming means describing those instructions precisely enough for the computer to follow and clearly enough for another person to understand.
Raven lets a small program stay small. You can begin with direct instructions, then introduce functions and types when they become useful.
Before you begin
Raven currently runs from a source checkout. Follow the build steps in Getting Started first.
Save each example as beginner.rav. From the Raven repository root, compile and
run it with:
dotnet run -f net10.0 --project src/Raven.Compiler --property WarningLevel=0 -- \
beginner.rav -o /tmp/raven-beginner.dll
dotnet /tmp/raven-beginner.dll
If the compiler reports an error, start with the first message. It normally includes the line and character where Raven became confused.
The examples build on ideas introduced earlier. When a later fragment calls
WriteLine, keep import System.Console.* at the top of your file.
Your first program
import System.Console.*
WriteLine("Hello, Raven!")
WriteLine displays text in the terminal. Text surrounded by quotation marks is
called a string.
The import line lets the program use the short name WriteLine for the .NET
operation System.Console.WriteLine.
The call is written directly in the file. This is a top-level statement. Raven does not require you to create a class before your program can do something.
Remembering values
A program can give a name to a value and use it later:
import System.Console.*
let name = "Mira"
let age = 12
WriteLine("$name is $age years old")
let gives a name to a value that will not be replaced later. The $name and
$age parts insert those values into the surrounding string.
When a value really needs to change, use var:
var score = 0
score = score + 10
Prefer let until you have a reason to use var.
Values have types
A type describes what kind of value something is and which operations make sense for it.
let name: string = "Mira"
let age: int = 12
let height: decimal = 1.52
let isLearning: bool = true
stringrepresents text.intrepresents whole numbers.decimalrepresents decimal numbers.boolrepresents eithertrueorfalse.
Raven can infer these types, so the annotations are often optional. Types help the compiler find mistakes before the program runs.
Doing calculations and comparisons
Operators combine or compare values. The arithmetic operators work much like they do in ordinary mathematics:
let sum = 8 + 4
let difference = 8 - 4
let product = 8 * 4
let quotient = 8 / 4
let remainder = 9 % 4
% gives the remainder after division, so 9 % 4 is 1. This is useful for
questions such as whether a number is even: number % 2 == 0.
Comparison operators produce a bool value:
let isEqual = 5 == 5
let isDifferent = 5 != 3
let isSmaller = 3 < 5
let isAtLeastFive = 7 >= 5
Use && when both conditions must be true, || when either condition may be
true, and ! to reverse a condition:
let hasTicket = true
let doorIsOpen = false
let canEnter = hasTicket && doorIsOpen
let shouldWait = hasTicket && !doorIsOpen
These expressions create values just like any other expression. You can store
their results in a variable or use them directly as an if condition.
Making decisions
Use if when a program should choose between two paths:
import System.Console.*
let temperature = 28
if temperature > 25 {
WriteLine("It is warm")
} else {
WriteLine("It is cool")
}
The condition after if is either true or false. Raven runs the first block
when it is true and the else block when it is false.
An if can also produce a value:
let description = if temperature > 25 {
"warm"
} else {
"cool"
}
Repeating work
A collection holds several values. This array contains three names:
import System.Console.*
let names = ["Mira", "Noah", "Ari"]
for name in names {
WriteLine("Hello, $name!")
}
The for loop runs the same instructions once for each name.
Understanding braces and scopes
The if and for examples both use braces: { begins a block of code and }
ends it. An ordinary block creates a new scope. A scope is the part of the
program where a name can be used.
import System.Console.*
let outside = "I belong to the outer scope"
if true {
let inside = "I belong to the if block"
WriteLine(outside)
WriteLine(inside)
}
WriteLine(outside)
// WriteLine(inside) // Error: inside is not in scope here.
Code inside a block can use names declared in an enclosing, outer scope. Code
outside the block cannot use locals declared inside it. At the closing }, the
name inside goes out of scope.
The same rule applies to loop bodies. A name declared inside the loop cannot be used after the loop ends:
for name in names {
let greeting = "Hello, $name"
WriteLine(greeting)
}
// name and greeting are not in scope here.
You will encounter the same idea again in function bodies and match arms.
Each block keeps its local names contained, so different parts of the program
can use simple names without interfering with one another.
Scope describes where a name is available. It is not exactly the same as how long an object remains alive. Later, you may return an object from a function or store it somewhere outside the block. That object can continue to exist even though the local name used inside the block is gone.
Naming behavior with functions
A function gives a name to an operation. It can receive input and produce a result.
import System.Console.*
func Greet(name: string) -> string {
return "Hello, $name!"
}
let message = Greet("Mira")
WriteLine(message)
Read the declaration from left to right:
funcdeclares a function.Greetis its name.name: stringis a text input namedname.-> stringsays the result is text.returnsupplies that result.
A short function can be written as one expression:
func Add(a: int, b: int) -> int => a + b
Functions do not need to be wrapped in a class. Use them directly for calculations, validation, formatting, and workflows.
Creating your own data types
Programs become easier to understand when important concepts have meaningful types.
record class Person(val Name: string, val Age: int)
let person = Person("Mira", 12)
let personName = person.Name
let personAge = person.Age
WriteLine("$personName is $personAge years old")
Person groups a name and age into one value. The val parameters become
properties that can be read as person.Name and person.Age.
Records are compared by their values. Two separately created records are equal when all their data is equal:
let anotherPerson = Person("Mira", 12)
let peopleAreEqual = person == anotherPerson
Here, peopleAreEqual is true, even though person and anotherPerson were
created separately.
Taking data apart with patterns
A pattern describes the shape of a value. You can use one to take a record apart and give names to its parts:
let (name, age) = person
WriteLine("$name is $age years old")
The parts appear in the same order as the parameters in the Person
declaration. Use _ when you do not need one of them:
let (firstName, _) = person
WriteLine(firstName)
You can also name the parts of the record explicitly. Named parts may appear in any order:
let (Age: yearsOld, Name: fullName) = person
Here, let creates the new names captured by the pattern. Patterns also appear
in if, for, and match, where they can both inspect a value and take it
apart.
Modeling alternatives
Some values can be one of several meaningful cases. A union names those cases:
union Weather {
case Sunny
case Rainy(amount: int)
case Snowy(depth: int)
}
Use match to handle every alternative:
func Describe(weather: Weather) -> string {
return weather match {
.Sunny => "It is sunny"
.Rainy(let amount) => "Rain: $amount mm"
.Snowy(let depth) => "Snow: $depth cm"
}
}
The data and the decisions use the same case names, which makes the program easier to follow.
Values that may be absent
Sometimes not finding a value is normal. Option<T> represents either a value
with Some(...) or no value with None.
A console program receives text, even when the person types a number. Use
int.TryParse to turn that text into an integer when possible:
import System.*
import System.Console.*
Write("Enter your age: ")
let input = ReadLine()
match int.TryParse(input) {
Some(let age) => WriteLine("Next year you will be ${age + 1}")
None => WriteLine("That was not a whole number")
}
ReadLine() may produce no text, and the text may not contain a valid integer.
int.TryParse(input) handles both possibilities by producing Some(age) when
parsing succeeds or None when it does not. The match makes the program say
what to do in either case.
You can use the same idea in your own functions. This one searches an array for an even number:
func FindEven(numbers: int[]) -> Option<int> {
for number in numbers {
if number % 2 == 0 {
return Some(number)
}
}
return None
}
The return type tells the caller that absence is possible. A match handles
both possibilities:
match FindEven([1, 3, 4, 7]) {
Some(let number) => WriteLine("Found $number")
None => WriteLine("No even number found")
}
When the rest of a function needs the contained value, use let ... else to
keep the successful path straight:
func PrintEven(number: Option<int>) -> unit {
let Some(value) = number else {
WriteLine("No even number found")
return
}
WriteLine("Found $value")
}
The pattern describes the value required to continue. If it does not match,
the else block must leave with return, throw, break, or continue.
Afterward, value is an ordinary local. Use if let when the value is needed
only inside one conditional branch, and match when several cases need their
own behavior.
Operations that may fail
Result<T, E> represents either success with Ok(...) or an expected problem
with Error(...).
func Divide(a: int, b: int) -> Result<int, string> {
if b == 0 {
return Error("Cannot divide by zero")
}
return Ok(a / b)
}
match Divide(10, 0) {
Ok(let answer) => WriteLine("Answer: $answer")
Error(let message) => WriteLine("Problem: $message")
}
The function's type makes the possible problem visible to its caller.
Passing behavior to functions
Functions are also values. One function can receive another function:
func Transform(value: int, operation: (int) -> int) -> int {
return operation(value)
}
let doubled = Transform(5, number => number * 2)
let squared = Transform(5, number => number * number)
(int) -> int means a function that receives an integer and returns an integer.
This lets programs customize behavior without creating a class containing one
method.
When to use a class
Classes are an important part of Raven. Use one when something has identity, owns changing state, controls a resource lifecycle, protects internal details, or participates in object-oriented polymorphism.
class ScoreBoard {
var Score: int = 0
func Add(points: int) -> () {
Score = Score + points
}
}
let board = ScoreBoard()
board.Add(10)
let anotherBoard = ScoreBoard()
anotherBoard.Add(10)
let scoresAreEqual = board.Score == anotherBoard.Score
let boardsAreEqual = board == anotherBoard
The score board owns changing state, so a class is a natural model. A standalone
calculation such as Add(a, b) does not need an object merely to contain it.
By default, classes are compared by reference: two variables are equal only
when they refer to the same object. In this example, scoresAreEqual is true,
but boardsAreEqual is false because the two score boards were created
separately. A class can define different equality behavior when a program needs
it.
Functional and object-oriented programming are not rival modes in Raven. They are tools that can be combined in one application.
A small complete program
import System.Console.*
record class Student(val Name: string, val Score: int)
union Grade {
case Passed(score: int)
case Failed(score: int)
}
func GradeStudent(student: Student) -> Grade {
if student.Score >= 60 {
return .Passed(student.Score)
}
return .Failed(student.Score)
}
func Describe(student: Student) -> string {
return GradeStudent(student) match {
.Passed(let score) => "${student.Name} passed with $score"
.Failed(let score) => "${student.Name} needs another try ($score)"
}
}
let students = [
Student("Mira", 84),
Student("Noah", 52),
Student("Ari", 71)
]
for student in students {
WriteLine(Describe(student))
}
The record models student data, the union models possible grades, functions hold the grading rules, and the loop processes the collection. Each construct has a clear job.
How to continue learning
Do not try to memorize the whole language. Change the examples and observe what happens:
- Change names and numbers.
- Add another student.
- Change the passing score.
- Add an
Excellentgrade case and update the match. - Write a function that calculates an average.
Compiler errors are part of learning. Read them, return to the smallest example that demonstrates the problem, and make one change at a time.
Continue with:
- Getting Started for compiler and project commands.
- Language Introduction for a broader tour.
- Domain Modeling for choosing among data and behavior shapes.