CSC 414-514 Notes

Download as pdf or txt
Download as pdf or txt
You are on page 1of 21

CSC 514: ORGANIZATION OF PROGRAMMING LANGUAGES

COURSE CONTENTS

1. Introduction to Programming Languages:


- Overview of programming language concepts and their importance.
Programming languages serve as the foundation for developing software applications, enabling
communication between humans and computers. These languages are designed with specific syntax and
semantics, providing a set of rules that govern how code is written and executed. Understanding
programming language concepts is crucial for software developers, as it forms the basis for creating
efficient, readable, and maintainable code. An overview of key programming language concepts and
their importance include:

Syntax:
Definition: Syntax refers to the set of rules that dictate how programs should be written in a particular
language.
Importance: Proper syntax ensures that code is written in a way that the compiler or interpreter can
understand. It contributes to code readability and helps prevent errors.
Semantics:

Definition: Semantics deals with the meaning of code and how statements are executed.
Importance: Understanding semantics is essential for writing code that behaves as intended. It involves
grasping concepts like variables, data types, and control flow to create logic and functionality.
Data Types:

Definition: Data types define the kind of data that can be stored and manipulated in a program, such as
integers, floating-point numbers, strings, and more.
Importance: Proper use of data types ensures data integrity and efficient memory usage. It also
influences the operations that can be performed on the data.

Variables:

Definition: Variables are symbolic names for storage locations in a program where values can be stored
and retrieved.
Importance: Variables allow developers to manage and manipulate data dynamically during program
execution. Understanding variable scope and lifetime is crucial for effective programming.

Control Structures:

Definition: Control structures, including loops and conditional statements, manage the flow of execution
in a program.
Importance: These structures determine the order in which statements are executed. Well-designed
control flow enhances code readability and helps in building complex algorithms.

Functions/Methods:

Definition: Functions or methods are blocks of reusable code that perform a specific task.
Importance: Modularizing code through functions promotes code reusability, readability, and
maintainability. It also facilitates the division of complex tasks into manageable units.
Object-Oriented Programming (OOP) Concepts:

Definition: OOP is a programming paradigm that uses objects, encapsulation, inheritance, and
polymorphism to organize code.
Importance: OOP fosters code organization, abstraction, and reuse. It enhances software design by
modeling real-world entities and their relationships.

Memory Management:

Definition: Memory management involves allocating and deallocating memory during program
execution.
Importance: Efficient memory management prevents memory leaks and enhances program
performance. Understanding concepts like manual memory allocation and garbage collection is crucial.

Error Handling:

Definition: Error handling involves dealing with unexpected situations and errors that may occur during
program execution.
Importance: Robust error handling improves the reliability of software. It includes techniques like
exception handling to gracefully manage errors without crashing the program.

Concurrency and Parallelism:

Definition: These concepts deal with executing multiple tasks simultaneously, either concurrently or in
parallel.
Importance: Understanding concurrency and parallelism is crucial for developing efficient and
responsive software, particularly in applications that require multitasking or utilize multiple cores.

- Historical context of programming languages.

2. Syntax and Semantics:


- Syntax definition and parsing techniques.
1. Syntax Definition:

Syntax refers to the set of rules governing the structure of sentences or expressions in a language. In the
context of computer science and programming languages, syntax definition plays a crucial role in
specifying the valid combinations of symbols that constitute well-formed programs. It involves defining
the grammar of a language, outlining how statements and expressions should be structured.

Components of Syntax Definition:

Lexical Elements: Define the basic building blocks such as identifiers, keywords, operators, and literals.

Grammar Rules: Specify how lexical elements can be combined to form valid statements or expressions.
Context-free grammars (CFGs) are commonly used for this purpose.
Syntax Diagrams or BNF Notation: Illustrate the grammar rules using diagrams or Backus-Naur Form
(BNF) notation, providing a visual representation of the language's structure.

2. Parsing Techniques:

Parsing is the process of analyzing a sequence of symbols to determine its grammatical structure with
respect to a given grammar. It is a crucial step in the compilation process of programming languages.
Various parsing techniques are employed to achieve this goal:

a. Top-Down Parsing:

Recursive Descent Parsing: In this approach, the parser starts with the top-level grammar rule and
recursively expands non-terminals until the input is recognized. Each non-terminal corresponds to a
specific syntactic construct.

LL Parsing: LL parsers predict the production rule to be applied based on the next input symbol. LL(k)
parsers use k lookahead symbols to make parsing decisions.

b. Bottom-Up Parsing:

LR Parsing: LR parsers, such as LR(1) or LALR(1), build a parse tree in a bottom-up manner, starting with
individual tokens and reducing them to higher-level constructs. LR parsers handle a broader class of
grammars compared to LL parsers.

Shift-Reduce Parsing: In shift-reduce parsing, the parser repeatedly shifts input symbols onto a stack or
reduces them using production rules until the entire input is reduced to the start symbol.

parsing examples using a simple context-free grammar and two common parsing techniques: Recursive
Descent Parsing and LR Parsing.

Example Grammar:

Consider the following simple arithmetic expression grammar:

E→E+T|T
T→T*F|F
F → ( E ) | id
This grammar describes expressions involving addition (+), multiplication (*), parentheses, and
identifiers (id).

1. Recursive Descent Parsing:

Recursive Descent Parsing involves constructing a set of recursive procedures to match the grammar
rules. Let's create a recursive descent parser for the given grammar in Python:

python
def parse_E(tokens):
if tokens[0] == 'id':
return parse_T(tokens[1:])
else:
return parse_T(tokens)

def parse_T(tokens):
if tokens[0] == 'id':
return parse_F(tokens[1:])
else:
return parse_F(tokens)

def parse_F(tokens):
if tokens[0] == '(':
return parse_E(tokens[1:])
elif tokens[0] == 'id':
return tokens[1:]
else:
raise SyntaxError("Invalid expression")

# Example usage:
expression = ["id", "+", "id", "*", "id"]
result = parse_E(expression)
print(result)
This recursive descent parser recursively calls functions corresponding to grammar rules to parse the
input expression.

2. LR Parsing:

Let's use an LR parser to parse the same grammar. We'll use a tool like Yacc (or Bison) to generate the
parser: Yacc stands for "Yet Another Compiler Compiler." It is a tool used in the construction of
compilers and interpreters for programming languages. Yacc is a code generator that generates parsers
based on formal grammars specified in a specific syntax. Yacc is often used in combination with Lex (a
lexical analyzer generator) to create a complete language processing system.

The Yacc tool reads a formal grammar description and generates a parser in C (or other target
languages) that can recognize and parse syntactic structures defined by the grammar. The generated
parser typically produces a parse tree or an abstract syntax tree, which can be further processed to
execute or translate the input language.

yacc
%token id PLUS TIMES LPAREN RPAREN

%%

expression: E
E: E PLUS T | T
T: T TIMES F | F
F: LPAREN E RPAREN | id
This Yacc specification defines tokens (id, PLUS, TIMES, LPAREN, RPAREN) and production rules for the
grammar. The generated parser uses LR parsing.

Parsing Process:

Suppose we have the input expression: id + id * id.

The LR parser would perform the following steps:

Shift id: F reduces to id.


Shift +: T reduces to F, and E reduces to T + F.
Shift id: F reduces to id.
Shift *: T reduces to F, and E reduces to E + T. Now, E is E + T * F.
Shift id: F reduces to id.
Reduce T * F: T becomes T * F.
Reduce E + T: E becomes E + T.
Done: The entire expression is parsed.
These examples illustrate how recursive descent and LR parsing can be applied to a simple grammar for
parsing arithmetic expressions. The actual parsing process involves traversing the grammar rules and
reducing them to recognize the input structure.

- Semantics and the distinction between static and dynamic semantics.

3. Language Translation:
- Compilers vs. interpreters.
- Lexical analysis and parsing.
- Intermediate code generation.

4. Language Implementation:
- Memory management and allocation.
- Symbol tables and symbol management.
- Scope and lifetime of variables.

5. Data Types:
Programming languages provide a range of data types to represent different kinds of information. The
organization of data types plays a crucial role in defining how data is stored, manipulated, and operated
upon in a program.

- Primitive data types (integers, floating-point, characters).


Integers: Integers represent whole numbers without any fractional part.
Example (C programming language):
int score = 95;

Floating-point: Floating-point types represent numbers with a fractional part.


Example (Python):
python
height = 5.9
Characters: Characters represent individual symbols, letters, or digits.
Example (Java):
char grade = 'A';

- Composite data types (arrays, records, unions).


Arrays: Arrays are collections of elements of the same data type, accessed by an index or key.
Example (C++):
int numbers[5] = {1, 2, 3, 4, 5};

Records (Structures):
Records or structures allow grouping different data types under a single name.
Example (C#):
struct Person {
string name;
int age;
}

Unions:
Unions allow a variable to hold different data types at different times.
Example (C):
union Data {
int i;
float f;
char c;
};

- User-defined data types.


Enumerations:
Enumerations allow defining a set of named integer constants.
Example (Python):
python
class Day(Enum):
MONDAY = 1
TUESDAY = 2

# ...
Classes (Object-Oriented Programming): Classes define blueprints for creating objects with attributes
(data) and methods (functions).
Example (Java):
class Dog {
String breed;
int age;
void bark() {
System.out.println("Woof!");
}
}
Abstract Data Types (ADTs): ADTs represent a mathematical model of a data structure along with a set
of operations to manipulate it.
Example (C++ - Standard Template Library):
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
}

Pointers: Pointers store memory addresses, allowing dynamic memory allocation and manipulation.
Example (C):
int* ptr = malloc(sizeof(int));
*ptr = 10;

- Conditional statements and expressions.


Conditional Statements:
If Statement: The if statement allows you to execute a block of code if a given condition is true.
Example (Python):
x = 10
if x > 0:
print("Positive number")

If-Else Statement: The if-else statement allows you to execute one block of code if the condition is true
and another block if it is false.
Example (JavaScript):
let temperature = 25;
if (temperature > 30) {
console.log("It's hot!");
} else {
console.log("It's not too hot.");
}

If-Elif-Else Statement: The if-elif-else statement allows you to check multiple conditions in sequence.
Example (C++):
int score = 85;
if (score >= 90) {
cout << "A";
} else if (score >= 80) {
out << "B";
} else {
cout << "C";
}

Conditional Expressions (Ternary Operators):


Ternary Operator (Conditional Expression): The ternary operator provides a concise way to write an if-
else statement in a single line.
Example (Java):
int age = 20;
String result = (age >= 18) ? "Adult" : "Minor";

Nested Ternary Operators: Ternary operators can be nested to handle more complex conditions.
Example (Python):
num = 7
classification = "Even" if num % 2 == 0 else ("Odd" if num % 2 != 0 else "Invalid")

Detailed Examples:
Example 1: Python If-Else Statement
# Check if a number is positive or negative
num = -8
if num > 0:
print("Positive number")
else:
print("Negative or zero")

Example 2: JavaScript Nested Ternary Operators


// Determine the type of a variable
let variable = "Hello";
let type = typeof variable === "string"
? "It's a string"
: (typeof variable === "number" ? "It's a number" : "It's neither a string nor a number");
console.log(type);

Example 3: C++ If-Elif Statement


// Classify the weather based on temperature
int temperature = 28;
if (temperature > 30) {
cout << "Hot";
} else if (temperature > 20) {
cout << "Moderate";
} else {
cout << "Cold";
}

- Looping and iteration.


Looping Constructs:

For Loop: Definition: The for loop is used for iterating over a sequence (such as a range of numbers) and
executing a block of code for each iteration.
Example (Python):
for i in range(5):
print(i)

While Loop: Definition: The while loop repeatedly executes a block of code as long as a specified
condition is true.
Example (JavaScript):
let i = 0;
while (i < 5) {
console.log(i);
i++;
}

Foreach Loop: The foreach loop (or for-each loop) is used for iterating over elements in a collection,
such as an array.
Example (Java):
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
System.out.println(num);
}

Detailed Examples:
Example 1: Python For Loop
# Print square of numbers from 1 to 5
for i in range(1, 6):
square = i ** 2
print(f"The square of {i} is {square}")

Example 2: JavaScript While Loop


// Generate random numbers until one is greater than 0.8
let randomNum;
while (true) {
randomNum = Math.random();
console.log(randomNum);
if (randomNum > 0.8) {
break;
}
}

Example 3: Java Foreach Loop


// Calculate the sum of elements in an array
int[] numbers = {10, 20, 30, 40, 50};
int sum = 0;
for (int num : numbers) {
sum += num;
}
System.out.println("Sum: " + sum);

Loop Control Statements:


Break Statement:The break statement is used to exit a loop prematurely.
Example (C++):
for (int i = 0; i < 10; ++i) {
if (i == 5) {
break;
}
cout << i << " ";
}

Continue Statement: The continue statement is used to skip the rest of the code inside a loop for the
current iteration.
Example (Python):
for i in range(10):
if i % 2 == 0:
continue
print(i)

Nested Loops:
Nested For Loops: Nested loops are loops inside another loop. They are used for tasks that involve
multiple levels of iteration.
Example (C#):
for (int i = 1; i <= 3; ++i) {
for (int j = 1; j <= 3; ++j) {
Console.WriteLine($"({i}, {j})");
}
}

Nested While Loops: Similar to nested for loops, while loops can also be nested to achieve complex
iteration patterns.
Example (JavaScript):
let i = 1;
while (i <= 3) {
let j = 1;
while (j <= 3) {
console.log(`(${i}, ${j})`);
j++;
}
i++;
}

- Subroutines and function calls.


Function Definition: A function is a self-contained block of code that performs a specific task. It typically
takes inputs (parameters), processes them, and produces an output (return value).
Example (Python):
def add_numbers(a, b):
return a + b

Subroutine: The term "subroutine" is a generalization that includes both functions and procedures.
Procedures are subroutines that perform a task without returning a value.
Example (C):
void print_message() {
printf("Hello, subroutine!\n");
}
Function Calls:
Function Call: A function call invokes the execution of a specific function with given arguments. The
result (return value) can be stored or used in further computations.
Example (JavaScript):
let result = addNumbers(3, 4);
console.log(result); // Output: 7

Function Parameters: Functions can accept parameters, which are values passed to the function when it
is called. These parameters act as inputs for the function.
Example (Java):
int multiply(int x, int y) {
return x * y;
}

Default Parameters: Some languages allow functions to have default parameter values, providing a
fallback if a value is not explicitly provided during the function call.
Example (Python):
def greet(name="Guest"):
return f"Hello, {name}!"

Subroutine Calls: A subroutine call (procedure call) invokes the execution of a specific subroutine. Unlike
functions, procedures typically do not return a value.
Example (Fortran):
CALL PrintMessage()

Passing by Reference: Subroutines can modify the values of variables passed to them by reference,
allowing for side effects.
void modifyValue(int &x) {
x *= 2;
}

Detailed Examples:
Example 1: Python Function Call
# Calculate the area of a rectangle using a function
def calculate_area(length, width):
return length * width

# Function call
area = calculate_area(5, 8)
print("Area:", area)

Example 2: C Procedure Call


#include <stdio.h>

// Procedure to print a message


void printMessage() {
printf("Hello, subroutine!\n");
}
int main() {
// Subroutine call
printMessage();
return 0;
}

Example 3: JavaScript Function Call with Default Parameter


// Function to generate a greeting message
function greet(name = "Guest") {
return `Hello, ${name}!`;
}

// Function call
let message = greet("John");
console.log(message);

7. Parameter Passing:
- Call by value, call by reference, and call by sharing.
Parameter passing defines how arguments are transferred to functions or methods. The three common
methods of parameter passing are Call by Value, Call by Reference, and Call by Sharing.

1. Call by Value:
In Call by Value, the actual value of the argument is passed to the function. This means that the function
receives a copy of the argument, and any modifications made to the parameter inside the function do
not affect the original value outside the function. It is a simple and safe method, commonly used in
languages like C and Python for primitive data types.
def square(number):
number = number ** 2
return number

x=5
result = square(x)
print(x) # Output: 5
print(result) # Output: 25

2. Call by Reference:
In Call by Reference, the memory address (reference) of the argument is passed to the function. This
allows the function to directly access and modify the original data. Call by Reference is often found in
languages like C++ and can lead to unintended side effects if not used carefully.
#include <iostream>
void increment(int &value) {
value++;
}

int main() {
int x = 5;
increment(x);
cout << x << endl; // Output: 6
return 0;
}

- Recursive function calls.


Recursive functions are functions that call themselves either directly or indirectly to solve a smaller
instance of the same problem. This programming technique is particularly useful for solving problems
that can be broken down into simpler subproblems. Recursive functions typically consist of a base case,
which defines the termination condition, and a recursive case, where the function calls itself.
Examples:
1. Factorial Calculation
def factorial(n):
# Base case
if n == 0 or n == 1:
return 1
# Recursive case
else:
return n * factorial(n - 1)

result = factorial(5)
print(result) # Output: 120

2. Fibonacci Sequence:
def fibonacci(n):
# Base case
if n <= 1:
return n
# Recursive case
else:
return fibonacci(n - 1) + fibonacci(n - 2)

result = fibonacci(6)
print(result) # Output: 8

3. Binary Search
def binary_search(arr, target, low, high):
# Base case
if low > high:
return -1
mid = (low + high) // 2
# Base case: element found
if arr[mid] == target:
return mid
# Recursive cases
elif arr[mid] > target:
return binary_search(arr, target, low, mid - 1)
else:
return binary_search(arr, target, mid + 1, high)

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
target = 5
result = binary_search(arr, target, 0, len(arr) - 1)
print(result) # Output: 4

3. Call by Sharing:
Call by Sharing is a concept associated with languages like Java and Python. In this method, the
reference to the object is passed to the function, but the function cannot modify the actual object.
However, if the object is mutable (e.g., a list or dictionary), changes made to its contents are reflected
outside the function.

def modify_list(my_list):
my_list.append(4)
my_list = [10, 20, 30]

numbers = [1, 2, 3]
modify_list(numbers)
print(numbers) # Output: [1, 2, 3, 4]

8. Exception Handling:
- Error handling mechanisms.
Error handling is a crucial aspect of programming that involves detecting, responding to, and resolving
errors or exceptional situations in a program. Proper error handling enhances the reliability and
robustness of code. Some common error handling mechanisms include:

1. Try-catch blocks and error propagation (Excetion handling: division by zero)


try:
# Code that may raise an exception
result = 10 / 0
except ZeroDivisionError as e:
# Handling specific exception
print(f"Error: {e}")
except Exception as e:
# Handling a more general exception
print(f"Unexpected error: {e}")
finally:
# Optional block, executed regardless of whether an exception occurred
print("Finally block")

2. Return Codes:
In languages like C or C++, functions often use return codes to indicate success or failure.
Example
#include <stdio.h>

int divide(int a, int b, int *result) {


if (b == 0) {
// Return an error code
return -1;
}
*result = a / b;
// Return success code
return 0;
}

int main() {
int result;
if (divide(10, 2, &result) == 0) {
printf("Result: %d\n", result);
} else {
printf("Error: Division by zero\n");
}
return 0;
}

3. Throw and Catch (Exception Handling):


In languages like Java and C++, exceptions can be thrown and caught.
public class Example {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: " + e.getMessage());
}
}

static int divide(int a, int b) {


if (b == 0) {
// Throw an exception
throw new ArithmeticException("Division by zero");
}
return a / b;
}
}

4. Assert Statements:
Assert statements are used to check assumptions during development and can be disabled in production
for better performance.

Example (Python):
def divide(a, b):
assert b != 0, "Division by zero"
return a / b

result = divide(10, 0) # Raises an AssertionError with the specified message

5. Logging:
Logging is a way to record information about the program's execution, including errors.

Example (Python):
import logging

try:
result = 10 / 0
except Exception as e:
# Log the error
logging.error(f"An error occurred: {e}", exc_info=True)

6. Custom Exceptions:
Defining custom exceptions can be useful for handling specific error scenarios.

Example (Python):
class CustomError(Exception):
def __init__(self, message):
self.message = message

def process_data(data):
if not data:
raise CustomError("Data is empty")

try:
process_data([])
except CustomError as e:
print(f"Custom error: {e.message}")

9. Functional Programming Languages:


- Overview of functional programming concepts.
Functional programming is a paradigm that emphasizes the use of functions as the primary building
blocks of computation. In functional programming languages, functions are treated as first-class citizens,
allowing them to be passed as arguments, returned as values, and assigned to variables. The core
principles include immutability, where data is immutable once created, and purity, emphasizing
functions that produce the same output for the same input, without side effects. Recursion is often
favored over iteration for control flow, and higher-order functions enable the creation of more abstract
and reusable code. Popular functional programming languages include Haskell, Lisp, and Erlang. The
focus on declarative and mathematical constructs in functional programming fosters concise, modular,
and often parallelizable code, promoting robust and maintainable software development practices.

- Lambda calculus and closures.


Lambda calculus provides a theoretical foundation for expressing computation in terms of functions,
while closures, as a practical implementation concept, allow programming languages to manage and
maintain the environment in which functions are created. Together, they contribute to the expressive
power and flexibility of functional programming paradigms.

- Examples from languages like Lisp, Haskell, or ML.


Lambda Calculus Example:
; Define a lambda expression that adds two numbers
(setq add-function (lambda (x y) (+ x y)))

; Call the lambda function


(print (funcall add-function 3 4)) ; Output: 7
In this example, a lambda expression is used to create an anonymous function that adds two numbers.
The funcall function is then used to invoke the lambda function with arguments.

Closures Example:
; Define a function that returns a closure
(defun make-counter ()
(let ((count 0))
(lambda ()
(setq count (+ count 1))
count)))

; Create a counter using the closure


(setq counter1 (make-counter))
(setq counter2 (make-counter))

; Call the counters


(print (funcall counter1)) ; Output: 1
(print (funcall counter1)) ; Output: 2
(print (funcall counter2)) ; Output: 1
In this example, the make-counter function returns a closure. The closure is an anonymous function that
captures the variable count from its lexical environment. Each time the closure is called, it increments
and returns the count. Multiple counters can be created independently, each with its own state,
showcasing the concept of closures.

Examples:
1. Write a lambda expression in Lisp that squares a given number.
(setq square (lambda (x) (* x x)))
2. Write a lambda expression that takes two arguments and returns the product of their squares.
(setq product-of-squares (lambda (x y) (* (* x x) (* y y))))

3. Write a lambda expression that represents the factorial function.


(setq factorial (lambda (n) (if (<= n 1) 1 (* n (funcall factorial (- n 1))))))

4. Create a function map-squared that takes a list of numbers and returns a new list containing the
squares of each number.
(defun map-squared (lst)
(mapcar (lambda (x) (* x x)) lst))

5. Define a function sum-recursive that uses recursion to calculate the sum of all elements in a list.
(defun sum-recursive (lst)
(if (null lst)
0
(+ (car lst) (sum-recursive (cdr lst)))))

10. Logic Programming Languages:


- Prolog and declarative programming.
Prolog is a programming language that embodies the principles of declarative programming. Declarative
programming is a paradigm that emphasizes specifying what should be achieved rather than describing
how to achieve it. In Prolog, developers define a set of logical rules and facts that represent
relationships and conditions within a problem domain. The language is particularly well-suited for tasks
related to artificial intelligence, natural language processing, and rule-based systems. Prolog utilizes a
form of logic programming, where programs are expressed as sets of logical statements, and queries are
posed to a Prolog interpreter to find solutions based on logical inference. Declarative programming, as
exemplified by Prolog, allows for a more abstract and concise representation of problem-solving,
enabling the programmer to focus on the problem's essential characteristics rather than the procedural
details of how to solve it. This approach promotes a clearer separation between the problem
specification and its solution, fostering modularity and ease of comprehension in program design.

- Rules and queries.


In Prolog, rules and queries are essential components of logic programming, allowing developers to
express relationships and perform logical inference on the provided information.

Rules:
In Prolog, rules define relationships and conditions within a problem domain. A rule consists of a head
and a body, separated by the :- (if) symbol. The head specifies a goal or conclusion, and the body
contains conditions that must be satisfied for the rule to be true. For example:
parent(john, mary).
parent(john, jim).

In this example, the rules state that John is the parent of Mary and John is also the parent of Jim.

Queries:
Queries in Prolog are used to retrieve information or test relationships based on the defined rules. A
query consists of a goal or a set of goals that the Prolog interpreter attempts to satisfy using the given
rules. For instance:
?- parent(john, mary).

Examples:
1. Define a rule in Prolog representing the "sibling" relationship.
% Define the "parent" relationship
parent(john, mary).
parent(john, jim).
parent(susan, mary).
parent(susan, jim).
parent(bob, ann).

% Define the "sibling" rule


sibling(X, Y) :-
parent(Z, X),
parent(Z, Y),
X \= Y.

2. Query the knowledge base to find all siblings of a specific person.


% Query for siblings of a specific person (e.g., find all siblings of mary)
?- sibling(mary, Sibling).

If the knowledge base and rules are loaded into a Prolog interpreter, running this query might yield
results like:
Sibling = jim

- Classes, objects, inheritance, and polymorphism.


Classes, objects, inheritance, and polymorphism are fundamental concepts that contribute to the
organization and structure of code. A class serves as a blueprint or template that defines a data
structure and behaviors for creating objects. Objects, instances of classes, encapsulate data and
methods that operate on that data. They are the building blocks of OOP, allowing the modeling of real-
world entities and interactions in a program. Inheritance facilitates the creation of new classes by
inheriting attributes and behaviors from existing classes, promoting code reuse and establishing a
hierarchical relationship between classes. Polymorphism allows objects of different classes to be treated
as instances of a common base class, enabling flexibility and adaptability in code design. Through
polymorphism, a single interface can be used to represent a variety of related objects, fostering
extensibility and ease of maintenance. These OOP concepts collectively contribute to the modularity,
scalability, and maintainability of software systems, making them widely adopted in modern
programming languages and development practices.

- Examples from languages like Python, Java or C++.


These examples demonstrate the creation of classes, objects, inheritance, and polymorphism in Python.
Each concept contributes to the flexibility and organization of code, allowing for the creation of modular
and reusable software.

1. Classes and Objects (Python):


class Dog:
def __init__(self, name, age):
self.name = name
self.age = age

def bark(self):
print(f"{self.name} is barking!")

# Creating objects (instances) of the Dog class


dog1 = Dog("Buddy", 3)
dog2 = Dog("Charlie", 5)

# Accessing object attributes and calling methods


print(f"{dog1.name} is {dog1.age} years old.")
dog2.bark()

2. Inheritance:
class Animal:
def __init__(self, name):
self.name = name

def speak(self):
pass

class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"

class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"

# Creating instances of the derived classes


dog = Dog("Buddy")
cat = Cat("Whiskers")

# Using inheritance to call speak method


print(dog.speak()) # Output: Buddy says Woof!
print(cat.speak()) # Output: Whiskers says Meow!

3. Polymorphism:
class Shape:
def area(self):
pass

class Square(Shape):
def __init__(self, side_length):
self.side_length = side_length

def area(self):
return self.side_length ** 2

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return 3.14 * self.radius ** 2

# Using polymorphism with different shapes


square = Square(4)
circle = Circle(3)

print(f"Area of the square: {square.area()}") # Output: 16


print(f"Area of the circle: {circle.area()}") # Output: 28.26

(Skip)

12. Language Design Principles:


- Language paradigms and their trade-offs.
- Abstraction, encapsulation, and modularity.

13. Case Studies:


- Analyzing and comparing specific programming languages and their features.

14. Project or Assignments:


- Hands-on assignments or a final project that may involve designing a small programming language or
implementing specific language features.

You might also like