Inherit

Inherit provides compile-time pseudo-inheritance in Elixir through sophisticated AST manipulation, enabling modules to inherit struct fields, functions, and behaviors with zero runtime overhead.

Features

Installation

Add inherit to your list of dependencies in mix.exs:

def deps do
  [
    {:inherit, "~> 0.4.2"}
  ]
end

Basic Usage

Making a module inheritable

Use Inherit in your module and define struct fields:

defmodule Person do
  use Inherit, [
    name: "",
    age: 0
  ]

  def greet(person) do
    "Hello, I'm #{person.name} and I'm #{person.age} years old"
  end
  defoverridable greet: 1

  def adult?(person) do
    person.age >= 18
  end
  defoverridable adult?: 1
  
  def name_length(person) do
    String.length(person.name)
  end
  # No defoverridable - child modules cannot override this
end

Inheriting from a module

Use the parent module in your child module and specify additional fields:

defmodule Employee do
  use Person, [
    salary: 0,
    department: ""
  ]

  # Override parent function with super call
  def greet(employee) do
    super(employee) <> " and I work in #{employee.department}"
  end
  defoverridable greet: 1

  # Access parent module directly using __PARENT__
  def is_adult_person(employee) do
    __PARENT__.adult?(employee)
  end
  
  # This would compile with warning but never be called:
  def name_length(employee),
    do: 999  # Parent didn&#39;t use defoverridable!
end

Using the inherited module

# Create an Employee struct with inherited fields
employee = %Employee{
  name: "John",
  age: 30,
  salary: 50000,
  department: "Engineering"
}

# Call overridden function (with super call)
Employee.greet(employee)
# => "Hello, I&#39;m John and I&#39;m 30 years old and I work in Engineering"

# Call inherited function
Employee.adult?(employee)
# => true

# Call parent function via __PARENT__
Employee.is_adult_person(employee)
# => true

# Function without defoverridable always calls parent version
Employee.name_length(employee)
# => 4 (calls Person.name_length, not any child override)

Advanced Usage

Custom __using__ macros with callbacks

Parent modules can define their own __using__ macros with callback support:

defmodule BaseServer do
  use GenServer
  use Inherit, [state: %{}]

  defmacro __using__(fields) do
    # Before callback ensures GenServer behavior is included first
    before_callback = quote do
      use GenServer
    end

    quote do
      require Inherit
      Inherit.from(unquote(__MODULE__), unquote(fields), before: unquote(before_callback))

      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
end

defmodule MyServer do
  use BaseServer, [additional_field: "value"]
  
  # Inherits GenServer behavior with proper callback order
  # Can override start_link and init if needed
end

Deep inheritance chains

defmodule LivingThing do
  use Inherit, [alive: true]
  
  def life_span(thing), do: thing.alive && 100
  defoverridable life_span: 1
end

defmodule Animal do
  use LivingThing, [mobile: true]
  
  def life_span(animal), do: super(animal) + 50
  defoverridable life_span: 1
end

defmodule Mammal do
  use Animal, [warm_blooded: true]
  
  def life_span(mammal), do: super(mammal) + 25
  defoverridable life_span: 1
end

# Mammal.life_span(%Mammal{}) => 175 (100 + 50 + 25)

Preventing inheritance with defwithhold

By default, all public functions are inherited by child modules. Use defwithhold to prevent specific functions from being inherited:

defmodule Parent do
  use Inherit, [field: 1]

  def inherited_function do
    "This will be inherited"
  end

  def not_inherited_function do
    "This will not be inherited"
  end
  defwithhold not_inherited_function: 0
end

defmodule Child do
  use Parent, []
  
  # Child.inherited_function() works automatically
  # Child.not_inherited_function() raises UndefinedFunctionError
end

Function Overriding Rules

Important: Parent modules control which functions can be overridden by child modules.

Example

defmodule 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"     # ✅ Works - parent used defoverridable
  defoverridable can_override: 0
  
  def cannot_override, do: "child"  # ⚠️ Compiles with warning, never called!
end

# Results:
Child.can_override()    # => "child"
Child.cannot_override() # => "parent only" (parent&#39;s version always used)

Key Differences from OOP Inheritance

Unlike traditional object-oriented inheritance, Inherit operates at compile-time through AST manipulation:

  1. Compile-time: All inheritance is resolved during compilation through AST generation
  2. Explicit overriding: Only functions marked defoverridable can be overridden
  3. Function delegation: Non-overridden functions are automatically generated as delegation calls
  4. AST-based super calls: super() calls are resolved at compile time to direct parent calls
  5. Module-level inheritance: Inheritance works at the module level, not the instance level

How It Works

The inheritance system creates a compile-time inheritance tree where modules can inherit from parent modules through AST manipulation:

flowchart TD
    LivingThing["LivingThing<br/>use Inherit, [alive: true]<br/>defines: breathe(), grow()"] 
    
    Animal["Animal<br/>use LivingThing, [mobile: true]<br/>inherits: breathe(), grow()<br/>defines: move(), hunt()"]
    
    Plant["Plant<br/>use LivingThing, [mobile: false]<br/>inherits: breathe(), grow()<br/>defines: photosynthesize()"]
    
    Mammal["Mammal<br/>use Animal, [warm_blooded: true]<br/>inherits: breathe(), grow(), move(), hunt()<br/>defines: nurse_young()"]
    
    LivingThing --> Animal
    LivingThing --> Plant
    Animal --> Mammal
    
    style LivingThing fill:#FF9800,stroke:#E65100,stroke-width:3px,color:#fff
    style Animal fill:#2196F3,stroke:#0D47A1,stroke-width:3px,color:#fff
    style Plant fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff
    style Mammal fill:#9C27B0,stroke:#4A148C,stroke-width:3px,color:#fff

Technical Implementation:

  1. Compile-time Processing: All inheritance is resolved during compilation for zero runtime overhead
  2. Dual Inheritance Strategy:
    • AST Copying: Functions with no private calls have their AST copied directly to child modules
    • Delegation: Functions that call private functions are inherited as delegation calls to preserve encapsulation
  3. Private Function Detection: The system analyzes parent function AST to detect calls to private functions
  4. Override Resolution: defoverridable functions can be overridden, while others generate delegation calls
  5. Macro Expansion: __PARENT__ and super() calls are expanded to direct module references during compilation

API Reference

Examples

Basic inheritance with field merging:

# Inheritance chain: Animal -> Mammal -> Primate -> Human
%Animal{species: "", habitat: ""}
%Mammal{species: "", habitat: "", warm_blooded: true, fur_type: ""}
%Primate{species: "", habitat: "", warm_blooded: true, fur_type: "", opposable_thumbs: true}
%Human{species: "Homo sapiens", habitat: "Global", warm_blooded: true, fur_type: "", opposable_thumbs: true, language: ""}

Function inheritance strategies:

# AST Copying vs Delegation based on private function usage

# Parent with private function calls
defmodule Parent do
  use Inherit, [field: 1]
  
  defp private_helper(x), do: x * 2
  
  def with_private_call(x) do
    private_helper(x) + 1    # Calls private function
  end
  defoverridable with_private_call: 1
  
  def without_private_call(x) do
    x + 10                   # No private function calls
  end
  defoverridable without_private_call: 1
end

defmodule Child do
  use Parent, [child_field: 2]
end

# Results demonstrate different inheritance strategies:
Child.with_private_call(5)    # => 11 (delegated: apply(Parent, :with_private_call, [5]))
Child.without_private_call(5) # => 15 (AST copied: x + 10)

# Real example - Animal.move/1 calls private validate_movement/1
Animal.move("walk")     # => "Moving by walk" (original implementation)
Mammal.move("run")      # => "Moving by run" (delegated to Animal due to private function call)
Primate.move("swing")   # => "Moving by swing" (delegated through inheritance chain)
Human.move("walk")      # => "Moving by walk" (delegated through inheritance chain)

# Override behavior with proper inheritance control
Animal.describe()       # => "I am an animal"
Mammal.describe()       # => "I am an animal that is warm-blooded" (overrides and calls super())
Primate.describe()      # => "I am an animal that is warm-blooded with opposable thumbs" (overrides and calls __PARENT__)

# Functions without defoverridable cannot be overridden (compilation warning)
Human.special_ability() # => "opposable thumbs" (calls Primate.special_ability, not Human - emits warning)

# Import resolution and GenServer integration with inheritance
%Human{} |> GenServer.start() # Works seamlessly with inherited GenServer behavior

Documentation

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/inherit.