Understanding 💭 include, extend, and prepend in Ruby

Ruby is well known for its flexibility and expressive syntax. One of its powerful features is the ability to share functionality using modules. While many languages offer mixins, Ruby stands out with three key methods: include, extend, and prepend. Understanding these methods can significantly improve code reuse and clarity. Let’s explore each with examples!

include → Adds Module Methods as Instance Methods

When you use include in a class, it injects the module’s methods as instance methods of the class.

Example: Using include for instance methods

module Greet
  def hello
    "Hello from Greet module!"
  end
end

class User
  include Greet
end

user = User.new
puts user.hello # => "Hello from Greet module!"

Scope: The methods from Greet become instance methods in User.


extend → Adds Module Methods as Class Methods

When you use extend in a class, it injects the module’s methods as class methods.

Example: Using extend for class methods

module Greet
  def hello
    "Hello from Greet module!"
  end
end

class User
  extend Greet
end

puts User.hello # => "Hello from Greet module!"

Scope: The methods from Greet become class methods in User, instead of instance methods.


prepend → Adds Module Methods Before Instance Methods

prepend works like include, but the methods from the module take precedence over the class’s own methods.

Example: Using prepend to override instance methods

module Greet
  def hello
    "Hello from Greet module!"
  end
end

class User
  prepend Greet
  
  def hello
    "Hello from User class!"
  end
end

user = User.new
puts user.hello # => "Hello from Greet module!"

Scope: The methods from Greet override User‘s methods because prepend places Greet before the class in the method lookup chain.


Method Lookup Order

Understanding how Ruby resolves method calls is essential when using these techniques.

The method lookup order is as follows:

  1. Prepend modules (highest priority)
  2. Instance methods in the class itself
  3. Include modules
  4. Superclass methods
  5. Object, Kernel, BasicObject

To visualize this, use the ancestors method:

class User
  prepend Greet
end

puts User.ancestors
# => [Greet, User, Object, Kernel, BasicObject]


How Ruby Differs from Other Languages

  • Ruby is unique in dynamically modifying method resolution order (MRO) using prepend, include, and extend.
  • In Python, multiple inheritance is used with super(), but prepend-like behavior does not exist.
  • JavaScript relies on prototypes instead of module mixins, making Ruby’s approach cleaner and more structured.

Quick Summary

KeywordInjects Methods AsWorks OnTakes Precedence?
includeInstance methodsInstancesNo
extendClass methodsClassesN/A
prependInstance methodsInstancesYes (overrides class methods)

By understanding and using include, extend, and prepend, you can make your Ruby code more modular, reusable, and expressive. Master these techniques to level up your Ruby skills!

Enjoy Ruby 🚀

Programming in C: A Small Note on Functions

Functions play a fundamental role in C programming, providing modularity, reusability, and better code organization. This article explores some key aspects of C functions, including their behavior, return values, and compilation.

Basic Function Concepts

Standard library functions like printf(), getchar(), and putchar() are commonly used in C. A C function cannot be split across multiple files; each function must be fully defined in one file.

The main Function and Return Values

The main function is the entry point of any C program. It returns an integer value, typically:

  • 0 for normal termination
  • A nonzero value for erroneous termination

Example:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0; // Indicates successful execution
}

Function Prototypes and Argument Handling

A function prototype must match its definition and usage. If the number of actual arguments exceeds the number of formal parameters, the extra arguments are ignored. Conversely, if fewer arguments are passed, the missing parameters may contain garbage values.

Example:

#include <stdio.h>

void greet(char *name) {
    printf("Hello, %s!\n", name);
}

int main() {
    greet("Alice", "ExtraArg"); // Compiler warning: too many arguments
    return 0;
}

Variable Scope and Persistence

  • Automatic variables (local variables) do not retain their values across function calls unless declared as static.
  • Static variables retain their values between function calls.

Example:

#include <stdio.h>

void counter() {
    static int count = 0; // Retains value across calls
    count++;
    printf("Count: %d\n", count);
}

int main() {
    counter();
    counter();
    counter();
    return 0;
}

Output:

Count: 1
Count: 2
Count: 3

Return Statements and Garbage Values

A function must return a value if it is declared with a non-void return type. Failing to return a value results in undefined behavior.

Example:

int faultyFunction() {
    // No return statement (causes garbage value to be returned)
}

int main() {
    int value = faultyFunction();
    printf("Returned value: %d\n", value); // Unpredictable result
    return 0;
}

Compilation Across Multiple Files

C allows functions to be spread across multiple source files. Compilation can be done using the gcc command:

$ gcc main.c fun1.c fun2.c -o my_program

This links all object files together to produce the final executable.

Undefined Order of Function Execution

In an expression like:

x = function1() + function2();

The order of execution of function1() and function2() is unspecified. The C standard does not dictate which function gets evaluated first, leading to potential unpredictability in results.

Example:

#include <stdio.h>

int function1() {
    printf("Executing function1\n");
    return 5;
}

int function2() {
    printf("Executing function2\n");
    return 10;
}

int main() {
    int x = function1() + function2();
    printf("x = %d\n", x);
    return 0;
}

Output order may vary, so avoid relying on execution sequence in such cases.

Conclusion

Understanding C functions, their behavior, and proper usage is crucial for writing robust and portable code. Following best practices such as defining proper prototypes, handling return values correctly, and being aware of evaluation order can help avoid unexpected bugs in C programs.