Say you have this in an Object-Oriented application:
module Talker
def talk(word)
puts word
end
end
module Swimmer
def swim(distance)
puts "swimming #{distance}"
end
end
class Organism
def initialize
rise
end
def rise
puts "hello world"
end
end
class Animal extends Organism
def think(something)
puts "think #{something}"
end
end
class Bird extends Animal
include Talker
end
class Fish extends Animal
include Swimmer
end
bird = new Bird
fish = new Fish
In this, you can call methods which are unique to each:
bird.talk("hello")
fish.swim(50)
But you can also call methods which are the same:
bird.think("fly")
fish.think("swim")
If I have a function that takes an animal, I can call the think function:
def experience(animal)
animal.think("one")
animal.think("two")
animal.think("one")
end
In a pseudo functional language, you can do the same basically:
function experience(animal) {
think(animal)
think(animal)
think(animal)
}
But not really, you would have to check the type:
function think(genericObject) {
if (genericObject is Animal) {
animalThink(genericObject)
} else if (genericObject is SomethingElse) {
somethingElseThink(genericObject)
}
}
That is because, when implementing your "experience" function, you don't want just animals to experience, you want rocks and trees and other things to experience too, but their experience functions are different.
function experience(thing) {
move(thing)
move(thing)
move(thing)
}
function move(thing) {
case thing {
match Animal then animalMove(thing)
match Plant then plantMove(thing)
match Rock then rockMove(thing)
}
}
In this way, you can't have a cleanly reusable function, your function must know of the specific types it's going to receive somewhere down the line.
Is there any way to avoid this and make it more like OO polymorphism, in a functional language?
If so, at a high level, how does it work under the hood if this can be solved in a functional language?
Achieving polymorphism in functional programming
https://www.quora.com/How-is-polymorphism-used-in-functional-programming-languages
https://wiki.haskell.org/OOP_vs_type_classes
Functional programming languages have a variety of ways of achieving polymorphism. I'm going to contrast Java (the OOP language I know best) with Haskell (the functional language I know best).
Way 1: "parametric polymorphism"
With parametric polymorphism, you don't need to know anything at all about the underlying type. For example, if I have a singly-linked list with elements of type T, I actually don't need to know anything about type T in order to find the length of the list. I would just write something like
length :: forall a . [a] -> Integer
length [] = 0
length (x:xs) = 1 + length xs
in Haskell (obviously I'd want to use a better algorithm in practice, but you get the idea). Note that it doesn't matter what the type of the list elements is; the code for getting the length is the same. The first line is the "type signature". It says that for every type a, length will take a list of a and output an integer.
This can't be used for too much "serious polymorphism", but it's definitely a strong start. It corresponds roughly to Java's generics.
Way 2: typeclass-style polymorphism
Even something as benign as checking for equality actually requires polymorphism. Different types require different code for checking equality, and for some types (generally functions), checking equality is literally impossible because of the halting problem. Thus, we use "type classes".
Let's say I define a new type with 2 elements, Bob and Larry. In Haskell, this looks like
data VeggieTalesStars = Bob | Larry
I would like to be able to compare two elements of type VeggieTalesStars for equality. To do this, I would need to implement an Eq instance.
instance Eq VeggieTalesStars where
Bob == Bob = True
Larry == Larry = True
Bob == Larry = False
Larry == Bob = False
Note that the function (==) has the type signature
(==) :: forall b . Eq b => b -> b -> Bool
This means that for every type b, if b has an Eq instance, then (==) can take two arguments of type b and return a Bool.
It's probably not too difficult for you to guess that the not-equals function (/=) also has the type signature
(/=) :: forall b . Eq b => b -> b -> Bool
Because (/=) is defined by
x /= y = not (x == y)
When we call the (/=) function, the function will deploy the correct version of the (==) function based on the types of the arguments. If the arguments have different types, you won't be able to compare them using (/=).
Typeclass-style polymorphism allows you to do the following:
class Animal b where
think :: b -> String -> String
-- we provide the default implementation
think b string = "think " ++ string
data Fish = Fish
data Bird = Bird
instance Animal Fish where
instance Animal Bird where
Both Fish and Bird implement the "Animal" typeclass, so we can call the think function on both. That is,
>>> think Bird "thought"
"think thought"
>>> think Fish "thought"
"think thought"
This use case corresponds roughly to Java interfaces - types can implement as many type classes as they want. But type classes are far more powerful than interfaces.
Way 3: Functions
If your object only has one method, it may as well just be a function. This is a very common way to avoid inheritance hierarchies - deal with functions rather than inheritors of a 1-method base class.
One might therefore define
type Animal = String -> String
basicAnimal :: Animal
basicAnimal thought = "think " ++ thought
An "animal" is really just a way of taking one string and producing another. This would correspond to the Java code
class Animal {
public String think(String thought) {
return "think " + thought;
}
}
Let's say that in Java, we decided to implement a subclass of animal as follows:
class ThoughtfulPerson extends Animal {
private final String thought;
public ThoughtfulPerson(final String thought) {
this.thought = thought;
}
#Override
public String think(String thought) {
System.out.println("I normally think " + this.thought ", but I'm currently thinking" + thought + ".");
}
}
In Haskell, we would implement this as
thoughtfulPerson :: String -> Animal
thoughtfulPerson originalThought newThought = "I normally think " ++ originalThought ", but I'm currently thinking" ++ newThought ++ "."
The "dependency injection" of Java code is realised by Haskell's higher-order functions.
Way 4: composition over inheritance + functions
Suppose we have an abstract base class Thing with two methods:
abstract class Thing {
public abstract String name();
public abstract void makeLightBlink(int duration);
}
I'm using Java-style syntax, but hopefully it's not too confusing.
Fundamentally, the only way to use this abstract base class is by calling its two methods. Therefore, a Thing should actually be considered to be an ordered pair consisting of a string and a function.
In a functional language like Haskell, we would write
data Thing = Thing { name :: String, makeLightsBlink :: Int -> IO () }
In other words, a "Thing" consists of two parts: a name, which is a string, and a function makeLightsBlink, which takes an Int and outputs an "IO action". This is Haskell's way of dealing with IO - through the type system.
Instead of defining subclasses of Thing, Haskell would just have you define functions which output a Thing (or define Things themselves directly). So if in Java you might define
class ConcreteThing extends Thing {
#Override
public String name() {
return "ConcreteThing";
}
#Override
public void makeLightsBlink(int duration) {
for (int i = 0; i < duration; i++) {
System.out.println("Lights are blinking!");
}
}
}
In Haskell, you would instead define
concreteThing :: Thing
concreteThing = Thing { name = "ConcreteThing", makeLightsBlink = blinkFunction } where
blinkFunction duration = for_ [1..duration] . const $ putStrLn "Lights are blinking!"
No need to do anything fancy. You can implement any behaviour you want by using composition and functions.
Way 5 - avoid polymorphism entirely
This corresponds to the "open vs closed principle" in object oriented programming.
Some times, the correct thing to do is actually to avoid polymorphism entirely. For example, consider how one might implement a singly-linked list in Java.
abstract class List<T> {
public abstract bool is_empty();
public abstract T head();
public abstract List<T> tail();
public int length() {
return empty() ? 0 : 1 + tail().length();
}
}
class EmptyList<T> {
#Override
public bool is_empty() {
return true;
}
#Override
public T head() {
throw new IllegalArgumentException("can't take head of empty list");
}
#Override
public List<T> tail() {
throw new IllegalArgumentException("can't take tail of empty list");
}
}
class NonEmptyList<T> {
private final T head;
private final List<T> tail;
public NonEmptyList(T head, List<T> tail) {
this.head = head;
this.tail = tail;
}
#Override
public bool is_empty() {
return false;
}
#Override
public T head() {
return self.head;
}
#Override
public List<T> tail() {
return self.tail;
}
}
However, this is actually not a good model because you'd like there to only be two ways of constructing a list - the empty way, and the non-empty way. Haskell allows you to do this quite simply. The analogous Haskell code is
data List t = EmptyList | NonEmptyList t (List t)
empty :: List t -> Bool
empty EmptyList = True
empty (NonEmptyList t listT) = False
head :: List t -> t
head EmptyList = error "can't take head of empty list"
head (NonEmptyList t listT) = t
tail :: List t -> List t
tail EmptyList = error "can't take tail of empty list"
tail (NonEmptyList t listT) = listT
length list = if empty list then 0 else 1 + length (tail list)
Of course, in Haskell we try to avoid functions that are "partial" - we try to make sure that every function always returns a value. So you won't see many Haskellers actually using the "head" and "tail" functions for precisely this reason - they sometimes error out. You'd instead see length defined by
length EmptyList = 0
length (NonEmptyList t listT) = 1 + length listT
using pattern-matching.
This feature of functional programming languages is called "algebraic data types". It's incredibly useful.
Hopefully, I've convinced you that not only does functional programming allow you to implement many object-oriented design patterns, it can actually allow you to express the same ideas in much more succinct and obvious forms.
I have added some sugar to your example because it was difficult to justify an object centric implementation with your functions.
Note that I don't write a lot of Haskell but I think it's the right language to draw a comparison.
I don't recommend comparing pure OO languages and pure FP languages directly as it is a waste of time. If you pick up a FP language and learn how to think functionally you will not miss any OO feature.
-- We define and create data of type Fish and Bird
data Fish = Fish String
nemo = Fish "Nemo";
data Bird = Bird String
tweety = Bird "Tweety"
-- We define how they can be displayed with the function `show`
instance Show Fish where
show (Fish name) = name ++ " the fish"
instance Show Bird where
show (Bird name) = name ++ " the bird"
{- We define how animals can think with the function `think`.
Both Fish and Bird will be Animals.
Notice how `show` dispatches to the correct implementation.
We need to add to the type signature the constraint that
animals are showable in order to use `show`.
-}
class Show a => Animal a where
think :: a -> String -> String
think animal thought =
show animal ++ " is thinking about " ++ thought
instance Animal Fish
instance Animal Bird
-- Same thing for Swimmer, only with Fish
class Show s => Swimmer s where
swim :: s -> String -> String
swim swimmer length =
show swimmer ++ " is swimming " ++ length
instance Swimmer Fish
-- Same thing for Singer, only with Bird
class Show s => Singer s where
sing :: s -> String
sing singer = show singer ++ " is singing"
instance Singer Bird
{- We define a function which applies to any animal.
The compiler can figure out that it takes any type
of the class Animal because we are using `think`.
-}
goToCollege animal = think animal "quantum physics"
-- we're printing the values to the console
main = do
-- prints "Nemo the fish is thinking about quantum physics"
print $ goToCollege nemo
-- prints "Nemo the fish is swimming 4 meters"
print $ swim nemo "4 meters"
-- prints "Tweety the bird is thinking about quantum physics"
print $ goToCollege tweety
-- prints "Tweety the bird is singing"
print $ sing tweety
I was wondering what it would look like in Clojure. It's not as satisfying because defprotocol doesn't provide default implementations, but then again: are we not forcing a style upon a language which is not designed for it?
(defprotocol Show
(show [showable]))
(defprotocol Animal
(think [animal thought]))
(defn animal-think [animal thought]
(str (show animal) " is thinking about " thought))
(defprotocol Swimmer
(swim [swimmer length]))
(defprotocol Singer
(sing [singer]))
(defrecord Fish [name]
Show
(show [fish] (str (:name fish) " the fish"))
Animal
(think [a b] (animal-think a b))
Swimmer
(swim [swimmer length] (str (show swimmer) " is swimming " length)))
(defrecord Bird [name]
Show
(show [fish] (str (:name fish) " the bird"))
Animal
(think [a b] (animal-think a b))
Singer
(sing [singer] (str (show singer) " is singing")))
(defn goToCollege [animal]
(think animal "quantum physics"))
(def nemo (Fish. "Nemo"))
(def tweety (Bird. "Tweety"))
(println (goToCollege nemo))
(println (swim nemo "4 meters"))
(println (goToCollege tweety))
(println (sing tweety))
The problem is that what kind of polymorphism you want. If you just need some polymorphism on compile time, Haskell's typeclass is nearly perfect for most situations.
If you want to have polymorphism of run time(i.e. dynamically switch behaviors based on runtime type), this programming pattern is discouraged in many functional programming languages since with powerful generics and typeclasses, dynamic polymorphism is not always necessary.
In short, If the language support subtype, you can choose dynamic polymorphism while in a strict functional language without complete subtypes, you should always program in a functional way. Lastly, If you still want both(dynamic polymorphism and powerful typeclasses), you can try languages with traits like Scala or Rust.
Could someone please clarify the concepts behind this use of the "this" keyword?
(define call
(lambda (obj method-name . args)
(apply (obj method-name) obj args)))
(define -cuboid-
(lambda (w l h)
(define volume
(lambda (this)
(* h (call this 'area))))
(define area
(lambda (this)
(* w l)))
(lambda (method-name)
(cond
((eq? 'volume method-name) volume)
((eq? 'area method-name) area)
(else (error "method not found: ~s" method-name))))
(define r1 (-cuboid- 2 3 4))
(call r1 'area) ;=> 6
(call r1 'volume) ;=> 24
I understand that this is a keyword to refer to the object that is being used. I found out that this alone doesn't have any particular meaning in this program (it needs to refer to the arguments of the lambda functions).
The call is ((-cuboid- 2 3 4) 'volume), which brings to (* h (call this 'area)), where has this been defined?
this is simply the argument of the lambda, this could be anything; try changing it to, e.g., myself in the first lambda and me in the second (where it is not used, by the way, but needs to be there for the call to work).
The call to ((-cuboid- 2 3 4) 'volume) returns that procedure, with names bound according to the sketch below:
In call, r1 'volume calls the "lookup method" of -cuboid- and returns the volume procedure, which is then called with the obj argument, binding that to the name this
Thus, this gets bound to the r1 argument to call
I understand that abstraction is about taking something more concrete and making it more abstract. That something may be either a data structure or a procedure. For example:
Data abstraction: A rectangle is an abstraction of a square. It concentrates on the fact a square has two pairs of opposite sides and it ignores the fact that adjacent sides of a square are equal.
Procedural abstraction: The higher order function map is an abstraction of a procedure which performs some set of operations on a list of values to produce an entirely new list of values. It concentrates on the fact that the procedure loops through every item of the list in order to produce a new list and ignores the actual operations performed on every item of the list.
So my question is this: how is abstraction any different from generalization? I'm looking for answers primarily related to functional programming. However if there are parallels in object-oriented programming then I would like to learn about those as well.
A very interesting question indeed. I found this article on the topic, which concisely states that:
While abstraction reduces complexity by hiding irrelevant detail, generalization reduces complexity by replacing multiple entities which perform similar functions with a single construct.
Lets take the old example of a system that manages books for a library. A book has tons of properties (number of pages, weight, font size(s), cover,...) but for the purpose of our library we may only need
Book(title, ISBN, borrowed)
We just abstracted from the real books in our library, and only took the properties that interested us in the context of our application.
Generalization on the other hand does not try to remove detail but to make functionality applicable to a wider (more generic) range of items. Generic containers are a very good example for that mindset: You wouldn't want to write an implementation of StringList, IntList, and so on, which is why you'd rather write a generic List which applies to all types (like List[T] in Scala). Note that you haven't abstracted the list, because you didn't remove any details or operations, you just made them generically applicable to all your types.
Round 2
#dtldarek's answer is really a very good illustration! Based on it, here's some code that might provide further clarification.
Remeber the Book I mentioned? Of course there are other things in a library that one can borrow (I'll call the set of all those objects Borrowable even though that probably isn't even a word :D):
All of these items will have an abstract representation in our database and business logic, probably similar to that of our Book. Additionally, we might define a trait that is common to all Borrowables:
trait Borrowable {
def itemId:Long
}
We could then write generalized logic that applies to all Borrowables (at that point we don't care if its a book or a magazine):
object Library {
def lend(b:Borrowable, c:Customer):Receipt = ...
[...]
}
To summarize: We stored an abstract representation of all the books, magazines and DVDs in our database, because an exact representation is neither feasible nor necessary. We then went ahead and said
It doesn't matter whether a book, a magazine or a DVD is borrowed by a customer. It's always the same process.
Thus we generalized the operation of borrowing an item, by defining all things that one can borrow as Borrowables.
Object:
Abstraction:
Generalization:
Example in Haskell:
The implementation of the selection sort by using priority queue with three different interfaces:
an open interface with the queue being implemented as a sorted list,
an abstracted interface (so the details are hidden behind the layer of abstraction),
a generalized interface (the details are still visible, but the implementation is more flexible).
{-# LANGUAGE RankNTypes #-}
module Main where
import qualified Data.List as List
import qualified Data.Set as Set
{- TYPES: -}
-- PQ new push pop
-- by intention there is no build-in way to tell if the queue is empty
data PriorityQueue q t = PQ (q t) (t -> q t -> q t) (q t -> (t, q t))
-- there is a concrete way for a particular queue, e.g. List.null
type ListPriorityQueue t = PriorityQueue [] t
-- but there is no method in the abstract setting
newtype AbstractPriorityQueue q = APQ (forall t. Ord t => PriorityQueue q t)
{- SOLUTIONS: -}
-- the basic version
list_selection_sort :: ListPriorityQueue t -> [t] -> [t]
list_selection_sort (PQ new push pop) list = List.unfoldr mypop (List.foldr push new list)
where
mypop [] = Nothing -- this is possible because we know that the queue is represented by a list
mypop ls = Just (pop ls)
-- here we abstract the queue, so we need to keep the queue size ourselves
abstract_selection_sort :: Ord t => AbstractPriorityQueue q -> [t] -> [t]
abstract_selection_sort (APQ (PQ new push pop)) list = List.unfoldr mypop (List.foldr mypush (0,new) list)
where
mypush t (n, q) = (n+1, push t q)
mypop (0, q) = Nothing
mypop (n, q) = let (t, q') = pop q in Just (t, (n-1, q'))
-- here we generalize the first solution to all the queues that allow checking if the queue is empty
class EmptyCheckable q where
is_empty :: q -> Bool
generalized_selection_sort :: EmptyCheckable (q t) => PriorityQueue q t -> [t] -> [t]
generalized_selection_sort (PQ new push pop) list = List.unfoldr mypop (List.foldr push new list)
where
mypop q | is_empty q = Nothing
mypop q | otherwise = Just (pop q)
{- EXAMPLES: -}
-- priority queue based on lists
priority_queue_1 :: Ord t => ListPriorityQueue t
priority_queue_1 = PQ [] List.insert (\ls -> (head ls, tail ls))
instance EmptyCheckable [t] where
is_empty = List.null
-- priority queue based on sets
priority_queue_2 :: Ord t => PriorityQueue Set.Set t
priority_queue_2 = PQ Set.empty Set.insert Set.deleteFindMin
instance EmptyCheckable (Set.Set t) where
is_empty = Set.null
-- an arbitrary type and a queue specially designed for it
data ABC = A | B | C deriving (Eq, Ord, Show)
-- priority queue based on counting
data PQ3 t = PQ3 Integer Integer Integer
priority_queue_3 :: PriorityQueue PQ3 ABC
priority_queue_3 = PQ new push pop
where
new = (PQ3 0 0 0)
push A (PQ3 a b c) = (PQ3 (a+1) b c)
push B (PQ3 a b c) = (PQ3 a (b+1) c)
push C (PQ3 a b c) = (PQ3 a b (c+1))
pop (PQ3 0 0 0) = undefined
pop (PQ3 0 0 c) = (C, (PQ3 0 0 (c-1)))
pop (PQ3 0 b c) = (B, (PQ3 0 (b-1) c))
pop (PQ3 a b c) = (A, (PQ3 (a-1) b c))
instance EmptyCheckable (PQ3 t) where
is_empty (PQ3 0 0 0) = True
is_empty _ = False
{- MAIN: -}
main :: IO ()
main = do
print $ list_selection_sort priority_queue_1 [2, 3, 1]
-- print $ list_selection_sort priority_queue_2 [2, 3, 1] -- fail
-- print $ list_selection_sort priority_queue_3 [B, C, A] -- fail
print $ abstract_selection_sort (APQ priority_queue_1) [B, C, A] -- APQ hides the queue
print $ abstract_selection_sort (APQ priority_queue_2) [B, C, A] -- behind the layer of abstraction
-- print $ abstract_selection_sort (APQ priority_queue_3) [B, C, A] -- fail
print $ generalized_selection_sort priority_queue_1 [2, 3, 1]
print $ generalized_selection_sort priority_queue_2 [B, C, A]
print $ generalized_selection_sort priority_queue_3 [B, C, A]-- power of generalization
-- fail
-- print $ let f q = (list_selection_sort q [2,3,1], list_selection_sort q [B,C,A])
-- in f priority_queue_1
-- power of abstraction (rank-n-types actually, but never mind)
print $ let f q = (abstract_selection_sort q [2,3,1], abstract_selection_sort q [B,C,A])
in f (APQ priority_queue_1)
-- fail
-- print $ let f q = (generalized_selection_sort q [2,3,1], generalized_selection_sort q [B,C,A])
-- in f priority_queue_1
The code is also available via pastebin.
Worth noticing are the existential types. As #lukstafi already pointed out, abstraction is similar to existential quantifier and generalization is similar to universal quantifier.
Observe that there is a non-trivial connection between the fact that ∀x.P(x) implies ∃x.P(x) (in a non-empty universe), and that there rarely is a generalization without abstraction (even c++-like overloaded functions form a kind of abstraction in some sense).
Credits:
Portal cake by Solo.
Dessert table by djttwo.
The symbol is the cake icon from material.io.
I'm going to use some examples to describe generalisation and abstraction, and I'm going to refer to this article.
To my knowledge, there is no official source for the definition of abstraction and generalisation in the programming domain (Wikipedia is probably the closest you'll get to an official definition in my opinion), so I've instead used an article which I deem credible.
Generalization
The article states that:
"The concept of generalization in OOP means that an object encapsulates
common state and behavior for a category of objects."
So for example, if you apply generalisation to shapes, then the common properties for all types of shape are area and perimeter.
Hence a generalised shape (e.g. Shape) and specialisations of it (e.g. a Circle), can be represented in classes as follows (note that this image has been taken from the aforementioned article)
Similarly, if you were working in the domain of jet aircraft, you could have a Jet as a generalisation, which would have a wingspan property. A specialisation of a Jet could be a FighterJet, which would inherit the wingspan property and would have its own property unique to fighter jets e.g. NumberOfMissiles.
Abstraction
The article defines abstraction as:
"the process of identifying common patterns that have systematic
variations; an abstraction represents the common pattern and provides
a means for specifying which variation to use" (Richard Gabriel)"
In the domain of programming:
An abstract class is a parent class that allows inheritance but can
never be instantiated.
Hence in the example given in the Generalization section above, a Shape is abstract as:
In the real world, you never calculate the area or perimeter of a
generic shape, you must know what kind of geometric shape you have
because each shape (eg. square, circle, rectangle, etc.) has its own
area and perimeter formulas.
However, as well as being abstract a shape is also a generalisation (because it "encapsulates common state and behavior for a category of objects" where in this case the objects are shapes).
Going back to the example I gave about Jets and FighterJets, a Jet is not abstract as a concrete instance of a Jet is feasible, as one can exist in the real world, unlike a shape i.e. in the real world you cant hold a shape you hold an instance of a shape e.g. a cube. So in the aircraft example, a Jet is not abstract, it is a generalisation as it is possible to have a "concrete" instance of a jet.
Not addressing credible / official source: an example in Scala
Having "Abstraction"
trait AbstractContainer[E] { val value: E }
object StringContainer extends AbstractContainer[String] {
val value: String = "Unflexible"
}
class IntContainer(val value: Int = 6) extends AbstractContainer[Int]
val stringContainer = new AbstractContainer[String] {
val value = "Any string"
}
and "Generalization"
def specialized(c: StringContainer.type) =
println("It's a StringContainer: " + c.value)
def slightlyGeneralized(s: AbstractContainer[String]) =
println("It's a String container: " + s.value)
import scala.reflect.{ classTag, ClassTag }
def generalized[E: ClassTag](a: AbstractContainer[E]) =
println(s"It's a ${classTag[E].toString()} container: ${a.value}")
import scala.language.reflectiveCalls
def evenMoreGeneral(d: { def detail: Any }) =
println("It's something detailed: " + d.detail)
executing
specialized(StringContainer)
slightlyGeneralized(stringContainer)
generalized(new IntContainer(12))
evenMoreGeneral(new { val detail = 3.141 })
leads to
It's a StringContainer: Unflexible
It's a String container: Any string
It's a Int container: 12
It's something detailed: 3.141
Abstraction
Abstraction is specifying the framework and hiding the implementation level information. Concreteness will be built on top of the abstraction. It gives you a blueprint to follow to while implementing the details. Abstraction reduces the complexity by hiding low level details.
Example: A wire frame model of a car.
Generalization
Generalization uses a “is-a” relationship from a specialization to the generalization class. Common structure and behaviour are used from the specializtion to the generalized class. At a very broader level you can understand this as inheritance. Why I take the term inheritance is, you can relate this term very well. Generalization is also called a “Is-a” relationship.
Example: Consider there exists a class named Person. A student is a person. A faculty is a person. Therefore here the relationship between student and person, similarly faculty and person is generalization.
I'd like to offer an answer for the greatest possible audience, hence I use the Lingua Franca of the web, Javascript.
Let's start with an ordinary piece of imperative code:
// some data
const xs = [1,2,3];
// ugly global state
const acc = [];
// apply the algorithm to the data
for (let i = 0; i < xs.length; i++) {
acc[i] = xs[i] * xs[i];
}
console.log(acc); // yields [1, 4, 9]
In the next step I introduce the most important abstraction in programming - functions. Functions abstract over expressions:
// API
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x]; // weird square function to keep the example simple
// some data
const xs = [1,2,3];
// applying
console.log(
foldr(x => acc => concat(sqr_(x)) (acc)) ([]) (xs) // [1, 4, 9]
)
As you can see a lot of implementation details are abstracted away. Abstraction means the suppression of details.
Another abstraction step...
// API
const comp = (f, g) => x => f(g(x));
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
// some data
const xs = [1,2,3];
// applying
console.log(
foldr(comp(concat, sqr_)) ([]) (xs) // [1, 4, 9]
);
And another one:
// API
const concatMap = f => foldr(comp(concat, f)) ([]);
const comp = (f, g) => x => f(g(x));
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
// some data
const xs = [1,2,3];
// applying
console.log(
concatMap(sqr_) (xs) // [1, 4, 9]
);
The underlying principle should now be clear. I'm still dissatisfied with concatMap though, because it only works with Arrays. I want it to work with every data type that is foldable:
// API
const concatMap = foldr => f => foldr(comp(concat, f)) ([]);
const concat = xs => ys => xs.concat(ys);
const sqr_ = x => [x * x];
const comp = (f, g) => x => f(g(x));
// Array
const xs = [1, 2, 3];
const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
// Option (another foldable data type)
const None = r => f => r;
const Some = x => r => f => f(x);
const foldOption = f => acc => tx => tx(acc) (x => f(x) (acc));
// applying
console.log(
concatMap(foldr) (sqr_) (xs), // [1, 4, 9]
concatMap(foldOption) (sqr_) (Some(3)), // [9]
concatMap(foldOption) (sqr_) (None) // []
);
I broadened the application of concatMap to encompass a larger domain of data types, nameley all foldable datatypes. Generalization emphasizes the commonalities between different types, (or rather objects, entities).
I achieved this by means of dictionary passing (concatMap's additional argument in my example). Now it is somewhat annoying to pass these type dicts around throughout your code. Hence the Haskell folks introduced type classes to, ...um, abstract over type dicts:
concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap (\x -> [x * x]) ([1,2,3]) -- yields [1, 4, 9]
concatMap (\x -> [x * x]) (Just 3) -- yields [9]
concatMap (\x -> [x * x]) (Nothing) -- yields []
So Haskell's generic concatMap benefits from both, abstraction and generalization.
Let me explain in the simplest manner possible.
"All pretty girls are female." is an abstraction.
"All pretty girls put on make-up." is a generalization.
Abstraction is usually about reducing complexity by eliminating unnecessary details. For example, an abstract class in OOP is a parent class that contains common features of its children but does not specify the exact functionality.
Generalization does not necessarily require to avoid details but rather to have some mechanism to allow for applying the same function to different argument. For instance, polymorphic types in functional programming languages allow you not to bother about the arguments, rather focus on the operation of the function. Similarly, in java you can have generic type which is an "umbrella" to all types while the function is the same.
Here is a more general (pun intended) description.
abstraction operation
changes the representation of an entity by hiding/reducing its properties that are not necessary for the desired conceptualization
has an inherent information loss, which makes it less flexible but more conclusive
generalization operation
doesn't change the representation of an entity, but defines similarities between entities of different kind
applies knowledge previously acquired to unseen circumstances or extends that knowledge beyond the scope of the original problem (knowledge transfer)
can be seen as a hypothesis that a set of entities of different kind have similar properties and will behave consistently when applied in a certain way
has no inherent information loss, which makes it more flexible but less conclusive (more error-prone)
Both operations reduce complexity either by hiding details or by reducing entities that perform similar functions to a single construct.