Variables
Variables in Charl store values with a name. All variables are immutable by default and statically typed.
Basic Variable Declaration
Variables are declared using the let keyword:
let x = 42
let name = "Charl"
let pi = 3.14159
let is_active = true
Note: The let keyword is required for all variable declarations. Variables cannot be used before they are declared.
Type Annotations
While Charl can infer types, you can explicitly specify them with type annotations:
// Explicit type annotations
let count: int = 42
let price: float = 19.99
let message: string = "Hello"
let flag: bool = true
// Type inference (preferred when obvious)
let count = 42 // Inferred as int
let price = 19.99 // Inferred as float
let message = "Hello" // Inferred as string
let flag = true // Inferred as bool
When to Use Type Annotations
| Situation | Recommendation |
|---|---|
| Type is obvious from value | Use inference: let x = 42 |
| Type might be ambiguous | Use annotation: let x: float = 42.0 |
| Function parameters | Always annotate: fn foo(x: int) |
| Complex types (arrays, tuples) | Consider annotation for clarity |
Immutability
All variables in Charl are immutable by default. Once a value is bound to a name, it cannot be changed.
let x = 42
// x = 50 // ERROR: Cannot reassign immutable variable
// To use a different value, declare a new variable
let x = 50 // This shadows the previous x
print(x) // 50
Shadowing: You can declare a new variable with the same name as a previous variable. This is called shadowing and creates a completely new variable.
Why Immutability?
- Safety: Prevents accidental modification of data
- Clarity: Makes code easier to reason about
- Optimization: Compiler can make better optimization decisions
- Concurrency: Immutable data is inherently thread-safe
// Example: Processing data without mutation
let original = [1, 2, 3, 4, 5]
let doubled = tensor_multiply(tensor(original), tensor([2.0]))
// original is unchanged, doubled contains new values
Variable Scope
Variables are scoped to the block in which they are declared. A block is defined by curly braces {}.
let outer = 10
if true {
let inner = 20
print(outer) // 10 - can access outer scope
print(inner) // 20
}
// print(inner) // ERROR: inner is not in scope
fn example() {
let function_var = 30
print(outer) // ERROR: outer is not in function scope
}
Scope Rules
- Variables are visible only within their declaring block and nested blocks
- Inner scopes can shadow outer scope variables
- Function parameters are scoped to the function body
- Variables declared in loop bodies are scoped to that iteration
let x = 1
fn process() {
let x = 2 // Shadows outer x
if true {
let x = 3 // Shadows function x
print(x) // 3
}
print(x) // 2
}
print(x) // 1
Working with Different Types
Primitive Types
// Integers
let age: int = 25
let year = 2024
// Floats
let temperature: float = 98.6
let ratio = 0.75
// Booleans
let is_valid: bool = true
let has_errors = false
// Strings
let name: string = "Alice"
let greeting = "Hello, World!"
Arrays
// Array of integers
let numbers: [int] = [1, 2, 3, 4, 5]
// Array of floats
let weights = [0.5, 1.0, 1.5, 2.0]
// Array of strings
let names: [string] = ["Alice", "Bob", "Charlie"]
// Empty array (requires type annotation)
let empty: [int] = []
// Nested arrays
let matrix: [[int]] = [[1, 2], [3, 4]]
Tuples
// Tuple with mixed types
let person: (string, int, float) = ("Alice", 30, 5.6)
// Accessing tuple elements
let name = person.0 // "Alice"
let age = person.1 // 30
let height = person.2 // 5.6
// Tuple of tuples
let coords: ((int, int), string) = ((10, 20), "point A")
Tensors
// 1D tensor
let vector = tensor([1.0, 2.0, 3.0, 4.0])
// 2D tensor via reshape
let data = tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
let matrix = tensor_reshape(data, [2, 3])
// Initialized tensors
let zeros = tensor_zeros([10, 10])
let ones = tensor_ones([5, 5])
let random = tensor_randn([100, 50])
Common Patterns
Multiple Variables
// Declare multiple variables
let x = 10
let y = 20
let z = 30
// Constants can be grouped logically
let pi = 3.14159
let e = 2.71828
let golden_ratio = 1.61803
Derived Values
// Calculate values from other variables
let width = 10
let height = 20
let area = width * height
// Chain calculations
let base = 5
let squared = base * base
let doubled = squared * 2
Naming Conventions
- snake_case: Use for variable names:
my_variable - Descriptive names: Prefer
learning_rateoverlr - Boolean prefixes: Use
is_,has_,can_for booleans - Constants: Still use snake_case (no special convention for constants)
// Good naming
let learning_rate = 0.01
let max_iterations = 1000
let is_training = true
let has_converged = false
// Avoid single letters except for common cases
let i = 0 // OK for loop counter
let x = 42 // OK for math/examples
let a = "hi" // Unclear - what is 'a'?
Complete Examples
Example 1: Basic Calculations
// Temperature conversion
let celsius = 25.0
let fahrenheit = celsius * 9.0 / 5.0 + 32.0
print("Temperature: " + str(fahrenheit) + "F")
// Circle calculations
let radius = 5.0
let pi = 3.14159
let area = pi * radius * radius
let circumference = 2.0 * pi * radius
Example 2: Machine Learning Setup
// Neural network parameters
let input_size = 784
let hidden_size = 128
let output_size = 10
// Training hyperparameters
let learning_rate = 0.01
let batch_size = 32
let num_epochs = 100
// Initialize weights
let w1 = tensor_randn([input_size, hidden_size])
let b1 = tensor_zeros([hidden_size])
let w2 = tensor_randn([hidden_size, output_size])
let b2 = tensor_zeros([output_size])
Example 3: Data Processing
// Dataset information
let dataset_name = "MNIST"
let num_samples = 60000
let image_width = 28
let image_height = 28
let num_classes = 10
// Computed values
let image_size = image_width * image_height
let validation_split = 0.2
let validation_samples = int(float(num_samples) * validation_split)
let training_samples = num_samples - validation_samples
Best Practices
DO:
- Use descriptive variable names
- Let the compiler infer types when obvious
- Keep variable scope as narrow as possible
- Initialize variables close to where they're used
- Use type annotations for clarity in complex code
DON'T:
- Use overly abbreviated names (except common conventions)
- Declare variables far from their usage
- Shadow variables unnecessarily
- Use global-like patterns (declare variables in narrow scopes)
- Mix naming conventions in the same codebase