Maybe I'm off, but to me the gist of the expression problem can be explained by contrasting how code extensibility is achieved in OOP/FP.
OOP Approach with interface/inheritance:
Easy: Adding new types (variants) of a base class/interface.
Hard: Adding new functionality to the base class/interface, as it requires implementing it in all existing types.
FP Approach with Discriminated Unions:
Easy: Adding new functions. Create a function and match on the DU; the compiler ensures all cases are handled.
Hard: Adding new types to the DU, as it requires updating all existing exhaustive pattern matches throughout the codebase.
Here's some Kotlin code. Kotlin is great because it can do both really well.
// Object-Oriented Approach
interface Shape {
fun area(): Double
fun perimeter(): Double
}
class Circle(val radius: Double) : Shape {
override fun area() = Math.PI \* radius \* radius
override fun perimeter() = 2 \* Math.PI \* radius
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun area() = width \* height
override fun perimeter() = 2 \* (width + height)
}
// Easy to add new shape
class Triangle(val a: Double, val b: Double, val c: Double) : Shape {
override fun area(): Double {
val s = (a + b + c) / 2
return Math.sqrt(s \* (s - a) \* (s - b) \* (s - c))
}
override fun perimeter() = a + b + c
}
// Hard to add new function (need to modify all existing shapes)
// interface Shape {
// fun area(): Double
// fun perimeter(): Double
// fun draw(): String // New function
// }
// Functional Approach
sealed class ShapeFP {
data class CircleFP(val radius: Double) : ShapeFP()
data class RectangleFP(val width: Double, val height: Double) : ShapeFP()
}
fun area(shape: ShapeFP): Double = when (shape) {
is ShapeFP.CircleFP -> Math.PI \* shape.radius \* shape.radius
is ShapeFP.RectangleFP -> shape.width \* shape.height
}
fun perimeter(shape: ShapeFP): Double = when (shape) {
is ShapeFP.CircleFP -> 2 \* Math.PI \* shape.radius
is ShapeFP.RectangleFP -> 2 \* (shape.width + shape.height)
}
// Easy to add new function
fun draw(shape: ShapeFP): String = when (shape) {
is ShapeFP.CircleFP -> "O"
is ShapeFP.RectangleFP -> "[]"
}
// Hard to add new shape (need to update all existing functions)
// sealed class ShapeFP {
// data class CircleFP(val radius: Double) : ShapeFP()
// data class RectangleFP(val width: Double, val height: Double) : ShapeFP()
// data class TriangleFP(val a: Double, val b: Double, val c: Double) : ShapeFP()
// }