CoRecursive #027

Abstraction and Learning

With Runar Bjarnason

Abstraction and Learning

What is abstraction? Can we have a precise definition of abstraction that, once understood, makes writing software simpler?

Runar has thought a lot about abstraction and how we can choose the proper level for the software we write.

In this interview, he explains these concepts using examples from the real world. Examples include SQL, effectful computing and several others areas.

We also talk about how to learn and acquire the skills necessary to understand complex concepts. Concepts like highly polymorphic code and category theory.

Runar also explains his latest project unison computing and how it uses the correct level of abstraction to rethink several foundation ideas in software development.

Transcript

Note: This podcast is designed to be heard. If you are able, we strongly encourage you to listen to the audio, which includes emphasis that's not on the page

Size-Shaped Holes in Everything

Adam: Hello, this is Adam Gordon Bell. Join me as I learn about building software. This is called CoRecursive.

Runar: Yes, it’s a sort of mind-body integration thing. I think it’s just like learning to dance, or play the guitar or whatever, you can’t really know how to play the guitar abstractly, and no one can really just tell you how to play the guitar. You got to do it.

Adam: That was Runar Bjarnason. He co-wrote Functional Programming in Scala, a book that was very influential for me, and for a number of people I know. But we aren’t talking about that today. Actually go back to episode five for that interview. Today, we talk about abstruction and precision, and also the relationship between constraints and freedom. My take on Runer’s ideas is that when writing software, if you constrain yourself to the least powerful abstraction that will work, then you will have more freedom to compose things at a higher level.

Runer, I interviewed you probably more than a year ago for the podcast, and we talked a lot about the basics of functional programming, from your book Functional Programming in Scala, that I guess you co-wrote. And a little bit at the end, we started talking about abstraction. So thanks for coming back. And I was hoping we could dig in more on that topic.

Runar: Yeah, my pleasure. Absolutely.

What Does Abstraction Actually Mean?

Adam: So in broad sense, what is abstraction?

Runar: What is abstraction? Abstraction is a sort of mental process that you go through all the time, like it’s the essential function of the human mind. And it’s just like taking a bunch of things that we have observed or encountered or experienced in some way, and then grouping them by their similarities, but in a way that they’re different. Like grouping them by like a specific way in which they differ.

Adam: Do you have an example?

Runar: Do you have an example? Well, like for instance, size. We can have an idea of size, that’s an abstract idea, so we’ll have a bunch of things and we’ll note that they’re all different in some particular way, and we’ll call that way size. But with regard to this idea, we’re going to regard them as the same in every other respect. So is this way of constructing sort of a new entity in our minds, that we can manipulate and sort of have, and that we can give it a name, and that refers to this particular difference among things. And then we’re going to then basically regard those things as being the same in every other respect.

Adam: So it’s like, if I have like an apple and a car, when I talk about their size abstractly, I’m not worrying about their caloric value, but merely that an apple is smaller than a car?

Runar: Right? So they have different size, but in every other respect with regard to this particular idea of their size, they are incomparable, or the same. And you can start with a really imprecise idea. Like, you can just say, “Well, the Apple is small, and the car is big.” So they’re only two sizes, big and small. But then as we get more and more abstract ideas, we can start to be very precise about how we compare, for instance, sizes, or colors or shapes, or programs.

Adam: Yeah. You used the word precision there. Actually, maybe before we get to precision, so how does this apply to the world of actual software development or code.

Runar: So the way I’m sort of looking at this in my mind, is that we’re viewing the things that we observe out there in the real world. We’re sort of looking at them and sort of lining them all up in some way, in such a way that we sort of punch a hole through the whole stack of things that we’re integrating under some abstraction. And then we’re going to sort of fill that hole with some quantity or quality, like for instance, with size, we’re going to just line everything up so it has a size shaped hole in it, and then we’re going to fill that with either big, small, or like an integer or square meters, or whatever it is. And sort of viewing it as like a stack of things that with like a hole punched through it that we can then fill with some other idea.

And then the same kind of thing happens in programming when we abstract. That is we take a program, and we want the program to vary in some way. Like for instance, we might want to take the sum of the numbers one, two, and three, but that’s not very abstract. So we might want to be able to take the sum of any numbers, and so we sort of punch a hole in our program, and say, “Well, we’re going to actually take those things as an argument. We’re going to call the numbers like X, Y, and Z.” And so we have number shaped holes in our program that we’re then later going to fill with some values.

Why X Plus Y Beats One Plus Two

Adam: So like variables are a form of obstruction. Is that the example?

Runar: Yeah, exactly. So a variable, you’ll note, can’t be absent, you can’t ignore the fact that it needs to have some value. But it could be any value, it stands for absolutely any value of a particular type of value that fits that sort of hole in the shape.

Adam: It’s an interesting example. So like 1+2+3, or let’s just say 1+2 is like a concrete addition, but like X+Y is abstract.

Runar: Mm-hmm (affirmative).

Adam: And I think you said before that somehow that’s more precise? How is X+Y more precise?

Runar: Is that more precise? I think that it’s certainly more abstract.

Adam: Yes.

Runar: 1+2 is already pretty abstract because, like even the idea of like one and two, these are abstract ideas. Like we’re not specifying one of what, or two of what. So these are ideas that work universally for anything that we might want to consider one or two of, right?

Adam: Yeah, like just counting. We’re not counting a specific thing, but just the number itself.

Runar: Right. So that’s an example of where… So just at that level, is an example of when like an abstraction buys us precision. Like out in the world, when we’re like looking at the actual universe, it’s sort of undifferentiated, and it’s just this one big interconnected mess. But we can abstract out like two of something, like we can notice that there’s a pattern. Like, everyone has some configuration of eyes, or legs, or like there might be like a pair of sheep in the field and two stars in the sky or something. And they all have this same relationship, is that there’s like there’s two of them. So then we have this notion of two, and then from there we get all the natural numbers and arithmetic. And now we can start to be very precisely about adding things together and subdividing groups of things. So we can do very precise arithmetic now with these abstract quantities. Whereas, like with actual stars and sheep and eyes, we can’t really.

Adam: Yeah, and if we talking precisely about eyes, it’s sort of a symbol, like there is no universal eye, right? We’re referring to a whole bunch of different people’s eyes as like a combined concept?

Runar: Right. Yeah, exactly. So even there, that’s abstract.

Adam: So to make it more specific, I feel like I’m going to end up otherwise in a really philosophical place.

Runar: Sure.

Adam: I mean, maybe we will regardless, you had this example of actually writing printer code, and how abstraction was lacking or something. I was wondering if you could share that?

The Printer That Couldn’t Rearrange

Runar: Oh, yeah. So in the talk I was talking about this time when, this is during the ’90s sometime, when somebody wanted to hire… Somebody hired me to write a program to make sales reports, and they had to be printed on this dot matrix printer that they had in their office, and they had their data in an SQL table. So I just took their paradox tables, or whatever it was, and I essentially just dumped them out onto the printer using very specialized code, using like the printer control codes. I wasn’t using any kind of abstraction, or anything like that. So talking very concretely to this particular printer. And then when the client comes back and says like, “Okay. Well, I want to manipulate these reports. Can I move this thing to over here?” For instance, or like, “Can we take two reports and put them side by side on a page or something?”

That becomes basically impossible without rewriting my whole program. Because the program isn’t able to talk about the location of things on the page, or whatever, it can only talk about like how the printer is being controlled in this particular instance. So I was missing the sort of abstract layer, it should have been talking about the reports, sort of in the abstract layout and other things like that, like even if I had known that PostScript existed, which I didn’t. So that would have afforded me the ability to talk precisely about where things are on the page and things like that.

Adam: So yeah, PostScript or PDF is like an abstraction that lets you specify like where things would go on a page without being specific to a printer?

Runar: Right. And so yeah, the point I was trying to make with that one was one of compositionality. So I didn’t have a compositional program, so it wasn’t able to deconstruct the program that I had written into smaller elements that I could then rearrange. It was all just one monolithic block of code.

Adam: So how does abstraction relate to composition? If you had written a more abstractly to PostScript, how is that more compositional? Or is it not?

Runar: Yeah. It certainly would have been more compositional, or like any kind of sort of algebraic abstraction, like where you can take things apart and then recombine them would have been better. I mean, will allow me to do what I wanted to do. And I mean, the way that relates is that you can, once things are very abstract, you can sort of manipulate them symbolically, according to some rules. So there will be some rules of composition or combination. For instance like if you have some kind of layout language, you might be able to say, like this thing appears above this other thing, or these two things appear side by side. Like that might be a rule of composition, but then the actual rendering of one element and the rendering of another element, if you combine those renderings together, you should get the same thing as if you are rendering the combined elements. So that makes sense?

Adam: Yeah. It’s almost like an intermediate representation, and then some sort of printer-specific interpreter or something?

Runar: Yeah. The thing that the abstraction buys you is to allow, again, this sort of sum, but any idea, that you can talk about. You have some printer, but it could be any printer, as long as it has a mapping from your abstraction down to how you actually control the printer.

Adam: Yeah. I have this quote from you, it says, “We should work symbolically and only go to a final representation at the latest possible time.” So this, I suppose, is an example of that. We would map things to this small language for representing layout, and then at the latest, like right when we, before we send it to the printer, we kind of interpret that into the printer codes?

Strings Let You Write Broken SQL

Runar: Right. Yeah. Because if you kind of think about it, there will be lots of different ways that you can combine things that are sort of in a very large language. And a favorite example of mine is like SQL and strings, and combining two strings together, even if both of those strings represent some SQL statement, there are going to be lots of different ways of combining strings, and most of those ways are not going to be valid SQL. So if you want to ensure that you have valid SQL at the end, you have to sort of recover the structure of the SQL and then sort of manipulate it in some way, and then recreate or sort of generate the combined string.

But if let’s say you had a symbolic representation of like the SQL syntax as a library, you could then just work using that library and combine your SQL in a way that’s legal, like in a way that is still legal SQL and then generate the string as the sort of last possible step. So you’re not like trying to do some kind of string matching or hacks.

Adam: Yeah. The interesting thing about that is almost necessarily there will be some SQL. If I’m using some intermediate form, that’s much more compositional, like almost necessarily there will be things like types of SQL I can’t do. So I’m limiting myself in using this intermediate form, would you say?

Runar: I’m not sure. I mean, if your intermediate form can represent… If that is like the syntactic specification for SQL, then no, there won’t be any legal SQL that you can’t generate. Maybe I’m misunderstanding your question.

Adam: No, I think that makes sense. Like SQL is large, so in practice, there’s things I may miss. But yeah, if I were able to specify… Like the perfect case is that I have this intermediate representation that lets me compositionally form any possible SQL that I want to do, where I think what you’re saying is that if you have a string representation, that is true, right? You can have any valid SQL, will fit into that. But so will like an infinite amount of things that are not SQL.

Runar: Yeah. Exactly. So it is true that like you can have any combination of valid SQL in your string, you can have lots and lots of junk. But if you have a precise specification, if you have an abstract representation of SQL, then you can see where precision comes in here. You can have precisely SQL and nothing else.

Adam: Yeah.

Runar: So in a way, precision separates things that are sort of distinct, so that we prevent confusion. So it separates the things that we don’t want to have confused. So it avoids additional noise so that there are no unnecessary elements.

Adam: Yeah, so it is. To use some of your language from before, like it is a constraint on what you can do, right? A string can hold more things but the representation that can only represent SQL is much more precise.

When More Abstract Means More Precise

Runar: Right. And this is the same with like every day concept formation, like with the idea of the natural numbers, for instance, with regard to like arithmetic on the natural numbers, the numbers are very precise. And there’s no additional noise, like the fact that those numbers represent guitars or something. Like that’s just additional noise that you don’t want to consider, you don’t need to consider. Like the fact that two guitars are different from some other guitars doesn’t matter, because if you have two have something and then two have something else, you still have four. So with regard to their cardinality, we want to regard them as being the same.

Adam: The place where… This is funny because this is talk about obstruction, I feel like it’s very abstract. I suppose that makes sense.

Runar: Sure.

Adam: The place where I really found a lot of value in this concept, I’m trying to think about how to communicate it well. The place that I found most interesting was like in terms of polymorphism. So if I have a function that takes in an integer, and returns an integer, then that is concrete as compared to if I have one that takes in anything and returns something of the same type. Right?

Runar: Right. So that’s now more abstract.

Adam: So what is that abstraction buy us?

Runar: Well, so it buys you that the… So the caller of the function can now select which type the function should operate on. And because the caller can pick absolutely any type, that means that you have no hope of writing a specialized program that works for every type. So you have to write a general purpose one, and there’s only one that could possibly work. You have to return the element of the type that you’re given. Because you have no idea upfront, when you’re writing that program, you don’t know where you could possibly be getting.

Adam: Which is interesting because it means that the generic version is actually more precise, even though it’s more abstract. Like the possible implementations are just one.

Runar: Right. So it’s a more abstract type, but it’s a more precise specification for a program.

Adam: And because the definition is so limited, the amount of potential callers of it increase. Like there seems to be some relationship there?

Runar: Yeah. There’s definitely a relationship there. That relationship is called adjunction.

Constraints That Set You Free

Adam: Okay. What’s an adjunction? I might regret asking that.

Runar: So adjunction is an idea from category theory. So it’s a relationship between two categories, so it’s a pair of factors, one going from, if you’re going to category C and D, one of them goes from C to D, and the other one goes from D to C. And they stand in this particular relationship such that they are what’s the sort of a layman’s way, there are sort of approximate inverses, or one of them finds the most efficient solution to the other, and the other finds the most difficult problem that the other one can solve. That makes sense?

Adam: They’re inverses. They’re sort of like a [qualities 00:17:16] but they’re in the opposite direction?

Runar: Yeah. So they’re in opposite directions, but they’re not exactly inverses. They’re sort of approximate inverses. That is, if you go across the adjunction in one direction and then back again, you don’t end up where you started. You end up either sort of below where you started or above where you started in some kind of way in the category, like in a hierarchy. Like you’ll be able to either get to where you ended up from where you started, or you’ll be able to get to where you started from where you ended up.

Adam: So in my world of writing software… So I’m constructing a function, so I think what you’re saying is like, the more abstract and precise I write the definition of my function, that allows for more reuse or more potential callers of it?

Runar: Yeah. So you can think of this in terms of variance, for instance. Like if you’re familiar with the way variance works in Scala, like in class hierarchies?

Adam: Mm-hmm (affirmative).

Runar: So like a function is contravariant in its argument. And so as you grow as type the things that you could possibly receive, you make it easier to use. Like you can pass more things to it.

Adam: Yeah. And at the same time, that limits what you can do, because you have to cover the most general case of all.

Runar: Right. Yeah, and conversely, if you shrink the type, you make it more difficult to use. This particular function that we’re talking about, like the identity function, like for all A takes an A and goes to A. You can see that it is giving a lot of freedom to the caller, and basically makes it really easy to call, you can call it with anything, but you can only set that in turn shrinks the things that you can return, you can only return one thing.

Adam: So it can be called by anything, but in fact, it does nothing?

Runar: Right.

Adam: So it is the most extreme example, I guess, right?

Runar: Right, exactly.

Side Effects Shrink Your Audience

Adam: A less extreme example would be like functions in general. So if I am constraining myself to not do side effects, that obviously limits my function, right? Like what I can do to make something a function, if there’s no side effects. However, that means, like more things could call it, I would say.

Runar: Yeah, that’s right. Because if you do have side effects, those side effects may be inappropriate, or impossible in lots of contexts. Like for instance, if you access the network, your function can be called anywhere where the network is unavailable, or where it’s inappropriate or dangerous to access the network.

Adam: Yeah, I’ve actually had this problem with, I think it’s like the Java URL class, where it actually does like a namespace lookup or something.

Runar: That’s a great example. Yeah.

Adam: I hate this problem, right? So you’re like, “What’s going on here? Like, it’s making a network call, why is it doing that?” The fact that it does a network call makes it not usable in a whole bunch of circumstances.

Runar: Exactly.

Adam: So the thing that you really, I guess, opened my eyes to is this back and forth relationship. And I think that like, as I write code, I want it to be reusable, I want composition. And that means that in this relationship, basically, I always want to go as abstract and precise as possible, right?

Runar: Well, I think that depends on your goals, really. Because sometimes going very abstract and very precise might be costly in terms of labor, or computation, or something like that. So I think that you got to weigh it against your other goals as well. So it’s not necessarily always correct to, and by correct I mean it doesn’t always align with your values to go completely abstract and absolutely precise.

Adam: Yeah. So like, here’s an example. Should I use explicit recursion, or does it make more sense to use like a float? To me, it seems like there’s less power in a float.

Runar: I agree with that. And I think that, therefore, you should choose a float. I mean, whenever you can. But often, the float might be very convoluted to do. Like in principle, it should be possible to write any function over a list using a float, but it might be very convoluted, you might have to do some sort of intermediate steps and things like that. And there are lots of algorithms on lists that are just easier to write using recursion. And which one to do, I think, just depends on what your goals are the time.

Adam: Yeah. It depends on the case. But you could be as abstract and precise as you possibly can in that case, I guess. This is the way I’m interpreting what you say like, “If I have a function that… Like the values of it are only going to be whole numbers, like I shouldn’t be returning like a double or a float?”

Runar: Right. Yeah, exactly.

Actors Have No Laws at All

Adam: All right. What do you think is more precise? A like actor based system or like some sort of I/O effect types?

Runar: That’s a hard thing to answer. I think that probably some kind of I/O effect, like a monadic I/O?

Adam: Yeah.

Runar: Yeah. I mean, things that you can do with one you can definitely do with the other. But sort of an I/O monad has an algebra, like it has some laws that you can use to sort of reason about your code. It doesn’t have a lot of laws, but you can for instance, always know that if you break out some part of your program into a sub routine, and call it from your main program, it’s going to execute in the order that you expect, for instance, and other things like that. Whereas, like an actor system has no algebra at all, there are no laws of composition for actors, at least to my knowledge. I mean, there have been systems to formalize the sort of mobile processes, I’m looking at my book here on pi-calculus for instance.

So yeah, there are lots of sort of formalisms for talking about processes that communicate using messages. But we don’t tend to talk about like, when we’re writing those systems, we don’t tend to write them sort of symbolically, in the abstract, like those formalisms do.

Adam: That makes sense. I have a, in a piece of software I worked on, there is like a an actor that does some sort of job execution. Is very basic, but it will take any type of input into its message queue, and then do something with it. So if I were to replace it with some sort of function that takes a specific input, and then returns like I/O unit or something, it seems to me that that is more precise, because I’ve limited its input.

Runar: Oh, yeah. Limiting the input I think is a good first step. But I think that you can go even more abstract than that, maybe not for that particular application. But you could, if you have some kind of network that is exchanging messages over lots of different actors, you could maybe write that symbolically. Like you could have some kind of library that allows you to describe networks of actors talking to each other. And then as the sort of last step, compile that to actual actors.

Adam: Very cool. If I have something that is like of type… Like I just described, like type like I/O unit, or is that actually a function?

Is IO Unit Even a Real Function?

Runar: Is I/O unit an actual function?

Adam: That’s just something that nobody should ever do.

Yeah.

Runar: Think of all the time this is wasted on like, formatting code, like every time I just like, make a new line and hit Tab a couple of times, or Backspace to like, make something a line up like, I die a little bit on the inside.

Adam: Yeah, I agree. And like, there’s like weird things like if I was doing like scheme programming, I really would like to have a way to tell my editor to just like, treat brackets as indents, like Python style, and like, not actually see this, like ending line with like, 1000 ending brackets?

Runar: Yeah, absolutely. And that’s another thing that I think that we’ll be able to do is to have lots of different surface syntaxes for the language. So if you want to know a lot of brackets, and you want to use expressions, you can. I mean, we’re not there yet. We only have one syntax, which is very Haskell like, but that’s only because that’s the aesthetic that we prefer. But there’s no reason that we couldn’t have any number of surface syntaxes for this abstract language very much like Microsoft is doing with C-Sharp and F-Sharp and all of those languages, they’re all under the hood are the same CLR.

Adam: But like even more so because you’re just Scala and Java both run on the JVM, but they’re not the same language. This is much, talking about the presentation could vary, right, but like, the actual semantics would be the same.

Runar: Right? Exactly.

Adam: That is a cool concept. You could get adoption by just having a mode that fits everybody’s weird preferences.

Runar: Yeah. Maybe we could make it easy to have sort of pluggable syntaxes.

Adam: That’s awesome. Well, thank you for your time, Runar. This has been a lot of fun.

Runar: Yeah, likewise. It’s been a blast. Thanks for having me.

Adam: Thank you very much. That’s the show. I hope you liked it. If I’m totally honest, the interview with Runar went in a slightly different direction in the second half than I had planned for, but it ended up super interesting to hear about his new project, to hear about how he likes to learn and approach topics. Just a super interesting person to talk to. If you like the show, tell a friend about it and help spread the word and also maybe join our Slack channel. After the last episode about Scala Native, we had some interesting talks about Big O notation and complexity in the channel. And also user Joe shared a file server open source project he made in F-Sharp, where you can upload files but you get to set kind of a time to live on. Kind of an interesting project exploring functional programming answering files. So yeah, until next time.

Support CoRecursive

Hello,
I make CoRecursive because I love it when someone shares the details behind some project, some bug, or some incident with me.

No other podcast was telling stories quite like I wanted to hear.

Right now this is all done by just me and I love doing it, but it's also exhausting.

Recommending the show to others and contributing to this patreon are the biggest things you can do to help out.

Whatever you can do to help, I truly appreciate it!

Thanks! Adam Gordon Bell

Support The Podcast
Select an episode