Skip to content

Chapter 2: Variables and Types

The Two Kinds of Variables

Wyn gives you two ways to declare variables, and the choice matters:

wyn
fn main() -> int {
    var count = 0      // Mutable
    const MAX = 100    // Immutable
    
    count = count + 1  // Fine
    // MAX = 200       // Compiler error
    
    return 0
}

Use const by default. Only use var when you actually need to mutate something. This isn't just style-it's about preventing bugs.

Here's why: when you see const, you know that value will never change. You can reason about the code without tracking all possible modifications. When you see var, you know to pay attention-this value might change.

In practice, you'll use const more than you think. Most values don't need to change.

var itself is optional. Assigning to a name that doesn't exist yet declares it, the same way var would:

wyn
fn main() {
    count = 0          // same as `var count = 0`
    count = count + 1
    print(count)
}

This is exactly as mutable as var count = 0-it's just shorter to type. const still has no shortcut, because immutability is worth being explicit about. Use bare assignment for quick, throwaway variables; reach for var/const when you want the declaration to say something about intent.

Type Inference: The Compiler Works For You

Wyn is statically typed, but you rarely write types explicitly:

wyn
fn main() -> int {
    var number = 42        // int
    var pi = 3.14          // float
    var name = "Alice"     // string
    var active = true      // bool
    
    return 0
}

The compiler figures out the types from the values. This isn't dynamic typing-the types are still checked at compile time. You just don't have to write them.

When should you write types explicitly?

wyn
fn main() -> int {
    var count: int = 0     // Explicit type
    return 0
}

Honestly? Almost never. The compiler is better at this than you are. Write explicit types when:

  1. The inferred type is wrong (rare)
  2. You want to document intent (sometimes useful)
  3. The compiler can't infer the type (we'll see examples later)

Otherwise, let the compiler do its job.

The Basic Types

Integers

Integers are whole numbers. On modern systems, they're 64-bit:

wyn
fn main() -> int {
    var age = 25
    var debt = -1000
    var zero = 0
    
    print(age.abs())    // 25
    print(debt.abs())   // 1000
    
    return 0
}

Notice abs() is a method on the integer. This is consistent with everything else in Wyn-if you have a value, you can call methods on it.

What about different integer sizes? Wyn keeps it simple: one integer type. If you need precise control over size, you're probably writing code that interfaces with C, and we'll cover that later.

Floating-Point Numbers

Floats represent decimal numbers:

wyn
fn main() -> int {
    var pi = 3.14159
    var temp = -40.0
    var big = 15000000000.0
    
    print(pi.round())   // 3
    print(pi.floor())   // 3
    print(pi.ceil())    // 4
    
    return 0
}

Floats are IEEE 754 double-precision. If you don't know what that means, don't worry-they work like floats in every other language.

One thing to watch out for:

wyn
fn main() -> int {
    var x = 0.1 + 0.2
    print(x)  // 0.30000000000000004
    return 0
}

This isn't a bug in Wyn. It's how floating-point math works on computers. If you need exact decimal arithmetic (like for money), use integers and track cents, not dollars.

Strings

Strings are sequences of UTF-8 characters:

wyn
fn main() -> int {
    var greeting = "Hello, 世界!"
    var empty = ""
    var multiline = "Line 1\nLine 2"
    
    print(greeting.len())           // 10 (characters, not bytes)
    print(greeting.contains("世"))  // true
    
    return 0
}

Strings are immutable. Every string method returns a new string:

wyn
fn main() -> int {
    var original = "hello"
    var upper = original.upper()
    
    print(original)  // "hello" - unchanged
    print(upper)     // "HELLO" - new string
    
    return 0
}

Why immutable? Because it prevents an entire class of bugs. When you pass a string to a function, you know that function can't modify it. When you store a string in a data structure, you know it won't change out from under you.

The performance cost is usually negligible. The safety benefit is huge.

Booleans

Booleans are true or false:

wyn
fn main() -> int {
    var is_valid = true
    var is_empty = false
    
    if (is_valid and not is_empty) {
        print("Valid and not empty")
    }
    
    return 0
}

That's it. No truthy/falsy values, no implicit conversions. A boolean is a boolean.

Operators: The Usual Suspects

Arithmetic works like you'd expect:

wyn
fn main() -> int {
    var x = 10
    var y = 3
    
    print(x + y)  // 13
    print(x - y)  // 7
    print(x * y)  // 30
    print(x / y)  // 3 (integer division)
    print(x % y)  // 1 (remainder)
    
    return 0
}

Integer division truncates. If you want float division, use floats:

wyn
fn main() -> int {
    var x = 10.0
    var y = 3.0
    print(x / y)  // 3.333...
    return 0
}

Comparison operators:

wyn
fn main() -> int {
    var x = 5
    var y = 10
    
    print(x == y)  // false
    print(x != y)  // true
    print(x < y)   // true
    print(x <= y)  // true
    print(x > y)   // false
    print(x >= y)  // false
    
    return 0
}

Logical operators:

wyn
fn main() -> int {
    var a = true
    var b = false
    
    print(a and b)  // false - AND
    print(a or b)   // true  - OR
    print(not a)    // false - NOT
    
    return 0
}

These short-circuit: and stops at the first false, or stops at the first true. This matters when you have side effects:

wyn
fn expensive_check() -> bool {
    print("Checking...")
    return true
}

fn main() -> int {
    var x = false
    
    if (x and expensive_check()) {
        print("Both true")
    }
    // expensive_check() never runs
    
    return 0
}

Variable Scope: Where Things Live

Variables exist in the block where they're declared:

wyn
fn main() -> int {
    var outer = 10
    
    {
        var inner = 20
        print(outer)  // Works - outer is visible here
        print(inner)  // Works - inner is visible here
    }
    
    print(outer)  // Works
    // print(inner)  // Error - inner doesn't exist here
    
    return 0
}

This is called lexical scoping. A variable's scope is determined by where it appears in the source code, not by when it's created at runtime.

Why does this matter? Because it makes code predictable. You can look at a variable and know exactly where it's valid. No hidden state, no action-at-a-distance.

Shadowing: Reusing Names

You can declare a new variable with the same name as an existing one:

wyn
fn main() -> int {
    var x = 5
    print(x)  // 5
    
    var x = x + 1
    print(x)  // 6
    
    var x = "hello"  // Different type!
    print(x)  // "hello"
    
    return 0
}

This is shadowing. Each var x creates a new variable that shadows the previous one. The old variable still exists-you just can't access it anymore.

Shadowing is useful when transforming values:

wyn
fn main() -> int {
    var input = "  hello  "
    var input = input.trim()
    var input = input.upper()
    print(input)  // "HELLO"
    return 0
}

Without shadowing, you'd need different names: input, trimmed, uppercased. Shadowing lets you reuse the name when the old value isn't needed anymore.

Type Conversions: Explicit is Better

Wyn doesn't do implicit conversions. If you want to convert between types, you have to say so:

wyn
fn main() -> int {
    var num = 42
    var str = num.to_string()
    print(str)  // "42"
    
    var pi = 3.14
    var rounded = pi.round()
    print(rounded)  // 3.0
    
    return 0
}

Why no implicit conversions? Because they hide bugs:

wyn
// In JavaScript:
"5" + 3  // "53" - string concatenation
"5" - 3  // 2 - silently coerced to numbers
// What?

// In Wyn:
"5" - 3  // Error: type mismatch - expected string, got int

The error is annoying, but it catches mistakes. If you want to do arithmetic with a string and an integer, you have to decide: are you doing string concatenation or arithmetic?

wyn
fn main() -> int {
    var str = "5"
    var num = 3
    
    // String concatenation
    var concat = str + num.to_string()  // "53"
    
    // Arithmetic (if str is a number)
    var sum = str.parse_int() + num  // 8 (we'll cover parse_int later)
    
    return 0
}

Explicit conversions make your intent clear.

The print Function: More Flexible Than It Looks

We've been using print without much explanation. It's polymorphic-it works with any type:

wyn
fn main() -> int {
    print(42)           // int
    print(3.14)         // float
    print("hello")      // string
    print(true)         // bool
    print([1, 2, 3])    // array (we'll cover these later)
    return 0
}

For strings, you can include escape sequences:

wyn
fn main() -> int {
    print("Line 1\n")      // Newline
    print("Tab\there")     // Tab
    print("Quote: \"hi\"") // Escaped quote
    return 0
}

Common Patterns You'll Use Constantly

Accumulator

wyn
fn main() -> int {
    var sum = 0
    sum = sum + 10
    sum = sum + 20
    sum = sum + 30
    print(sum)  // 60
    return 0
}

This pattern-start with an initial value, update it repeatedly-is everywhere. You'll see it in loops, in recursive functions, in data processing.

Swap

wyn
fn main() -> int {
    var a = 5
    var b = 10
    
    var temp = a
    a = b
    b = temp
    
    print(a)  // 10
    print(b)  // 5
    return 0
}

Classic algorithm technique. You'll use this when sorting, when rotating arrays, when implementing data structures.

Method Chaining

wyn
fn main() -> int {
    var text = "  HELLO  "
    var result = text.trim().lower().capitalize()
    print(result)  // "Hello"
    return 0
}

This is one of Wyn's killer features. Because methods return values, you can chain them. The code reads left-to-right: trim, then lowercase, then capitalize.

Compare to nested function calls:

wyn
// Hypothetical function-based approach
var result = capitalize(lower(trim(text)))

You have to read this inside-out. Method chaining is more natural.

What You've Learned

Variables in Wyn are simple: use const unless you need var. Types are inferred but checked at compile time. The basic types-integers, floats, strings, booleans-work like you'd expect, with methods instead of functions.

Immutability is the default. Explicit conversions prevent bugs. Scope is lexical and predictable.

These aren't arbitrary rules. They're design choices that make code easier to write, read, and maintain. You'll appreciate them more as you write larger programs.

Exercises

  1. Temperature converter: Write a program that converts Celsius to Fahrenheit. Formula: F = C * 9/5 + 32. Try it with both integer and float values. Notice the difference.

  2. String manipulation: Take a string with mixed case and spaces, trim it, convert to lowercase, and print the length. Do it in one expression using method chaining.

  3. Shadowing practice: Start with a string, shadow it with its length, shadow that with the length converted to a string. Print the final value.

  4. Type conversion: Create an integer, convert it to a string, print the string's length. The answer should be the number of digits.

  5. Operator precedence: Write an expression that evaluates to 20 using only the numbers 2, 3, and 4, and the operators +, *, and parentheses.

  6. Scope experiment: Create a variable in an inner block, try to access it outside the block. Read the error message. Now create a variable in the outer block and access it from the inner block. Understand why one works and the other doesn't.

Don't just read the exercises-write the code. The mistakes you make are more valuable than the solutions you read.


Next: Chapter 3: Control Flow

Try It Yourself

Install Wyn and run this example:

bash
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn

MIT License - v1.19.1