Functions
Functions define reusable operations. They can accept parameters, return values, and be declared at namespace scope, as members of types, or locally inside another function.
Functions are also first-class values in Raven. They can be assigned to variables, passed to other functions, returned as values, and converted to compatible .NET delegate types.
func add(a: int, b: int) -> int {
a + b
}
let result = add(2, 3)
A named func declaration may be:
- a top-level function at namespace scope
- a method when declared as a member of a type
- a local function when declared inside another body
Top-level functions are implicitly static. Methods follow the usual instance and static member rules.
See Top-level code and entry points for namespace-level code and entry points, Classes and members for method-specific member rules, and Async functions for asynchronous functions.
Function bodies and return values
Functions can use a block body:
func add(a: int, b: int) -> int {
a + b
}
The final expression of the block provides the function's result. An explicit
return can be used when returning earlier:
func absolute(value: int) -> int {
if value < 0 {
return -value
}
value
}
Functions may also use an expression body with =>:
func add(a: int, b: int) -> int => a + b
The return type is written after ->. Returned expressions must be convertible
to the declared return type.
Parameters
Parameters normally use the name: Type syntax:
func greet(name: string) {
Console.WriteLine("Hello, ${name}")
}
When an implementation intentionally does not use a parameter, its name may be
replaced with the discard _:
func Handle(_: Request) {
Console.WriteLine("Request received")
}
The discarded parameter remains part of the function signature but does not introduce a name in the body.
Parameter types and ref/out modifiers participate in overload resolution.
val and var binding keywords are not used on ordinary function parameters.
Primary-constructor parameter promotion is the exception, where val and var
declare promoted members.
Default arguments
Parameters can provide default values:
func greet(name: string, punctuation: string = "!") {
Console.WriteLine("Hello, ${name}${punctuation}")
}
greet("Raven")
greet("Raven", "!!!")
A parameter with a default value is optional at the call site. Optional parameters must appear after required parameters.
Default expressions must be compile-time constants and must be implicitly
convertible to the parameter type. This includes literals such as numbers,
strings, and null, parenthesized literals, and unary + or - applied to
numeric literals. As a deliberately narrow union exception, an Option<T>
parameter may default to .None:
func find(name: string, fallback: Option<int> = .None) -> Option<int> {
// ...
}
Raven records this default with Raven-specific parameter metadata so callers
compiled from another assembly reconstruct the active None case rather than
the inactive CLR default state of the union carrier. Payload-bearing .Some
defaults and defaults for other union types are not compile-time parameter
constants.
Raven also recognizes optional parameters from imported .NET methods. Metadata
defaults, including those represented by
System.Runtime.InteropServices.DefaultParameterValueAttribute and
System.Runtime.InteropServices.OptionalAttribute, participate in calls in the
same way as source-declared defaults. When an imported optional parameter has no
stored literal default, Raven uses the parameter type's CLR default value.
Generic functions
Functions and methods can declare type parameters after their name:
func identity<T>(value: T) -> T {
value
}
let number = identity(42)
let text = identity("hello")
Type arguments can usually be inferred from the arguments and expected result. They can also be supplied explicitly:
let text = identity<string>("hello")
Local functions may be generic as well.
Generic constraints
Constraints restrict which types can be used for a type parameter. They can be written inline:
func process<T: class>(value: T) {
// ...
}
or with a where clause:
func process<T>(value: T) where T: class {
// ...
}
For a given type parameter, use either inline constraints or where clauses,
not both.
Supported constraints include:
class— a reference typestruct— a non-nullable value typenotnull— a non-null typeunmanaged— an unmanaged value type- a base class type
- interface types
new()— a public parameterless constructor
Multiple constraints are conjunctive: the type argument must satisfy all of them.
At most one class or struct constraint and one base-class constraint may be
specified. Any number of interface constraints may be used, and new() may
appear at most once. Duplicate constraints are not permitted.
When several constraints are written, their order is:
classorstruct- base class
- interfaces
new()
Where supported by the enclosing declaration, generic type parameters may also
use out for covariance or in for contravariance.
Local functions
Functions can be declared inside other functions, methods, and block bodies:
func calculate(value: int) -> int {
func double(value: int) -> int => value * 2
double(value) + 1
}
A local function is visible within its containing body and can capture values from the enclosing scope:
func createCounter(start: int) {
var current = start
func next() -> int {
current = current + 1
current
}
// ...
}
Local functions support the same generic syntax and constraints as other functions.
Bodies may also contain local class, struct, record, and enum
declarations when a helper type should remain local to that body.
Function expressions
Functions do not need to have a declaration name. A function expression creates a function value that can be stored, passed, or returned:
let add = (a: int, b: int) => a + b
The explicit func form is also available:
let addA = func (x: int) => x + 42
let addB = func (x: int) {
x + 42
}
The shorter lambda form is convenient when the surrounding context already makes it clear that a function value is expected:
let add = x => x + 42
Function expressions may use async, static, or both:
let load = async func (url: string) =>
await client.GetStringAsync(url)
A function expression can optionally declare a local name for recursion:
let fib = func Fib(n: int) =>
if n <= 1 then n else Fib(n - 1) + Fib(n - 2)
The name is visible only inside the function expression.
Target typing
Function expressions are target-typed. When a compatible delegate type is known
from the surrounding context, Raven can infer parameter types from the
delegate's Invoke signature:
let write: (string) -> () = value => Console.WriteLine(value)
The same function expression can therefore be assigned to, passed to, or returned as any compatible delegate type.
Compatibility is based on the delegate's parameter types, ref/out
modifiers, and return type. Delegate types themselves are not implicitly
convertible to one another merely because their signatures match; converting
between distinct delegate types requires an explicit cast.
Destructuring parameters
Function-expression parameters can destructure their input.
Positional deconstruction can unpack tuples and other positional values:
let pickSecond: ((int, string)) -> string =
((a, b)) => b
Sequence deconstruction can unpack collections:
let sumTail: (int[]) -> int =
([head, ..tail]) => head + tail[0]
Patterns can be nested:
let project: (((int, string), int[])) -> string =
(((id, name), [head, ..tail])) =>
"$id:$name:$head:${tail.Length}"
Both ..name and ...name are accepted as rest syntax in sequence
deconstruction.
When binding keywords are omitted in a destructuring function parameter, elements are bound as immutable values by default.
Functions as values
Named functions and methods are first-class values. Referencing a function or method without invoking it produces a callable value:
let writeLine: (string) -> () = Console.WriteLine
writeLine("Hello from Raven!")
This makes functions easy to pass to other functions:
func run(action: (string) -> ()) {
action("ready")
}
run(Console.WriteLine)
The expected function type provides context for selecting a compatible method overload.
If a method has a unique signature, Raven can often infer the function type:
let increment = Counter.Increment
For overloaded methods, an explicit function type or another target-typed context is needed to select the intended overload:
let writeLine: (string) -> () = Console.WriteLine
Method-reference diagnostics
A method group cannot be used where no function or delegate type is available.
For example, let callback = Logger.Log reports RAV2201 when the declaration
does not otherwise determine a callable signature. If more than one overload
matches the target, Raven reports RAV2202; if no overload has the required
signature, it reports RAV2203.
Instance method references capture their receiver:
class Counter {
value: int = 3
func Increment(delta: int) -> int {
self.value + delta
}
func Run() -> int {
let increment = self.Increment
increment(7)
}
}
Here, increment continues to invoke Increment on the same Counter
instance.
Delegate conversions
Values of the same generic delegate type follow its declared variance: return values may widen covariantly and parameters may narrow contravariantly through implicit reference conversions. Nullable reference widening is supported; boxing and unsafe nullable narrowing do not make a variant conversion valid.
Captured values
Local functions and function expressions can capture locals, parameters, and
self from their enclosing scope. Captured mutable variables remain shared, so
changes are visible to every function that captures the same variable.
var count = 0
let next = () => {
count = count + 1
count
}
static func declarations and static function expressions do not capture
enclosing state. self is likewise unavailable in static contexts.
base is available in instance members of classes that have a base class.
Calling a member through base dispatches directly to the selected base member
rather than through an override on the current class.
Raven implements captures using compiler-generated closure storage. Nested capturing functions share the relevant closure state so that all references to a captured variable observe the same value. Non-capturing function expressions use the same general callable representation without capture fields.
Attributes
Functions and their parameters may carry .NET attributes:
[Trace]
func compute(x: int) -> int => x * 2
Attributes can also target the return value:
[return: MaybeNull]
func find(name: string) -> string {
// ...
}
Parameter attributes use the same attribute syntax supported by other function-like declarations.
Explicit attribute targets are validated according to where they appear:
[assembly: ...]and[module: ...]apply at compilation-unit scope.[type: ...]applies to type declarations.[method: ...]applies to functions and methods, and can target a synthesized primary constructor where applicable.[return: ...]applies to callable return metadata.[param: ...]and[parameter: ...]apply to parameters.[property: ...]applies to properties.[field: ...]applies to fields or synthesized backing fields where applicable.[event: ...]applies to events.[class: ...], when followed by a blank line at namespace scope, applies to the synthesizedNamespaceMembersclass of the enclosing lexical namespace. Without that separation it is treated as a target on the following member and rejected there.
.NET delegate interoperability
Raven function values interoperate with .NET delegates. When a function
expression is used where a delegate type is expected, its parameters and return
value are checked against the delegate's Invoke signature.
Method references use the same target-typing rules. For example:
func Run(action: System.Action<string>) {
action("ready")
}
Run(Console.WriteLine)
selects the Console.WriteLine(string) overload.
When a referenced method requires implicit conversions to match the target delegate signature, Raven may synthesize an internal bridge that performs those conversions before invoking the method.
If a function signature cannot be represented by an existing framework
delegate—for example because it contains ref or out parameters—Raven can
synthesize a compatible delegate type:
class Accumulator {
static func TryAccumulate(ref state: int, out doubled: int) -> bool {
state = state + 1
doubled = state * 2
true
}
static func Execute(value: int) -> int {
let callback = Accumulator.TryAccumulate
var current = value
var doubled = 0
callback(¤t, &doubled)
current + doubled
}
}
Imported methods may also use
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute. When
multiple applicable candidates belong to the same overload set, Raven keeps the
highest-priority candidates before applying normal overload comparison.