There's a Quora question about abstraction that has an answer by Panicz Godek that I really like.
It's been on my mind since reading it. "An abstraction is a thing with holes in it" often seems like a useful way to think about it. It is better to read the Quora answer than this post.
Anyway the first example in the answer uses a pair of "find the differences" pictures, and then a third "abstraction" picture that is the same as the two others where those two pictures are equal but has "holes" where the two pictures are different. I've made my own example pictures for this post. They're bad but simple:
Each of the pictures has like four shapes arranged in a two by two grid. The upper left and lower right shapes are the same in both pictures, but the other two are different. Here's a third picture that's the same as the two previous ones but with "holes" where those two have different shapes:
So we can think of this last one as an abstraction that we can use to create either of the first two pictures (as well as other pictures) by filling in the holes.
It's a pretty visual example and I tend to think about it if stuff like "the nature of abstraction" or something is brought up. It aligns pretty good with how I think about functions and abstraction in programming, and it frames or highlights certain things I'm interested in in ways I like.
In something like lambda calculus, we can think of bound variables as holes that can be filled in. If we have, say, a function with a function in it:
λa.λb.a b aThen with a couple of function applications:
(λa.λb.a b a) foo barWe can like fill in those holes/variable references:
foo bar fooIn many programming languages, we can think of at least some of the function calls in the same way, and that can be a useful mental model. See also SICP: The Substitution Model for Procedure Application. In Lua we can do stuff like:
local function f(a, b)
return a .. " " .. b .. " " .. a
end
print(f("foo", "bar"))Here's a piece of code that calculates and prints the greatest common divisor of 24 and 54:
local a, b = 24, 54
while b ~= 0 do
local new_a = b
b = a % b
a = new_a
end
print(a)Or as a function that returns the number instead of just a piece of code that prints it:
function gcd_of_24_and_54()
local a, b = 24, 54
while b ~= 0 do
local new_a = b
b = a % b
a = new_a
end
return a
endNow we can uh, calculate it several times:
print(gcd_of_24_and_54())
print(gcd_of_24_and_54())
print(gcd_of_24_and_54())
print(gcd_of_24_and_54())
print(gcd_of_24_and_54())I dunno it get's less exciting after maybe the third time?
And then like, here's a greatest common divisor abstraction:
function gcd(x, y)
local a, b = x, y
while b ~= 0 do
local new_a = b
b = a % b
a = new_a
end
return a
endWe can apply it to 24 and 54 to get the same number that we got with the other stuff:
print(gcd(24, 54))And we can apply it to some random numbers:
for _ = 1, 10 do
local a, b = math.random(2, 1000), math.random(2, 1000)
print("gcd(" .. a .. ", " .. b .. ") = " .. gcd(a, b))
endI think this is similar to the "find the differences" pictures: The function body is a thing with x and y holes in it, and we can put numbers in those by applying the function to arguments.
I dunno it just seems like so much of the talk about things being abstract and general and stuff is like, oh, abstract? You mean like AbstractRefreshableConfigApplicationContext? Like there's all this nonsense that passes as abstract and general that's like, mostly just a fucking mess, and, I dunno. I might be wrong and all, but I kind of get the impression that people frequently end up thinking that abstract mostly means that things are very unclear and hard to figure out.
The gcd function is an abstraction. It is abstract: It is not concretely the number that is the GCD of 24 and 54 and it is not concretely the number that is the GCD of any two other concrete and specific numbers. It's an abstraction that has two holes in it: You can get a concrete GCD by putting two numbers into those holes, but on its own it is more abstract than a concrete GCD number.
That tends to be what I mean with "abstraction" in programming. It's a particularly good fit for pure and purely functional functions and immutable data and stuff, but I think you can view some other stuff kind of like that too. We're maybe filling in some method-holes by extending an "abstract class." Is a Spring Boot application a massive thing with thousands of holes for annotations and other weird shit? Sure, why not? Also: Why?
In my experience, good and reusable abstractions tend to be closer to the pure function stuff. I think it's good to aim for having it be as clear and obvious as possible what the holes are, how to fill them with stuff and what that results in. (There's no "A is better than B" intended here, but possibly a little bit of "I have found A to be better than B for building this paricular kind of thing.")
So then, uh. I don't think abstract means or implies imprecise. I think the gcd function is very precise. It is more abstract and I suppose more indirect than like gcd_of_24_and_54: There's an extra step where you have to supply the argument numbers, but I don't really think that results in any vagueness or anything. I think that a good abstraction kind of emphasizes both what things have in common and what is different between them, the holes being for differences and the rest being common. (Many attempts at abstraction go off the rails and it often seems to be because someone's trying to force things to have more in common than they do and smooth over their differences.)
That's kind of it. Abstraction can be this clear and precise and not very nebulous thing. And then if we mean something along these lines with abstraction, then making things "more abstract" doesn't like naturally lead to like an enterprise framework or anything. (Building an enterprise framework would then be more about making bad design decisions than about abstraction.)
The remainder is just functions that make pictures because we started out with find the difference pictures:
If we can return values that represent pictures, we can make function abstractions that are very much like the "picture abstraction" above.
So like, assuming we have some shapes:
right_triangle = '<polygon fill="currentColor" points="0,0 1,0 0,1" />'
pointy_triangle = '<polygon fill="currentColor" points="0,1 0.5,0 1,1" />'
star =
'<polygon fill="currentColor" points=' ..
'"0,0.4 0.3,0.4 0.5,0 0.7,0.4 1,0.4 0.8,0.6 1,1 0.5,0.8 0,1 0.2,0.6" />'
wrect = '<rect fill="currentColor" x="0" y="0.2" width="1" height="0.6" />'
hrect = '<rect fill="currentColor" x="0.2" y="0" width="0.6" height="1" />'
circle = '<circle fill="currentColor" cx="0.5" cy="0.5" r="0.5" />'
hole =
'<rect fill="none" stroke="currentColor" '
.. 'stroke-dasharray="4 4" vector-effect="non-scaling-stroke"'
.. 'x="0" y="0" width="1" height="1" />'And also a couple of helper functions for SVG stuff:
function svg(w, h, ...)
local res = { '<svg width="', w, '" height="', h, '">' }
for _, s in ipairs({...}) do res[#res + 1] = s end
res[#res + 1] = '</svg>'
return table.concat(res)
end
function g(x, y, w, h, ...)
local res = {
'<g transform="translate(', x, ' ', y, ') ',
'scale(', w, ' ', h, ')">'
}
for _, s in ipairs({...}) do res[#res + 1] = s end
res[#res + 1] = '</g>'
return table.concat(res)
endWe can implement the find the differences picture abstraction as a function:
function picture_abstraction(a, b)
return svg(
200, 200,
g(5, 5, 90, 90, wrect),
g(105, 5, 90, 90, a),
g(5, 105, 90, 90, b),
g(105, 105, 90, 90, hrect))
endAnd make the first picture:
web.html(picture_abstraction(right_triangle, pointy_triangle))And the second picture:
web.html(picture_abstraction(circle, star))And also other pictures:
web.html(picture_abstraction(star, right_triangle))
print("...")
web.html(picture_abstraction(hole, hole))Is that nice? I'm at least going to claim that it is, on a techincality, an illustrative example.