Part 4 of a series exploring the technical challenges behind building Miraas, an Islamic inheritance calculator

A Blast from the Past: Logic Programming Meets Modern C#

Before we dive into the blocking engine, I want to share something that might surprise you: some of the best ideas for modeling complex business rules come from a programming paradigm that peaked in the 1980s.

I’m talking about Prolog.

Now, before you close this tab thinking “I’m not learning Prolog in 2025,” hear me out. You don’t need to learn Prolog. But understanding how it thinks about problems can fundamentally change how you approach complex rule-based systems - whether you’re writing Java, Python, or C#.

The Prolog Way: Declaring Truth, Not Writing Algorithms

Let’s start with the classic Prolog example - family relationships. Here’s how you’d model a simple family tree in Prolog:

% Facts: These are just true statements about the world
parent(john, mary).
parent(john, paul).
parent(susan, mary).
parent(susan, paul).
parent(mary, anne).
parent(paul, peter).

male(john).
male(paul).
male(peter).
female(susan).
female(mary).
female(anne).

% Rules: Logical implications
father(F, C) :- parent(F, C), male(F).
mother(M, C) :- parent(M, C), female(M).

grandparent(G, C) :- parent(G, P), parent(P, C).

% Recursive rule
ancestor(A, C) :- parent(A, C).
ancestor(A, C) :- parent(A, X), ancestor(X, C).

Look at what’s happening here. We’re not writing loops or if-statements. We’re declaring what is true, and Prolog figures out how to prove it.

When you query:

?- ancestor(john, anne).

Prolog doesn’t execute a function. It searches through the facts and rules to prove whether this statement is true. It’s like having a tiny theorem prover built into your programming language.

Why This Matters for Business Rules

Islamic inheritance law is fundamentally a rule-based system:

  • “Sons block uncles from inheriting”
  • “If there are male descendants, full brothers are blocked”
  • “A mother gets 1/6 if children exist, otherwise 1/3”

These aren’t algorithms - they’re logical rules. And when you have dozens of interdependent rules, the Prolog way of thinking becomes incredibly powerful.

From Prolog to JavaScript: Making It Practical

Now, we’re not going to write our inheritance calculator in Prolog. But we can borrow its declarative style using functional programming concepts that exist in modern languages.

Let’s translate that family example to JavaScript:

Step 1: Facts as Data

// Facts are just immutable data structures
const parents = [
    { parent: 'john', child: 'mary' },
    { parent: 'john', child: 'paul' },
    { parent: 'susan', child: 'mary' },
    { parent: 'susan', child: 'paul' },
    { parent: 'mary', child: 'anne' },
    { parent: 'paul', child: 'peter' }
];

const people = [
    { name: 'john', gender: 'male' },
    { name: 'paul', gender: 'male' },
    { name: 'peter', gender: 'male' },
    { name: 'susan', gender: 'female' },
    { name: 'mary', gender: 'female' },
    { name: 'anne', gender: 'female' }
];

Step 2: Rules as Functions

// Father rule: parent(F, C) AND male(F)
function fathers(parents, people) {
    return parents
        .filter(p => people.find(person => 
            person.name === p.parent && person.gender === 'male'))
        .map(p => ({ father: p.parent, child: p.child }));
}

// Grandparent rule: parent(G, P) AND parent(P, C)
function grandparents(parents) {
    return parents.flatMap(p1 =>
        parents
            .filter(p2 => p1.child === p2.parent)
            .map(p2 => ({ grandparent: p1.parent, child: p2.child }))
    );
}

Notice flatMap? That’s doing something subtle and powerful. For each parent relationship, it’s finding all the ways that person is also a parent, then flattening the results. This is how we handle the “multiple possible answers” nature of logic programming.

Step 3: Recursive Rules

Here’s where it gets interesting:

// Ancestor rule: recursive reasoning
function ancestors(person, parents, visited = new Set()) {
    // Prevent infinite loops
    if (visited.has(person)) return [];
    visited.add(person);
    
    // Find direct parents
    const directParents = parents
        .filter(p => p.child === person)
        .map(p => p.parent);
    
    // Recursively find their ancestors
    const ancestorParents = directParents.flatMap(p => 
        ancestors(p, parents, visited)
    );
    
    return [...directParents, ...ancestorParents];
}

// Usage
console.log(ancestors('anne', parents));
// ['mary', 'john', 'susan']

This is the JavaScript equivalent of Prolog’s recursive rule. We’re not writing an algorithm that “searches for ancestors” - we’re declaring that ancestors are either direct parents OR ancestors of parents.

The Functional Concepts You Just Used

If you’re coming from Java or Python, you might not realize you just used some powerful functional programming concepts:

1. Immutable Data

Our facts never change. We don’t modify the parents array; we create new arrays from it.

2. Pure Functions

fathers() and grandparents() always return the same output for the same input. No side effects, no hidden state.

3. Higher-Order Functions

filter, map, and flatMap are functions that take other functions as arguments. This is how we compose complex logic from simple pieces.

4. Declarative Style

We describe what we want (all fathers, all grandparents) rather than how to compute it (loop through this, check that, accumulate results).

Enter C#: The Best of Both Worlds

Now here’s where C# shines. Unlike pure functional languages, C# lets us mix object-oriented modeling with functional composition. We get type safety, inheritance, and all the OOP goodness, while still thinking in a declarative, rule-based way.

The Heir Hierarchy: OOP for Domain Modeling

Remember our Heir hierarchy from the previous post? This is classic OOP:

public abstract class Heir
{
    public abstract GenderType Gender { get; }
    public abstract RelationType Relation { get; }
    public int Count { get; set; } = 1;
}

public class Son : Heir
{
    public override GenderType Gender => GenderType.Male;
    public override RelationType Relation => RelationType.Son;
}

public class Daughter : Heir
{
    public override GenderType Gender => GenderType.Female;
    public override RelationType Relation => RelationType.Daughter;
}

This hierarchy gives us:

  • Type safety (can’t create an invalid heir)
  • Polymorphism (each heir knows its own identity)
  • IntelliSense support (IDE guides you to valid operations)

Extension Methods: Functional Rules Over OOP Objects

But here’s where we blend in the functional style. Instead of putting logic inside the Heir classes, we use extension methods to create Prolog-like rules:

static class DomainExtensions
{
    public static bool DeceasedHasChildren(this InheritanceCase i) =>
        i.HasHeir(RelationType.Son) || i.HasHeir(RelationType.Daughter);
    
    public static bool DeceasedHasDescendants(this InheritanceCase i) =>
        DeceasedHasChildren(i) || DeceasedHasGrandChildren(i);
    
    public static bool DeceasedHasMaleDescendantsOrAscendants(this InheritanceCase i) =>
        DeceasedHasMaleDescendants(i) || DeceasedHasMaleAscendants(i);
}

These read like Prolog rules:

has_descendants(Case) :- has_children(Case).
has_descendants(Case) :- has_grandchildren(Case).

But they’re just C# extension methods. The beauty is that they compose:

if (inheritanceCase.DeceasedHasDescendants())
{
    // This reads like natural language
    // But it's actually a chain of logical rules
}

The Blocking Engine: Rules as Data

Now let’s look at how we applied these concepts to the blocking engine. In Islamic inheritance, certain heirs block others from inheriting. This is a perfect use case for declarative rules.

Approach 1: The Naive Way (Don’t Do This)

// ❌ Imperative, hard to maintain
public bool IsBlocked(RelationType heir, InheritanceCase case)
{
    if (heir == RelationType.Grandfather)
    {
        if (case.HasHeir(RelationType.Son)) return true;
        if (case.HasHeir(RelationType.Father)) return true;
    }
    if (heir == RelationType.FullBrother)
    {
        if (case.HasHeir(RelationType.Father)) return true;
        if (case.HasHeir(RelationType.Son)) return true;
        // ... 20 more conditions
    }
    // ... 15 more heir types
}

This is the procedural approach. It works, but it’s a nightmare to maintain and doesn’t reflect how Islamic scholars think about blocking rules.

Approach 2: Rules as Data (Prolog-Inspired)

class BlockingRule
{
    public RelationType Blocker { get; }
    public RelationType Blocked { get; }
    
    public BlockingRule(RelationType blocker, RelationType blocked)
    {
        Blocker = blocker;
        Blocked = blocked;
    }
}

public class BlockingEngine
{
    readonly List<BlockingRule> rules;
    
    public BlockingEngine()
    {
        rules =
        [
            new BlockingRule(RelationType.Son, RelationType.Grandfather),
            new BlockingRule(RelationType.Father, RelationType.Grandfather),
            new BlockingRule(RelationType.Son, RelationType.FullBrother),
            new BlockingRule(RelationType.Son, RelationType.FullSister),
            // ... more rules
        ];
    }
}

Look at what we did here. The rules are data, not code. This is the Prolog way - facts and rules are just declarations.

Now the logic becomes simple:

public List<RelationType> GetBlockedHeirs(InheritanceCase inheritanceCase)
{
    var blocked = new HashSet<RelationType>();
    
    foreach (var rule in rules)
        if (inheritanceCase.HasHeir(rule.Blocker))
            blocked.Add(rule.Blocked);
    
    // Handle conditional blocking...
    
    return blocked.ToList();
}

Why This Approach Wins

  1. Readable: Islamic scholars can review the rules without understanding algorithms
  2. Maintainable: Adding a new blocking rule is one line of data
  3. Testable: Each rule can be tested independently
  4. Auditable: The rules are visible, not hidden in nested if-statements

Compare this to the naive approach. Which one would you rather maintain? Which one would you rather explain to a domain expert?

The Conditional Rules: Where It Gets Interesting

Not all blocking rules are simple “A blocks B” relationships. Some are conditional:

// Sons block grandsons AND granddaughters
if (inheritanceCase.HasHeir(RelationType.Son))
{
    blocked.Add(RelationType.SonOfSon);
    blocked.Add(RelationType.DaughterOfSon);
}

// Two or more daughters block granddaughters (but not grandsons!)
if (!inheritanceCase.HasHeir(RelationType.Son) && 
    inheritanceCase.GetHeirCount(RelationType.Daughter) >= 2)
{
    blocked.Add(RelationType.DaughterOfSon);
}

// Male descendants OR ascendants block full siblings
if (inheritanceCase.DeceasedHasMaleDescendantsOrAscendants())
{
    blocked.Add(RelationType.FullBrother);
    blocked.Add(RelationType.FullSister);
}

These conditional rules are where our extension methods shine. Instead of writing:

if (inheritanceCase.HasHeir(RelationType.Son) || 
    inheritanceCase.HasHeir(RelationType.SonOfSon) ||
    inheritanceCase.HasHeir(RelationType.Father) ||
    inheritanceCase.HasHeir(RelationType.Grandfather))

We write:

if (inheritanceCase.DeceasedHasMaleDescendantsOrAscendants())

The code reads like the Islamic jurisprudence text it implements.

Lessons from Logic Programming

What did we learn from Prolog’s approach?

1. Separate Facts from Logic

  • Facts = immutable data (heirs, relationships)
  • Logic = pure functions over that data (blocking rules, share calculations)

2. Make Rules Visible

When rules are buried in nested if-statements, they’re hard to verify. When they’re declared as data or simple functions, domain experts can review them.

3. Compose Complex Logic from Simple Rules

DeceasedHasMaleDescendantsOrAscendants() =>
    DeceasedHasMaleDescendants() || DeceasedHasMaleAscendants()

Each function does one thing. Complex logic emerges from composition.

4. Let the Type System Help

C#’s enums and type system prevent invalid states:

new BlockingRule(RelationType.Son, RelationType.Grandfather)

You can’t accidentally create a blocking rule with a typo or invalid heir type.

Why This Matters for Java and Python Developers

If you’re coming from Java, you might be thinking: “I’d use a rules engine like Drools for this.”

But look at our solution: under 100 lines of code for the entire blocking engine. No external dependencies, no DSL to learn, no rule compilation step. Sometimes the simplest solution is the best solution.

If you’re coming from Python, you might be thinking: “I’d use dictionaries and functions.”

And you’d be right! Python’s functional features (filter, map, list comprehensions) can absolutely implement this pattern. The main difference is C#’s type system catches errors at compile time that Python would only catch at runtime.

The Hybrid Approach

What we’ve built is a hybrid:

  • OOP for domain modeling (Heir hierarchy, type safety)
  • Functional for business logic (extension methods, LINQ, pure functions)
  • Declarative for rules (blocking rules as data)
  • Prolog-inspired for composition (rules build on rules)

This isn’t pure functional programming, and it’s not pure OOP. It’s pragmatic engineering that borrows the best ideas from multiple paradigms.

What’s Next

In our next post, we’ll dive deeper into the share calculation engine - the mathematical heart of the system. We’ll explore how we handle the infamous Awl problem (when shares exceed 100%), residuary distribution, and the complex conditional logic that determines each heir’s share.

But the foundation we’ve built with this declarative, rule-based approach makes that complexity manageable. When your code mirrors the domain, both become easier to understand.


The best ideas in programming aren’t always the newest ones.


Miraas is available at https://miraas.azurewebsites.net - but remember, it’s a tool to assist understanding, not replace scholarly consultation.

More from Miraas Inheritance Calculator