Data Types & Abstraction
Data Types & Abstraction¶
Topic: Programming Lesson: 3 of 16 Prerequisites: What Is Programming, Programming Paradigms Objective: Understand data types, type systems, abstract data types, and how abstraction manages complexity.
What Are Types?¶
A type is a classification of data that determines: - What values the data can hold - What operations can be performed on it - How much memory it occupies - How it's interpreted by the computer
Analogy: Types are like containers — a glass bottle holds liquids, a cardboard box holds solid items. You can't pour water into a cardboard box (or rather, you shouldn't). Types enforce similar constraints in code.
Why Types Matter¶
# Without types (conceptually):
x = "42"
y = 10
z = x + y # What should this mean? "4210" or 52? Error?
# With types:
x: str = "42"
y: int = 10
# z = x + y # Type error: can't add string and int
z = int(x) + y # Explicit conversion: 52
Types prevent errors by enforcing constraints on what operations are valid.
Primitive Types¶
Primitive types are the building blocks provided by the language. They typically map directly to hardware representations.
Integers¶
Whole numbers without fractional components.
Python:
age = 25
population = 7_800_000_000 # Python allows underscores for readability
negative = -42
Java:
byte smallNumber = 127; // 8-bit: -128 to 127
short mediumNumber = 32000; // 16-bit: -32,768 to 32,767
int standardNumber = 100000; // 32-bit: ~-2B to 2B
long largeNumber = 10000000000L; // 64-bit
C++:
int x = 42;
unsigned int y = 100; // Only positive values
long long z = 9223372036854775807LL; // 64-bit
Floating-Point Numbers¶
Numbers with fractional parts. Approximate representations due to binary encoding.
Python:
pi = 3.14159
scientific = 6.022e23 # 6.022 × 10^23 (Avogadro's number)
JavaScript:
let price = 19.99;
let tiny = 0.0000001;
let notExact = 0.1 + 0.2; // 0.30000000000000004 (floating-point precision issue)
Java:
float f = 3.14f; // 32-bit, single precision
double d = 3.14159; // 64-bit, double precision (default)
Booleans¶
True or false values for logical operations.
Python:
is_active = True
has_permission = False
if is_active and has_permission:
print("Access granted")
JavaScript:
let isLoggedIn = true;
let isAdmin = false;
console.log(isLoggedIn && !isAdmin); // true
C++:
bool flag = true;
bool result = (5 > 3); // true
Characters¶
Single characters, often represented as integers (ASCII/Unicode code points).
Java:
char letter = 'A'; // Single quotes for char
char unicode = '\u0041'; // Unicode: also 'A'
C++:
char c = 'x';
char newline = '\n';
Python:
# Python has no separate char type; single-character strings
letter = 'A'
Strings¶
Sequences of characters. Some languages treat strings as primitives, others as objects.
Python:
name = "Alice"
message = 'Hello, World!'
multiline = """This is
a multi-line
string"""
JavaScript:
let greeting = "Hello";
let template = `Hello, ${name}!`; // Template literals
Java:
String text = "Hello, World!"; // String is an object, not primitive
C++:
#include <string>
std::string message = "Hello, C++!";
Composite Types¶
Types built from primitive types.
Arrays¶
Fixed-size, ordered collections of elements of the same type.
Python:
# Python lists are dynamic, not fixed-size, but conceptually similar
numbers = [1, 2, 3, 4, 5]
JavaScript:
let numbers = [1, 2, 3, 4, 5]; // Dynamic arrays
Java:
int[] numbers = {1, 2, 3, 4, 5}; // Fixed size
int[] array = new int[10]; // Allocate size 10, initialized to 0
C++:
#include <array>
std::array<int, 5> numbers = {1, 2, 3, 4, 5}; // Fixed size 5
Records/Structs¶
Group related data of different types.
C:
struct Person {
char name[50];
int age;
double salary;
};
struct Person alice = {"Alice", 30, 75000.0};
printf("%s is %d years old\n", alice.name, alice.age);
C++:
struct Point {
int x;
int y;
};
Point p = {10, 20};
std::cout << "x: " << p.x << ", y: " << p.y << std::endl;
Tuples¶
Ordered, fixed-size collections of elements, possibly of different types.
Python:
person = ("Alice", 30, "Engineer") # (name, age, job)
name, age, job = person # Unpacking
JavaScript (using arrays):
let person = ["Alice", 30, "Engineer"];
let [name, age, job] = person; // Destructuring
Type Systems¶
Static vs Dynamic Typing¶
Static Typing: Types are checked at compile time. Variables have fixed types.
Languages: Java, C++, C, Rust, Go, TypeScript
Java example:
int x = 10;
// x = "hello"; // Compile error: incompatible types
x = 20; // OK
C++ example:
int count = 5;
// count = "text"; // Compile error
count = 10; // OK
Benefits: - Catch errors early (before running the program) - Better tooling (autocomplete, refactoring) - Performance optimizations (compiler knows types)
Trade-offs: - More verbose (type annotations) - Less flexibility
Dynamic Typing: Types are checked at runtime. Variables can hold any type.
Languages: Python, JavaScript, Ruby, PHP
Python example:
x = 10 # x is an int
x = "hello" # Now x is a string — no error
x = [1, 2] # Now x is a list
JavaScript example:
let x = 10;
x = "hello"; // OK
x = {key: "value"}; // OK
Benefits: - Less boilerplate - More flexible - Faster prototyping
Trade-offs: - Errors caught at runtime (might crash in production) - Harder to reason about code in large projects - Slower performance (runtime type checks)
Strong vs Weak Typing¶
Strong Typing: Strict enforcement of type rules. No implicit conversions between incompatible types.
Python (strong):
x = "5"
y = 10
# z = x + y # TypeError: can't add string and int
z = int(x) + y # Must explicitly convert: 15
Weak Typing: Allows implicit type conversions (type coercion).
JavaScript (weak):
let x = "5";
let y = 10;
let z = x + y; // "510" — string concatenation (implicit conversion)
let w = x - y; // -5 — subtraction (implicit conversion to number)
console.log(z, w); // "510", -5
Benefits of strong typing: Fewer surprises, clearer intent Benefits of weak typing: More permissive, less verbose (but more bugs)
Type Inference¶
The compiler/interpreter deduces types automatically.
Kotlin:
val x = 10 // Inferred as Int
val name = "Alice" // Inferred as String
// x = "text" // Error: type mismatch
Rust:
let x = 10; // Inferred as i32 (32-bit integer)
let y = 3.14; // Inferred as f64 (64-bit float)
TypeScript:
let count = 5; // Inferred as number
// count = "text"; // Error: Type 'string' is not assignable to type 'number'
Benefits: Conciseness of dynamic typing + safety of static typing.
Abstract Data Types (ADTs)¶
An Abstract Data Type separates the interface (what operations are available) from the implementation (how those operations are performed).
Key idea: Users interact with the ADT through a well-defined interface without needing to know internal details.
Stack ADT¶
Interface (operations):
- push(item): Add item to top
- pop(): Remove and return top item
- peek(): View top item without removing
- is_empty(): Check if stack is empty
Implementation 1: Using an array
Python:
class ArrayStack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._data.pop()
def peek(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._data[-1]
def is_empty(self):
return len(self._data) == 0
# Usage (same interface regardless of implementation)
stack = ArrayStack()
stack.push(10)
stack.push(20)
print(stack.pop()) # 20
Implementation 2: Using a linked list
Python:
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class LinkedStack:
def __init__(self):
self._top = None
self._size = 0
def push(self, item):
self._top = Node(item, self._top)
self._size += 1
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
value = self._top.value
self._top = self._top.next
self._size -= 1
return value
def peek(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self._top.value
def is_empty(self):
return self._top is None
# Usage (same interface!)
stack = LinkedStack()
stack.push(10)
stack.push(20)
print(stack.pop()) # 20
Key point: The interface is the same. Users don't need to know if it's array-based or linked-list-based. This is abstraction.
Queue ADT¶
Interface:
- enqueue(item): Add to rear
- dequeue(): Remove and return from front
- is_empty(): Check if empty
Java implementation:
import java.util.LinkedList;
public interface Queue<T> {
void enqueue(T item);
T dequeue();
boolean isEmpty();
}
public class LinkedQueue<T> implements Queue<T> {
private LinkedList<T> data = new LinkedList<>();
public void enqueue(T item) {
data.addLast(item);
}
public T dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return data.removeFirst();
}
public boolean isEmpty() {
return data.isEmpty();
}
}
Map/Dictionary ADT¶
Interface:
- put(key, value): Store key-value pair
- get(key): Retrieve value by key
- remove(key): Delete key-value pair
- contains(key): Check if key exists
Python (using built-in dict):
# Python's dict is an implementation of the Map ADT
phonebook = {}
phonebook["Alice"] = "555-1234"
phonebook["Bob"] = "555-5678"
print(phonebook["Alice"]) # "555-1234"
print("Alice" in phonebook) # True
JavaScript (using Map):
let map = new Map();
map.set("Alice", "555-1234");
map.set("Bob", "555-5678");
console.log(map.get("Alice")); // "555-1234"
console.log(map.has("Alice")); // true
Generics and Templates¶
Generics (Java, C#, TypeScript) and Templates (C++) allow you to write code that works with any type.
Java Generics¶
Without generics:
// Must use Object, lose type safety
public class Box {
private Object item;
public void set(Object item) {
this.item = item;
}
public Object get() {
return item;
}
}
Box box = new Box();
box.set("Hello");
String s = (String) box.get(); // Explicit cast needed
With generics:
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String s = stringBox.get(); // No cast needed, type-safe
Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("text"); // Compile error
C++ Templates¶
template <typename T>
class Box {
private:
T item;
public:
void set(T value) {
item = value;
}
T get() const {
return item;
}
};
// Usage
Box<int> intBox;
intBox.set(42);
std::cout << intBox.get() << std::endl; // 42
Box<std::string> stringBox;
stringBox.set("Hello");
std::cout << stringBox.get() << std::endl; // Hello
TypeScript Generics¶
class Box<T> {
private item: T;
set(value: T): void {
this.item = value;
}
get(): T {
return this.item;
}
}
let stringBox = new Box<string>();
stringBox.set("Hello");
console.log(stringBox.get()); // Hello
let numberBox = new Box<number>();
numberBox.set(42);
console.log(numberBox.get()); // 42
Benefits: Code reuse, type safety, no runtime overhead (types erased in Java, template instantiation in C++).
Algebraic Data Types¶
Sum Types (Enums, Tagged Unions)¶
A value can be one of several variants.
Rust enum:
enum Status {
Success,
Error(String),
Loading,
}
let result = Status::Error("Network timeout".to_string());
match result {
Status::Success => println!("Success!"),
Status::Error(msg) => println!("Error: {}", msg),
Status::Loading => println!("Loading..."),
}
TypeScript discriminated unions:
type Status =
| { kind: "success"; data: string }
| { kind: "error"; message: string }
| { kind: "loading" };
function handleStatus(status: Status) {
switch (status.kind) {
case "success":
console.log("Data:", status.data);
break;
case "error":
console.log("Error:", status.message);
break;
case "loading":
console.log("Loading...");
break;
}
}
Product Types (Tuples, Records)¶
A value contains multiple fields together.
Rust struct:
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 10, y: 20 };
println!("({}, {})", p.x, p.y);
TypeScript:
type Point = {
x: number;
y: number;
};
let p: Point = { x: 10, y: 20 };
console.log(`(${p.x}, ${p.y})`);
Null and Its Problems¶
The Billion-Dollar Mistake — Tony Hoare (inventor of null references):
"I call it my billion-dollar mistake. It was the invention of the null reference in 1965... This has led to innumerable errors, vulnerabilities, and system crashes."
The Problem¶
String name = getUserName();
int length = name.length(); // NullPointerException if name is null
Many languages allow variables to be null, leading to runtime crashes.
Solution: Option/Maybe Types¶
Rust's Option
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
let result = divide(10, 2);
match result {
Some(value) => println!("Result: {}", value),
None => println!("Cannot divide by zero"),
}
// Or use combinators
let result = divide(10, 2).unwrap_or(0); // Default to 0 if None
Java's Optional
import java.util.Optional;
public Optional<String> findUserName(int id) {
if (id == 1) {
return Optional.of("Alice");
} else {
return Optional.empty();
}
}
Optional<String> name = findUserName(1);
name.ifPresent(n -> System.out.println("Name: " + n));
String result = name.orElse("Unknown"); // Default value
TypeScript:
function divide(a: number, b: number): number | null {
return b === 0 ? null : a / b;
}
let result = divide(10, 2);
if (result !== null) {
console.log("Result:", result);
} else {
console.log("Cannot divide by zero");
}
Benefits: Forces you to handle the absence of a value explicitly. No more NullPointerException surprises.
Type Annotations and Documentation¶
Even in dynamically-typed languages, you can (and should) document types.
Python Type Hints¶
def greet(name: str) -> str:
"""
Greet a person by name.
Args:
name: The person's name
Returns:
A greeting message
"""
return f"Hello, {name}!"
# Type checker (mypy) can catch errors:
# greet(42) # Error: Argument 1 has incompatible type "int"; expected "str"
JavaScript with JSDoc¶
/**
* Calculate the area of a rectangle
* @param {number} width - The width
* @param {number} height - The height
* @returns {number} The area
*/
function area(width, height) {
return width * height;
}
TypeScript¶
function area(width: number, height: number): number {
return width * height;
}
// area("5", 10); // Error: Argument of type 'string' is not assignable to 'number'
Exercises¶
Exercise 1: Type System Analysis¶
Given this JavaScript code:
let x = "10";
let y = 5;
console.log(x + y); // "105"
console.log(x - y); // 5
- Why does
+produce"105"but-produces5? - Would this work in Python? Why or why not?
- Is this strong or weak typing? Static or dynamic?
Exercise 2: Implement a Stack ADT¶
Implement a stack ADT in your language of choice:
- Use an array as the underlying data structure
- Implement push, pop, peek, is_empty
- Test with integers, then test with strings (same code should work)
Exercise 3: Generics¶
Implement a generic Pair<T, U> class that holds two values of potentially different types:
- Constructor: Pair(T first, U second)
- Methods: getFirst(), getSecond(), setFirst(T), setSecond(U)
- Test: Pair<String, Integer> for ("Alice", 30)
Exercise 4: Option Type¶
Implement a simple Option<T> type (similar to Rust or Java):
- Some(value): Contains a value
- None: Empty
- Methods:
- isSome(): Returns true if Some
- isNone(): Returns true if None
- unwrap(): Returns value or throws error if None
- unwrapOr(default): Returns value or default if None
Exercise 5: ADT Design¶
Design an ADT for a Library System: - What operations should it support? - Add book - Remove book - Search by title, author, ISBN - Borrow book - Return book - Define the interface (don't implement yet) - Consider: What data structures could implement this?
Exercise 6: Null Safety¶
Refactor this Java code to use Optional:
public String getUserEmail(int userId) {
if (userId == 1) {
return "alice@example.com";
}
return null;
}
String email = getUserEmail(1);
System.out.println(email.toUpperCase()); // Potential NullPointerException
Make it safe using Optional<String>.
Summary¶
- Types classify data: primitives (int, float, bool, char, string), composites (arrays, structs, tuples)
- Type systems:
- Static vs dynamic: compile-time vs runtime checking
- Strong vs weak: strict vs permissive type rules
- Type inference: automatic type deduction
- Abstract Data Types: Interface (what) vs implementation (how)
- Stack, Queue, Map/Dictionary
- Generics/Templates: Type-parameterized code for reusability and type safety
- Algebraic Data Types: Sum types (enums), product types (tuples/records)
- Null safety:
Option/Maybe/Optionaltypes prevent null-related crashes - Documentation: Type annotations make code clearer and catch errors early
Key Insight: Abstraction is about managing complexity. ADTs let you think at a higher level without worrying about implementation details. Types help catch errors and make code more maintainable.
Navigation¶
← Previous: Programming Paradigms | Next: Control Flow Patterns →