
# Bodhas
Dimensions of Pāṇinian Grammar Explored, Part 3 - Object Oriented Programming View of Pāṇinian Grammar
20 March, 2024
4166 words
share this article
This series of articles is dedicated to Pāṇinian grammar of the Saṃskṛta language, as presented in his treatise “Aṣṭādhyāyī”. In trying to characterize the grammar of the Saṃskṛta language spoken in his day, Pāṇini Maharṣi manufactures legitimate and only legitimate words of the Saṃskṛta language, by designing a set of 4000 rules in a highly compressed language of the “sūtra” that operates on the sounds, verb roots, prefixes and suffixes, to generate all the words of Saṃskṛta language.
In this process, Pāṇini uses techniques from modern computing (especially the theory of formal languages that are used in analysis and compiler design for programming languages) and anticipates them even though it would be 2500 years from his time when the first actual computer would be invented. Techniques like optimal ordering of data, information compression, metalanguage, metarules, use of non-terminal symbols, light before heavy are all used in this masterpiece.
To study the Aṣṭādhyāyī, it is essential to know the Saṃskṛta language, as that is the language for which the grammar is designed. However in this series, my aim is to introduce how Pāṇini uses these techniques in his treatise for people who don’t have any knowledge of the Saṃskṛta language, or formal language theory in computer science. I also present the rich developments in the philosophy of language that Pāṇini’s work inspired in his successors and compare it with Western philosophies of language like that of Russell, Wittgenstein, de Saussure, etc.
Language was an important concern in ancient Indian philosophy, especially for the Āstika schools, as they believed that the efficacy of the Vedic rituals directly depended on the sounds of the chanted mantras. They wanted to justify how this efficacy is brought about, and in the process, they had to analyze how the sounds of a language relate to meaning and ultimately reality itself. By the end of this series, you will have a deep appreciation of the importance, depth and scope of Aṣṭādhyāyī and the Indian views on language.
Read Part 1 of the series here and Part 2 of the series here.
Introduction to Programming for Dummies Like Me
If you are a computer scientist and programming is your day job, you can safely skip this and the next section. I am not a computer scientist either - I am so bad at coding. But what I am going to say is more about problem solving than it is about the exact coding. Hence, it is okay if you do not understand all the exact details in your head. Read through this section where I introduce in as simple terms as possible, the various concepts of object oriented programming and its uses. In the next section, I will connect all these concepts to Pāṇinian grammar and how he brilliantly uses them in his Aṣṭādhyāyī.
Let us consider a very simple problem - to check whether a given positive integer is even or odd. This is how a program for that will look like in the C++ programming language - don’t be scared if you did not study computer science (I also did not), but just watch it and I will explain it step by step.
include <iostream>
using namespace std;
int main() {
int n;
cout << "Enter an integer: ";
cin >> n;
if ( n % 2 == 0)
cout << n << " is even.";
else
cout >> m >> " is odd.";
}
Now, let me explain this program. The first two lines are routine stuff that need to be typed for every program. The core part starts with the main block. Let us focus between the int main { }
thing.
int n;
The symbol “int” here is an abbreviation for integer and the line int n; defines that a variable named n is an integer. This is the number that the user is going to enter and have it checked for even or odd. So this line defines the name and nature of the data that the user is going to enter - which is an integer named “n”. So, this line in the code is not doing anything but only defining things that are going to be involved in the task that is going to be solved by an algorithm. The line:
cout << "Enter an integer: ";
is an output that comes from the computer to the user. The computer here is being asked or programmed to give an output that prompts the user to enter an integer. Upon seeing this message, the user will enter the positive integer. Now, coming to the next line.
cin >> n;
After the user has entered the positive integer, this line in the code is instructing the computer to take the data he entered as the positive integer “n” that he wants to be checked as even or odd. Now, coming to the core task. Look at the following lines
if ( n % 2 == 0)
cout << n << " is even.";
else
cout << n << " is odd.";
Here, the symbol “%” is the remainder operator - i.e. “a%b” is the remainder obtained by dividing “a” by “b”. So, n%2 will give the remainder that is obtained when the integer n is divided by 2. Now when any integer is divided by 2, it is either 0 or 1. The line above tells us that if the remainder obtained in the operation n%2 is 0, then output to the user that the number n is even. Or else, output and prompt the user that the number n is odd. This is a very simple problem. We all know that a number is even if and only if the remainder obtained after dividing it by 2 is zero and odd otherwise. But there are technical names for certain things that are seen in this code.
Reserved keyword:The abbreviation int that stands for a positive integer is called a keyword in a computer program. This abbreviation cannot be used to name any other variable and is reserved and defined by the programming language system itself - you cannot name any other entity in your program by these abbreviations like int that are already reserved by the programming language (in this case C++) for something else.
Standard function:The symbol % is also reserved by the system for a specific purpose - from what I have told you, it is the remainder operation. Such standard operations that are defined already in the programming languages are called standard functions. The % operator takes two positive integers, divides the first one by the second and outputs the reminder of the result obtained. Again, this remainder function with its symbol % is already reserved by the system. The system already knows everything that this % operator does. It knows
- What it takes in as its input - in this case, two positive integers
- What it does to the input - in this case, compute the remainder of the first divided by second
- What it returns or gives back - in this case, return the remainder obtained
Now, let us execute the same task in a slightly different way - using the concept of user-defined functions. Read the code below. Here, in the main part of the code, I have called upon a strange operation that I have created on my own - it is called evenoddcheck. It takes an integer, divides it by 2 and returns the remainder. Using this I rewrite the code as follows:
include <iostream>
using namespace std;
int evenoddcheck(int n);
int main() {
int n;
cout << "Enter an integer: ";
cin >> n;
if(evenoddcheck(n)==0)
cout << n << " is even.";
else
cout << n << " is odd.";
}
But how will the computer know what this strange operation evenoddcheck stands for? It turns out that after the program, I need to type what the function does and define it completely. That definition follows outside of the main bracket int main {....}
and is given below.
int evenoddcheck(int n) {
int result;
if(n%2==0)
result=0;
else
result=1;
return result;
}
Here, let me explain the function definition step by step. The first line of the code is
int evenoddcheck(int n) { ... }
Here, the part evenoddcheck is the name I assign to the function. In the bracket that has (int n), I tell that this function has to take an integer named n as the input. In the first word int, I tell that the output of the function is also another integer - in this case, the remainder obtained when the integer n is divided by 2. The rest part inside is the standard computation of the remainder.
Now what difference is achieved? By rewriting the remainder by 2 computations separately. The thing is that I can use this function readily whenever I want in the main program instead of typing it all again. It may not mean much for simple operations as in here but a lot for more complex operations. I can define all the complicated operations that I am going to use separately and my main program will be clean and call readily just what I want.
Introduction to Object Oriented Programming for Dummies Like Me
In this section, I will take a little step further and introduce object oriented programming concepts. Again, this can be safely skipped for whom it is a day job. In this section, I am going to introduce a few more concepts that may sound sophisticated but are in essence simple to understand. They are the following - class, object and inheritance.
Class & ObjectThe standard types of data that computers are trained to manipulate are routine objects like integers or strings or arrays. When we deal with more complicated data types, one needs to come up with the notion of a class. A class is simply a blueprint for a combination of a novel data type and a set of instructions one is interested in. For example, I can define the class for driving a car named CAR_CONTROLS with the following attributes - fuel, speed that also has some operations associated with it - accelerating and driving. All these characterize a car control system (there are lots of others too but for now, let us stick to only these). For example,
class CAR_CONTROLS {
float fuel_level
float speed
float driving(float accelerator) {...}
float braking(float brake_position) {...}
}
Here, the class CAR_CONTROLS has two data associated with it - fuel_level and speed. And we have two operations - driving and braking that take as their inputs, accelerator and brake position and modify the speed accordingly in the {...}
section. So, this is what the concept of a class is - a template for a combination of a data set and instructions.
Now, this class is a template for creating so many actual cars having some values for these numbers specified. A specific car may have a fuel_level of 0.5 in liters. An object is an instance of a class.
InheritanceA very important concept in object oriented programming is the notion of inheritance. Consider two classes - the class HUMAN and the class STUDENT. We see that all students are humans and hence we would like the class STUDENT to inherit the data and instructions in the class HUMAN. This phenomenon is called inheritance. Of course the class STUDENT could contain more specific information like name of the school that is not present in the generic class HUMAN but we all agree that it should logically get the properties of the class HUMAN. This inheritance saves lots of programming time and space since one need not keep on mentioning the repeating set of features in all the inherited classes from a given parent class.
Consider the even more generic class ANIMAL. And now the class HUMAN which is an inherited class from ANIMAL. Now consider the instruction MOVEMENT. In the generic class ANIMAL, the instruction MOVEMENT would be something like: position four limbs vertically upright and touching the ground and then actuate to move. But we know that humans, while being an inherited class from ANIMAL do the movement differently - we walk erect with just two of our backlimbs (called legs) positioned vertically and touching the ground and we reserve our forelimbs (called hands) for novel tasks like tool making and grasping. So, consider the scenario:
class ANIMAL {
movement {..walk using four limbs..}
}
For other classes of animals like DOG, CAT, etc this function “movement” using four limbs can be inherited directly. But it cannot be done so for humans. So, for the class HUMAN, we must have
class HUMAN: ANIMAL {
movement {..walk using two back limbs..}
}
Now, let us say that I create an object from the class HUMAN and call the function “movement”. What should be implemented? It turns out that instructions in the inherited class overrides the instructions in the parent classes. This phenomenon is called function overriding in object oriented programming. Its formal definition is given below.
Function OverridingFunction overriding is a concept in object-oriented programming which allows a function within an inherited class to override a function in its base class. This is very important and saves lots of programming time - if you have a general instruction that is applicable by default but some special set of instruction to override it for special cases, then function overriding ensures that by simply rewriting the function for the special case, the special instruction is implemented for the special case and the general instruction is ignored. The following picture summarizes inheritance and overriding.

How about a class that inherits from two or more base classes? After all, biologically, a child inherits from two parents. This phenomenon is called multiple inheritance. For example, the class INDIAN_BOY needs to inherit both from the class INDIAN and the class BOY. But what if there are two instructions of the same name but doing different things in the two parent classes? If both the parent classes contain different implementations of a function named X, then when the subclass inherits it, from what parent class will it implement X? If the inherited class has another instruction for X, then it turns out that by overriding, it overrides both of the instructions of X in the parent classes. Otherwise, there should be an explicit redefinition of X in the inherited class that tells us from which parent class, the function X is inherited in the inherited class. A picture illustrating it is given below.

The Programming Genius of Pāṇini
You might ask again what the above things have to do with Saṃskṛta? In this section, let us see it. In Saṃskṛta, to form the present tense of most verbs, one adds an extra अ before the personal endings. I give some examples:
- Root पठ् (to read) => पठ् + अ = पठ => पठ + ति => पठति (he/she/it reads)
- Root पत् (to fall) => पत् + अ = पत => पत + ति => पतति (he/she/it falls)
- Root लिख् (to write) => लिख् + अ = लिख => लिख + ति => लिखति (he/she/it writes)
- Root रक्ष् (to guard) => रक्ष् + अ = रक्ष => रक्ष + ति => रक्षति (he/she/it guards)
This is the case with most of the verbs. About 80% of the verb roots get an extra अ vowel before the personal endings are attached in the present and some other related tenses. But there are some classes of verbs that refuse to adhere to this rule or do something weird. Some examples are:
- Root हन् (to kill) => हन् + ति => हन्ति (he/she/it kills)
- Root अस् (to be) => अस् + ति => अस्ति (he/she/it is)
So, we again have a general rule applicable to all verb roots but a special exception for only some small class of roots. Does this remind you of inheritance? Note that there are many other rules in action as well and the picture is more complicated - I am simplifying it and am taking only one aspect to demonstrate in a simple fashion.
Pāṇini as usual solves this by using inheritance. He defines a class of ROOTS in general and divides them into ten subclasses - CLASS_1 , CLASS_2, CLASS_3, …CLASS_10. He generally gives an instruction to add the extra vowel अ to all roots by giving it in the parent class ROOTS but redefines that instruction in certain irregular classes and overrides them in the irregular classes.
This is a general rule in Pāṇinian grammar:
Whenever there are two rules competing to be applied on the same object, with one rule for a generic case (applicability in a larger domain) and the other for a particular case (applicability in a smaller domain), the smaller domain is viewed as an inheritance of the larger domain since it it is a part of it and hence the rule for the smaller domain overrides the rule for the larger domain.
Let us now see an example of the multiple inheritance case. Consider the three sets of Sandhi rules below.
- इ/उ/ऋ/ऌ + vowel = य्/व् /र् /ल् + vowel (Part 1 - इकः यण् अचि)
- अ + vowel = guna (medium grade of the vowel)
- Vowel + Vowel (same pair) = Long Vowel
I give various examples of the three rules:
- प्रति + उषा = प्रत्युषा, सु + आगतम् = स्वागतम्, मातृ + अंशः = मात्रांशः
- राज + इन्द्रः = राजेद्रः, मह + उत्सवः = महोत्सवः, सप्त + ऋषिः = सप्तर्षिः
- ब्रह्म + अस्त्रम् = ब्रह्मास्त्रम्, योगि + इन्द्रः = योगीन्दरः, गुरु + उत्तमः = गुरूत्तमः
We see that rule 3 conflicts with rules 1 and 2. In the examples of rule 3, ब्रह्म + अस्त्रम् where in sandhi, an अ+अ is encountered, I can also apply rule 2 because it falls under the domain of अ+ any vowel . In the same rule 3 example of योगि + इन्द्रः where in sandhi, an इ+इ is encountered, I can also apply Rule 1 because it falls under the domain of इ + any vowel. So, how does Pāṇini ensure that it is only Rule 3 that applies in all those examples and not rules 1 and 2. The concept of overriding for the case of multiple inheritance comes into play. Consider the possible pairs of vowels visualized below. The like vowel combinations come at the diagonal of the table.
अ | इ | उ | ऋ | |
अ | अ+अ | अ+इ | अ+उ | अ+ऋ |
इ | इ+अ | इ+इ | इ+उ | इ+ऋ |
उ | उ+अ | उ+इ | उ+उ | उ+ऋ |
ऋ | ऋ+अ | ऋ+इ | ऋ+उ | ऋ+ऋ |
Now, the information of whether the combination is under the applicability of rule 1 (इ,उ,ऋ+vowel) or rule 2 (अ+vowel) or rule 3 (like vowels) is also given.
अ | इ | उ | ऋ | |
अ | Rule 2,3 | Rule 2 | Rule 2 | Rule 2 |
इ | Rule 1 | Rule 1,3 | Rule 1 | Rule 1 |
उ | Rule 1 | Rule 1 | Rule 1,3 | Rule 1 |
ऋ | Rule 1 | Rule 1 | Rule 1 | Rule 1,3 |
One sees that the domain of Rule 3 is contained in the combinations of the domains of rules 1 and 2. This is visualized below.

So, Pāṇini does not have to qualify anything more - the overriding principle automatically overrides applications of rules 1 and 2 whenever rule 3 also can be applied. So, rules 1 and 2 will be applied only when rule 3 cannot be applied. This is because the rules 1 and 2 are more general than rule 3 which is more specific and always an instruction for a specific class is followed whenever it clashes with an instruction from a generic case.
Recursion and Production of More Objects
We saw that classes are templates for objects which contain information about data and instructions (functions). And we saw that functions like evenoddcheck act on data and give out new data (take two numbers and return remainder). But wait, can a function throw out new objects, thus adding to a database? This is possible in object oriented programming - one can have functions inside classes (that act inside all the objects of those classes) and one can arrange these functions to return new objects of the existing classes with its own new functions. I do not want to write a code again for this but the idea should be clear.
Pāṇini does this too - he utillises important aspects of Saṃskṛta language. Let us take one simple application - the desideratives or सनाद्यन्तः. Consider the following two sentences in English:
- I read books.
- I want to read books.
In the first sentence, the verb is factual - the reading is a matter of fact. Whereas in the second sentence, the verbal part “want to read” is not factual - it merely conveys desire or intention. English conveys this desire by using the verb “want” and then putting a “to” in front of the verb that is desired. But Saṃskṛta modifies the verb itself to achieve this meaning. This mood of desire or intention is called the desiderative mood in grammar. See in Saṃskṛta.
- सः पुस्तकानि पठति (He reads books)
- सः पुस्तकानि पिपठिषति (He wants to read books)
So, we see that the verb transforms internally from पठति to पिपठिषति. What is happening are two crucial steps.
- Reduplication of the first consonant of the initial consonant part of the root with some vowel - i.e. पठ् => (the initial consonant प् in पठ् reduplicates with इ) पि + पठ् = पिपठ्
- Then, an additional स् /इष् is added to the reduplicated root - पिपठ् + इष् = पिपठिष्
Then, as usual we add the usual thematic vowel अ and the personal ending ति to get the final verb.
- पिपठिष् + अ + ति = पिपठिषति
How Pāṇini thinks of this process is that to convey desire, one manufactures a new root पिपठिष् from the ordinary verb पठ् through reduplication and addition of इष्. These roots are called secondary roots that convey desiderative grade.
Pāṇini uses a special function that acts on all roots to create new roots. If we consider roots as objects like he did, then what he is doing is coming up with a function that takes in a root and gives out another root. The technical name that he gives for this function in his grammar is सन्. When the suffix सन् is added to a root, it triggers appropriate reduplication and additions to give up another root related to it and conveying the meaning of desire.
To translate from the language of object oriented programming to Pāṇinian grammatical terms,
Pāṇini | Programming |
Optimal Ordering of the data of roots, suffixes and sounds | Database management |
Definitions संज्ञा of technical terms (eg. the terms गुण ,वृद्धि in vowel grades) | Reserved keywords (eg. int, float) |
Roots, Suffixes, and various groupings and subgroupings within them like तद्धित suffixes or class 1 (भ्वादि) roots | Classes and inherited subclasses |
The actual data of the roots and suffixes | Objects |
विधि - rules | Standard Function like % |
Instructions for combinations and transformations of data | Functions defined inside the appropriate class that rewrite and give back a new object |
A rule with a smaller domain overriding a rule or rules with a larger domain containing the smaller domain | Function overriding via inheritance |
Thus, we have seen how Pāṇini anticipates modern object oriented programming techniques through his masterpiece. This is just a scratch on the surface but I hope I have whetted your appetite enough to explore more.