Inherit (inherit v0.4.1)
View SourceInherit provides compile-time pseudo-inheritance in Elixir through sophisticated AST manipulation, allowing modules to inherit struct fields, generate inherited function definitions, and override behaviors from parent modules.
All inheritance is resolved at compile-time through AST processing, making it highly efficient with no runtime overhead.
Features
- Compile-time AST-based inheritance: Functions are inherited through AST generation, not runtime delegation
- Struct field inheritance: Child modules inherit all fields from parent modules with field merging
- Function overriding with
defoverridable: Parent functions marked withdefoverridablecan be overridden by child modules __PARENT__module access: Use__PARENT__macro for direct parent module references in function bodiessuper()calls: Call the parent implementation when overriding inherited functions (resolved at compile-time)- Function withholding: Use
defwithholdto prevent specific functions from being inherited - Deep inheritance chains: Support for multiple levels of inheritance with proper AST propagation
- Custom
__using__inheritance: Parent modules can define custom__using__macros that are inherited - Private function call detection: Automatically detects and handles private function calls within inherited functions
- GenServer integration: Works seamlessly with GenServer and other OTP behaviors
Basic Usage
Making a module inheritable
defmodule Parent do
use Inherit, [
field1: "default_value",
field2: 42
]
def some_function(value) do
value + 1
end
defoverridable some_function: 1 # Child modules can override this
def another_function do
"parent implementation"
end
# No defoverridable - child modules cannot override this
endInheriting from a module
defmodule Child do
use Parent, [
field3: "additional_field"
]
# Override a parent function (only works if parent used defoverridable)
def some_function(value) do
super(value) + 10 # Calls Parent.some_function/1 and adds 10
end
defoverridable some_function: 1
# Access parent module directly using __PARENT__
def call_parent do
__PARENT__.another_function()
end
# This would compile with a warning but never be called:
# def another_function, do: "child implementation" # Parent didn't use defoverridable!
endAdvanced Usage
Custom __using__ macros
Parent modules can define their own __using__ macros that will be inherited:
defmodule BaseServer do
use GenServer
use Inherit, [state: %{}]
defmacro __using__(fields) do
quote do
use GenServer
require Inherit
Inherit.from(unquote(__MODULE__), unquote(fields))
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
defoverridable start_link: 1
end
end
@impl true
def init(opts) do
{:ok, struct(__MODULE__, opts)}
end
defoverridable init: 1
endDeep inheritance chains
defmodule LivingThing do
use Inherit, [alive: true]
def life_span(thing), do: thing.alive && 50
defoverridable life_span: 1 # Must mark as overridable for children to override
end
defmodule Animal do
use LivingThing, [mobile: true]
def life_span(animal), do: super(animal) + 30
defoverridable life_span: 1 # Mark as overridable for further children
end
defmodule Mammal do
use Animal, [warm_blooded: true]
def life_span(mammal), do: super(mammal) + 20
defoverridable life_span: 1
end
# Mammal.life_span(%Mammal{}) => 100 (50 + 30 + 20)Function withholding with defwithhold
defmodule Parent do
use Inherit, [field: 1]
def inherited_function do
"This will be inherited"
end
def private_function do
"This will not be inherited"
end
defwithhold private_function: 0 # Prevents inheritance
end
defmodule Child do
use Parent, []
# Child.inherited_function() works automatically
# Child.private_function() raises UndefinedFunctionError
endImportant: Function Overriding Rules
Parent modules control which functions can be overridden by child modules:
Functions marked with
defoverridablein the parent can be overridden by childrenFunctions NOT marked with
defoverridablecannot be overridden (attempts will compile with warnings but never execute)Child modules must also use
defoverridablewhen overriding to allow further inheritancedefmodule Parent do
use Inherit, [field: 1] def can_override, do: "parent" defoverridable can_override: 0 def cannot_override, do: "parent only" # No defoverridable!end
defmodule Child do
use Parent, [] def can_override, do: "child" # This works - parent used defoverridable defoverridable can_override: 0 def cannot_override, do: "child" # Compiles with warning, never called!end
# Child.can_override() => "child" # Child.cannot_override() => "parent only" (parent's version always used)
How It Works
The inheritance system operates at compile-time with sophisticated AST processing:
- @before_compile Timing: Uses
@before_compilecallback for optimal AST access and processing timing - Intelligent Import Resolution: Automatically detects imported functions/macros and injects
requirestatements - Dual Inheritance Strategy: Functions are inherited using two approaches based on their implementation:
- AST Copying: Functions with no private calls have their AST copied directly to child modules
- Delegation: Functions calling private functions are inherited as delegation calls to preserve encapsulation
- Enhanced Private Function Detection: Uses
Module.definitions_in/2for accurate private function tracking - Advanced Argument Processing: Handles complex argument patterns (guards, defaults, destructuring, pattern matching)
- Callback System: Supports
beforeandaftercallbacks during inheritance for custom setup - Macro Expansion:
__PARENT__andsuper()calls are expanded to direct module references during compilation
Function Inheritance Strategies
AST Copying (for functions with no private calls):
# Parent function:
def simple_add(a, b), do: a + b
# Child gets this AST directly copied:
def simple_add(a, b), do: a + b # Same implementationDelegation (for functions with private calls):
# Parent function:
defp private_multiply(x), do: x * 2
def complex_calc(x), do: private_multiply(x) + 1
# Child gets a delegation call:
def complex_calc(x), do: apply(Parent, :complex_calc, [x])This ensures private functions remain encapsulated in their original module while still allowing inheritance.
API Reference
__PARENT__- Compile-time macro that expands to the immediate parent modulesuper(args...)- Calls the parent implementation when overriding inherited functions (compile-time resolved)defwithhold- Prevents specified functions from being inherited by child modules
Real Examples from Refactored Implementation
# Inheritance chain: Animal -> Mammal -> Primate -> Human (from test suite)
defmodule Animal do
use GenServer
use Inherit, [species: "", habitat: "", alive: true]
# Custom __using__ with callback support
defmacro __using__(fields) do
before_callback = quote do
use GenServer # Ensure GenServer behavior is included
end
quote do
require Inherit
Inherit.from(unquote(__MODULE__), unquote(fields), before: unquote(before_callback))
def breathe(animal), do: "breathing as #{animal.species}"
defoverridable breathe: 1
end
end
# Function that calls private function - will be DELEGATED in children
def move(animal, method) do
validate_movement(method) # Calls private function
"Moving by #{method}"
end
defoverridable move: 2
defp validate_movement(method) do
method in ["walk", "run", "swim"] || raise "Invalid movement: #{method}"
end
# Function with no private calls - AST will be COPIED to children
def describe(animal) do
"I am a #{animal.species}" # No private function calls
end
defoverridable describe: 1
# Function using imported utility with automatic require injection
def log_species(animal) do
Logger.info("Species: #{animal.species}") # Auto-detects Logger import
end
end
defmodule Mammal do
use Animal, [warm_blooded: true, fur_type: ""]
def describe(mammal) do
super(mammal) <> " that is warm-blooded" # Calls parent via super
end
defoverridable describe: 1
end
defmodule Primate do
use Mammal, [opposable_thumbs: true]
def describe(primate) do
__PARENT__.describe(primate) <> " with opposable thumbs" # Direct parent call
end
defoverridable describe: 1
end
defmodule Human do
use Primate, [language: "", culture: ""]
# This function will never be called because Primate doesn't mark describe/1
# as defoverridable (demonstrates inheritance control)
def describe(human) do
__PARENT__.describe(human) <> " and complex language"
end
end
# Results demonstrate sophisticated inheritance features:
# move/2 is DELEGATED because it calls private validate_movement/1
Mammal.move(%Mammal{species: "dog"}, "run") # => "Moving by run" (delegated to Animal)
Primate.move(%Primate{species: "chimp"}, "swing") # => "Moving by swing" (delegated through chain)
# describe/1 has AST COPIED and demonstrates override chain
Human.describe(%Human{species: "Homo sapiens"}) # Uses Primate.describe (not Human due to no defoverridable)
# => "I am a Homo sapiens that is warm-blooded with opposable thumbs"
# GenServer integration works seamlessly through callback system
{:ok, pid} = GenServer.start(Human, []) # Inherits GenServer behavior properly
Summary
Functions
Makes a module inheritable by setting up the inheritance infrastructure.
Retrieves inheritance attributes for a given module.
Prevents specified functions from being inherited by child modules.
Establishes inheritance from a parent module to the current module.
Merges parent struct fields with child fields for inheritance.
Functions
Makes a module inheritable by setting up the inheritance infrastructure.
This macro is used when creating a new inheritable module (root parent). It:
- Defines a struct with the specified fields
- Sets up custom macro imports for inheritance functionality
- Creates a default
__using__/1macro for child modules to inherit from this module - Establishes the module as the root of an inheritance hierarchy
Parameters
fields- A keyword list defining the struct fields and their default values
Example
defmodule Animal do
use Inherit, [
species: "",
habitat: "",
alive: true
]
def breathe(animal) do
if animal.alive, do: "breathing", else: "not breathing"
end
defoverridable breathe: 1
endThis creates an Animal module that can be inherited from using use Animal, [...].
Retrieves inheritance attributes for a given module.
Returns the inheritance metadata stored in the module's $inherit attribute,
which contains information about the module's inheritance hierarchy and AST data.
Parameters
module- The module to retrieve inheritance attributes for
Returns
A map containing inheritance metadata including:
:parent- The parent module (if any):ast- Stored AST for functions:private_funcs- Private function definitions:requires- Required modules for imports
Example
iex> Inherit.attributes_for(MyChildModule)
%{parent: MyParentModule, ast: [...], private_funcs: [...], requires: [...]}
Prevents specified functions from being inherited by child modules.
This macro removes functions from the inheritance mechanism, ensuring they will
not be automatically generated in child modules through AST processing. Functions
marked with defwithhold remain exclusive to the module that defines them.
Parameters
keywords- A keyword list of{function_name, arity}pairs specifying which functions should not be inheritable
Example
defmodule Vehicle do
use Inherit, [wheels: 4]
def start_engine do
"Engine starting..."
end
def internal_diagnostics do
"Running internal checks..."
end
defwithhold internal_diagnostics: 0 # Keep this function private to Vehicle
end
defmodule Car do
use Vehicle, [doors: 4]
# Car automatically inherits start_engine/0
# Car does NOT inherit internal_diagnostics/0
# Calling Car.internal_diagnostics() would raise UndefinedFunctionError
endTechnical Implementation
defwithhold removes function entries from the module's AST tracking, excluding
them from the inheritance AST generation process.
Establishes inheritance from a parent module to the current module.
This macro is the core of the inheritance system. It processes the parent module's functions and generates appropriate inheritance code based on whether each function calls private functions or not. It also merges struct fields from the parent.
Parameters
parent- The parent module to inherit fromfields- A keyword list of additional struct fields to define in the child module
Inheritance Process
- Field Merging: Merges parent struct fields with child fields (child fields override parent fields)
- Function Analysis: Analyzes each parent function to detect private function calls
- AST Generation: Generates either direct AST copies or delegation calls based on analysis
- Override Setup: Preserves
defoverridableinformation for child overrides
Generated Code Strategies
For functions with no private calls:
# Parent function AST is copied directly:
def some_function(arg), do: arg + 1For functions with private calls:
# Delegation call is generated:
def some_function(arg), do: apply(Parent, :some_function, [arg])Example
defmodule Animal do
use Inherit, [species: ""]
defp validate_species(animal), do: animal.species != ""
def describe(animal) do
validate_species(animal) # Calls private function
"A #{animal.species}"
end
defoverridable describe: 1
end
defmodule Mammal do
use Animal, [warm_blooded: true] # Calls Inherit.from(Animal, [warm_blooded: true])
# Mammal.describe/1 is generated as: apply(Animal, :describe, [mammal])
# This preserves access to Animal's private validate_species/1
endTechnical Notes
This macro is automatically called when a module uses another inheritable module.
It should not be called directly by users - instead use use ParentModule, fields.
Merges parent struct fields with child fields for inheritance.
This function is used internally during the inheritance process to combine parent module struct fields with additional fields specified by the child module. Child fields take precedence over parent fields when there are conflicts.
Parameters
parent- The parent module to inherit struct fields fromfields- A keyword list of additional fields to merge
Returns
A keyword list containing the merged struct fields.
Example
iex> Inherit.merge_from(ParentModule, [child_field: "value"])
[parent_field1: "default", parent_field2: 42, child_field: "value"]Technical Notes
This function is called automatically during inheritance setup and should not typically be called directly by user code.