Skip to main content
Examples Verified (100%)

Generic Functions & Classes

Generics are one of the most powerful features in T-Ruby, allowing you to write code that works with multiple types while maintaining type safety. Think of generics as "type variables"—placeholders that get filled in with concrete types when your code is used.

Why Generics?

Without generics, you'd need to write the same function multiple times for different types, or lose type safety by using Any. Generics let you write code once and reuse it with different types.

The Problem: Without Generics

# Without generics, you need separate functions for each type
def first_string(arr: Array<String>): String | nil
arr[0]
end

def first_integer(arr: Array<Integer>): Integer | nil
arr[0]
end

def first_user(arr: Array<User>): User | nil
arr[0]
end

# Or you lose type safety
def first(arr: Array<Any>): Any
arr[0] # Return type is Any - no type safety!
end

The Solution: With Generics

# One function that works for all types
def first<T>(arr: Array<T>): T | nil
arr[0]
end

# TypeScript-style inference works automatically
names = ["Alice", "Bob", "Charlie"]
result = first(names) # result is String | nil

numbers = [1, 2, 3]
value = first(numbers) # value is Integer | nil

Generic Functions

Generic functions use type parameters in angle brackets (<T>) to represent types that will be determined when the function is called.

Basic Generic Function

# A simple generic function
def identity<T>(value: T): T
value
end

# Works with any type
str = identity("hello") # String
num = identity(42) # Integer
arr = identity([1, 2, 3]) # Array<Integer>

Multiple Type Parameters

You can use multiple type parameters when needed:

# A function with two type parameters
def pair<K, V>(key: K, value: V): Hash<K, V>
{ key => value }
end

# Type inference works for both parameters
result = pair("name", "Alice") # Hash<String, String>
data = pair(:id, 123) # Hash<Symbol, Integer>
mixed = pair("count", 42) # Hash<String, Integer>

Generic Functions with Arrays

A common use case is working with arrays of any type:

# Get the last element of an array
def last<T>(arr: Array<T>): T | nil
arr[-1]
end

# Reverse an array
def reverse<T>(arr: Array<T>): Array<T>
arr.reverse
end

# Filter an array with a predicate
def filter<T>(arr: Array<T>, &block: Proc<T, Boolean>): Array<T>
arr.select { |item| block.call(item) }
end

# Usage
numbers = [1, 2, 3, 4, 5]
evens = filter(numbers) { |n| n.even? } # Array<Integer>

words = ["hello", "world", "foo", "bar"]
long_words = filter(words) { |w| w.length > 3 } # Array<String>

Generic Functions with Return Type Transformation

Sometimes the return type differs from the input type, but is still generic:

# Map function that transforms type T to type U
def map<T, U>(arr: Array<T>, &block: Proc<T, U>): Array<U>
arr.map { |item| block.call(item) }
end

# Transform integers to strings
numbers = [1, 2, 3]
strings = map(numbers) { |n| n.to_s } # Array<String>

# Transform strings to their lengths
words = ["hello", "world"]
lengths = map(words) { |w| w.length } # Array<Integer>

Generic Classes

Generic classes allow you to create data structures that work with any type while maintaining type safety throughout the class.

Basic Generic Class

# A simple generic container
class Box<T>
@value: T

def initialize(value: T): void
@value = value
end

def get: T
@value
end

def set(value: T): void
@value = value
end
end

# Create boxes with different types
string_box = Box<String>.new("hello")
puts string_box.get # "hello"

number_box = Box<Integer>.new(42)
puts number_box.get # 42

# Type safety is enforced
string_box.set("world") # OK
string_box.set(123) # Error: Type mismatch

Generic Class with Type Inference

T-Ruby can often infer the type parameter from the constructor:

class Container<T>
@item: T

def initialize(item: T): void
@item = item
end

def item: T
@item
end

def update(new_item: T): void
@item = new_item
end
end

# Type inference from constructor argument
container1 = Container.new("hello") # Container<String>
container2 = Container.new(42) # Container<Integer>

# Or explicitly specify the type
container3 = Container<Boolean>.new(true)

Generic Stack Example

Here's a practical example of a generic stack data structure:

class Stack<T>
@items: Array<T>

def initialize: void
@items = []
end

def push(item: T): void
@items.push(item)
end

def pop: T | nil
@items.pop
end

def peek: T | nil
@items.last
end

def empty?: Boolean
@items.empty?
end

def size: Integer
@items.length
end

def to_a: Array<T>
@items.dup
end
end

# Usage with strings
string_stack = Stack<String>.new
string_stack.push("first")
string_stack.push("second")
string_stack.push("third")
puts string_stack.pop # "third"
puts string_stack.size # 2

# Usage with integers
int_stack = Stack<Integer>.new
int_stack.push(1)
int_stack.push(2)
int_stack.push(3)
puts int_stack.peek # 3 (doesn't remove)
puts int_stack.size # 3

Generic Class with Multiple Type Parameters

Generic classes can have multiple type parameters:

class Pair<K, V>
@key: K
@value: V

def initialize(key: K, value: V): void
@key = key
@value = value
end

def key: K
@key
end

def value: V
@value
end

def swap: Pair<V, K>
Pair.new(@value, @key)
end

def to_s: String
"#{@key} => #{@value}"
end
end

# Create pairs with different type combinations
name_age = Pair.new("Alice", 30) # Pair<String, Integer>
id_name = Pair.new(123, "Bob") # Pair<Integer, String>
coords = Pair.new(10.5, 20.3) # Pair<Float, Float>

# Swap creates a new pair with types reversed
swapped = name_age.swap # Pair<Integer, String>

Generic Collection Class

A more complex example showing a custom collection:

class Collection<T>
@items: Array<T>

def initialize(items: Array<T> = []): void
@items = items.dup
end

def add(item: T): void
@items.push(item)
end

def remove(item: T): Boolean
if index = @items.index(item)
@items.delete_at(index)
true
else
false
end
end

def contains?(item: T): Boolean
@items.include?(item)
end

def first: T | nil
@items.first
end

def last: T | nil
@items.last
end

def map<U>(&block: Proc<T, U>): Collection<U>
Collection<U>.new(@items.map { |item| block.call(item) })
end

def filter(&block: Proc<T, Boolean>): Collection<T>
Collection.new(@items.select { |item| block.call(item) })
end

def each(&block: Proc<T, void>): void
@items.each { |item| block.call(item) }
end

def to_a: Array<T>
@items.dup
end

def size: Integer
@items.length
end
end

# Usage
numbers = Collection<Integer>.new([1, 2, 3, 4, 5])
numbers.add(6)

# Map transforms the collection to a new type
strings = numbers.map { |n| n.to_s } # Collection<String>

# Filter maintains the same type
evens = numbers.filter { |n| n.even? } # Collection<Integer>

# Iterate over items
numbers.each { |n| puts n }

Generic Methods in Non-Generic Classes

You can have generic methods in classes that aren't themselves generic:

class Utils
# Generic method in a non-generic class
def self.wrap<T>(value: T): Array<T>
[value]
end

def self.duplicate<T>(value: T, times: Integer): Array<T>
Array.new(times, value)
end

def self.zip<T, U>(arr1: Array<T>, arr2: Array<U>): Array<Pair<T, U>>
arr1.zip(arr2).map { |t, u| Pair.new(t, u) }
end
end

# Usage
wrapped = Utils.wrap(42) # Array<Integer>
duplicates = Utils.duplicate("hello", 3) # Array<String>
zipped = Utils.zip([1, 2], ["a", "b"]) # Array<Pair<Integer, String>>

Nested Generics

Generics can be nested to create complex type structures:

# A cache that stores arrays of values for each key
class Cache<K, V>
@store: Hash<K, Array<V>>

def initialize: void
@store = {}
end

def add(key: K, value: V): void
@store[key] ||= []
@store[key].push(value)
end

def get(key: K): Array<V>
@store[key] || []
end

def has_key?(key: K): Boolean
@store.key?(key)
end
end

# Usage
user_tags = Cache<Integer, String>.new # Cache<Integer, String>
user_tags.add(1, "ruby")
user_tags.add(1, "programming")
user_tags.add(2, "design")

tags = user_tags.get(1) # Array<String> = ["ruby", "programming"]

Best Practices

1. Use Descriptive Type Parameter Names

# Good: Descriptive names for domain-specific types
class Repository<Entity, Id>
def find(id: Id): Entity | nil
# ...
end
end

# OK: Standard conventions for generic collections
class List<T>
# ...
end

# Avoid: Non-descriptive single letters for complex scenarios
class Processor<A, B, C, D> # Too cryptic
# ...
end

2. Keep Generic Functions Simple

# Good: Simple, focused generic function
def head<T>(arr: Array<T>): T | nil
arr.first
end

# Less good: Too many responsibilities
def process<T>(arr: Array<T>, flag: Boolean, count: Integer): Array<T> | Hash<Integer, T>
# Too complex, hard to understand the generic behavior
end

3. Use Type Inference When Possible

# Let T-Ruby infer types from arguments
container = Container.new("hello") # Container<String> inferred

# Only specify types when necessary
container = Container<String | Integer>.new("hello")

Common Patterns

Option/Maybe Type

class Option<T>
@value: T | nil

def initialize(value: T | nil): void
@value = value
end

def is_some?: Boolean
!@value.nil?
end

def is_none?: Boolean
@value.nil?
end

def unwrap: T
raise "Called unwrap on None" if @value.nil?
@value
end

def unwrap_or(default: T): T
@value || default
end

def map<U>(&block: Proc<T, U>): Option<U>
if @value
Option.new(block.call(@value))
else
Option<U>.new(nil)
end
end
end

# Usage
some = Option.new(42)
none = Option<Integer>.new(nil)

puts some.unwrap_or(0) # 42
puts none.unwrap_or(0) # 0

result = some.map { |n| n * 2 } # Option<Integer> with value 84

Result Type

class Result<T, E>
@value: T | nil
@error: E | nil

def self.ok(value: T): Result<T, E>
result = Result<T, E>.new
result.instance_variable_set(:@value, value)
result
end

def self.err(error: E): Result<T, E>
result = Result<T, E>.new
result.instance_variable_set(:@error, error)
result
end

def ok?: Boolean
!@value.nil?
end

def err?: Boolean
!@error.nil?
end

def unwrap: T
raise "Called unwrap on Err: #{@error}" if @error
@value
end

def unwrap_err: E
raise "Called unwrap_err on Ok" if @value
@error
end
end

# Usage
def divide(a: Integer, b: Integer): Result<Float, String>
if b == 0
Result.err("Division by zero")
else
Result.ok(a.to_f / b)
end
end

result = divide(10, 2)
puts result.unwrap if result.ok? # 5.0

result = divide(10, 0)
puts result.unwrap_err if result.err? # "Division by zero"

Next Steps

Now that you understand generic functions and classes, you can: