Why Functional Programming
We can generalise this idea of being forced to handle the failure cases by saying that Haskell makes us write total functions rather than partial functions. This means that we have to handle the entire input domain rather than only part of the input domain, otherwise the compiler will complain at us and, sometimes, point-blank refuse to give us a programme. https://www.gtf.io/musings/why-haskell
FP and Dependency Rejection
“Dependencies are, by nature, impure. They're either non-deterministic, have side-effects, or both. Pure functions can't call impure functions (because that would make them impure as well), so pure functions can't have dependencies. Functional programming must reject the notion of dependencies. Obviously, software is only useful with impure behaviour, so instead of injecting dependencies, functional programs must be composed in impure contexts. Impure functions can call pure functions, so at the boundary, an application must gather impure data, and use it to call pure functions. This automatically leads to the ports and adapters architecture.”
“A common pattern for Haskell: First, gather data from impure sources. Second, pass pure data to pure functions. Third, take the pure output from the pure functions, and do something impure with it.”
Great article describing why dependency injection usually makes functions impure due to impure dependencies.
Aris Goudouras
Lists advantages in the end of this video. Effect polymorphism is a powerful idea.
Declarative vs Imperative / Global vs Local / Denotational vs Operational Approach in Problem Solving
The way I understand functional/reactive programming is that it treats all input as it were pre-existing. So, instead of thinking of: okay, now the user moved the mouse or clicked or pressed a key and we have to react to it and have handlers for it and so on, we think like this: okay, here’s a list of all actions that the user will do, so it sort of says “I know everything about the universe, I know all the future”, this is the list of mouse clicks from now till the program terminates, this is the list of keys that the user will press, okay, here is a list of input from the network that will come, okay? Here, go and calculate something for me. And you just go and take this list and say: okay, I will apply a function to a list like `fmap`, or I can do a fold on this list, or I can merge two lists, stuff like this - you just work on the whole list as if it existed. Of course it doesn’t exist at the moment the program starts running but it doesn’t matter. It’s like, if you tell it ‘’do something for every digit of pi”, does it have every digit of pi? […] it doesn’t have everything at the same time. […] in a lazy language like Haskell we don’t care that we don’t have the whole list. Conceptually we do have the whole list. When time comes to get the next element, well maybe we’ll block / the system will block waiting for user input, but we don’t care: we just have a list and operate on the whole list […]. Bartosz Milewski, https://youtu.be/3XTQSx1A3x8?si=VXcQVdOJ6koJ16oa&t=1904
Don’t think about how things are implemented in hardware. What computers are is just one of the many models of computation. We shouldn’t get attached to it. You can perform computations in your mind, or with pen and paper. The physical substrate is irrelevant to the idea of programming. Milewski, The Dao of Functional Programming, p. 1
[…] when we write code in what now I’m calling programming only language – okay, what we’ll call programming language, I’m now calling a programming-only language, meaning it can only talk about the surface and only talk about the implementation, so it misses the heart of the matter and the reusability. I think that the purpose of my code is to take some data and transform it into other data. That’s, of course, a functional perspective, but even when you’re doing imperative programming, you’re still doing that just in a sequential way. Start with some data, you end up with some data. But I don’t think that’s really what computation is about. I think that’s what the surface layer is about. I think that’s what the executable is about. But the whole process of why am I generating this computation and why am I happy with it, or should I be happy with it, and if so, why, that is not about transforming. The purposeful computation, or meaningful computation, is not about transforming data into other data. It’s about transforming meaningful data into other meaningful data and doing it meaningfully. And what I mean by that is that when I write code to do, say, binary addition, there’s a reason for that code. It is not just because I have a vector of bits and a vector of bits. It’s because I have a particular interpretation of that vector of bits. I have a particular way in which I’m interpreting that vector of bits as a natural number. So my job in constructing computation is not to transform data to data. It’s to transform, in a way, an interpretation. So there’s a type of data, and there’s a type of its meaning, and then there’s a function from the formative ladder, from the representations to the meanings. That’s the domain. That’s what I’m transforming. That’s what I’m taking in, is meaningful data, is the data type interpreted in a particular way. And that’s what my computation is generating. That’s the whole thing. That’s the higher-dimensional thing that has the purposefulness in it and has the ability to then have the logical proof in it as well. When I’m just reflecting, I look back, and when I’m programming, of course, I have an interpretation in mind. Everybody has an interpretation of mine, but what often happens is maybe I’m working with you and we have different interpretations in mind and our programming language cannot capture either one of those. And so it cannot notice the difference. You write your code. I write my code. They’re both correct. We put them together. It’s not correct. Why not? Because we didn’t say what we meant. We said, “I’m transferring bit vectors to bit vectors. […] So our programs miss their purpose, and the languages cannot capture that purpose, and therefore they cannot support us in fulfilling it. Especially we, who like typed languages, for instance, appreciate that the program is helping us, supporting us in fulfilling our intentions. https://haskell.foundation/podcast/62/ (0:52:06)
[…] there's a mental bottleneck which is the von Neumann paradigm. The von Neumann style in the in the title of this during award lecture is exactly this. And it forces to think small. We cannot think clearly and profoundly and deeply and gain insight within the von Neumann model of programming. The von Neumann programming is still the dominant model of programming. Operational semantics, which is mainly what PL people are taught is essentially mirrors this weakness. it's an essentially sequential and mechanistic way of thinking which is not rooted in beauty. It is sort of fundamentally sequential, whereas computation isn't. If if you think it is, it's because you've been programmed right with this deep misunderstanding. […] So anyway, this turning a word lecture for me is really about breaking out of the Von Neumann bottleneck, the spiritual, mental, cognitive, right, philosophical bottleneck, and and and getting back into this programming. https://open.spotify.com/episode/4jPTxGfaUa84IqsQceKywL (1:04:55)
There's one meta idea that applies, I think, to everything everybody should ever do. Certainly applies to everything I do, which is you should understand what you're talking about in the simplest, most elegant compositional terms. That is denotational semantics. That's what I call giving, having a denotation. So all your types have a mathematical model. It's simple and of course it has to be precise. Otherwise simplicity is an illusion. OK, so it's as simple as it can possibly be. And then every operation that you provide can be fully explained in terms of that model and that model has nothing to do with the implementation at all. OK, so so it it it it's a very severe discipline that forces you to to think in the clearest well, it encourages you to think in the clearest possible way about what you're trying to say, independent of any bias about implementation. That's very hard for people who have been conditioned to think that programming is about making machines do stuff. Because any anything. You know, if you think computation and if you've been taught operational semantics and operational thinking, and like, like linear algebra is about arrays and machine learning is about neural Nets, all these like arbitrary bad choices about particular representation, or programming is about sequences of effects, all these harmful ways to look at things. https://open.spotify.com/episode/4jPTxGfaUa84IqsQceKywL?context=spotify%3Ashow%3A1gE06aXNjm4BEcwbsow0Zu (1:56:43)
Yeah, so so I thought I got to think about 3D graphics in a a functional way, a compositional way. A way of what it means, not how it not what it does. OK. And that's the difference. The graphic systems are almost always implemented the way almost everything else is implemented. But especially graphics it seems is is if they are a graphics program is a sequence of commands, maybe structured, you know sequence of commands, the words like you might have loops and subroutines, what they used to call them, right. So procedure definition, so on. But they're essentially just ways of of organizing sequences of instructions that have side effects, right? But instead of the system memory, they're going to be the video memory, right? And the pattern that we want in memory after we're done as an emergent effect is one that looks like something to you like some geometry or something like that. All right. But that model is a terrible model if what you care about is clarity. Because it's extremely hard to reason about in exactly the way John [Backus] has said that that because of imperative programs, that is programs that are about sequences of effects and which have no nice mathematical properties, therefore they cannot give you any powerful insights. So what I did instead is: I said OK, what are graphics programs for? In other words, what are the meanings they are trying to convey? So I want to make a graphics program. It's because I want to help somebody look through a tool at something they are interested in. So I'm trying to invent a telescope for something. What is that something? And that's the important thing. And the secondary thing is how can I make a device that helps you look through it? How can I use a computer? How can I make a device you can look through to see what you want to see and then how can I make a language describe what you want to see? That's very different from the dominant design paradigm, which is how can I make a language or a library so that you can describe a sequence of effects right, that emerge that have this, you know eventually, you know that cause a billion effects whose whose overall aggregate result is to draw a picture, right? Because it because it messes up these two things. The essence of what it is that the the idea you're trying to describe and give somebody a a a opportunity to look at and then how the machine works and what are the kind of physics in a way what are the effects. So I divided those two things and instead of a language of rendering or even of affecting a data structure that would get rendered I designed a language with a library of vocabulary, typed, composable vocabulary of geometry and colors. I mean it's like essentially what Euclid did within a modern a more modern vocabulary, a more modern linguistic framework, right? So a collection of types and typed operations on three-dimensional things. […] https://open.spotify.com/episode/4jPTxGfaUa84IqsQceKywL?si=168fed48e7fe47fd (1:32:19)
Denotational Design Principles
So in denotational design, you say for most important thing is, what do I want to talk about? Ok, I'm going to talk about these linear maps. And then the second question is, what is a language? What's a vocabulary in which I can express these things? I can build them up. I combine them, build them out of nothing, all these things; it's these operations. What algebraic abstraction already exists? And we've already proven things. We've already learned it for some other reason. Then the next step is now we're going to say, OK, so then now I kind of understand the mathematical realm, the specification. That's the story I'm going to tell the user. Now what about the implementer? What I'm going to tell the implementer, which may be me, maybe somebody else. What I'm going to tell them is I want you to, I want you to find a way of implementing the first story. And there are two kinds of choices to make. One is the representation you're going to use for the things of interest. And the other is how you're going to implement the operations. Which operations? The one that the first model already wants to talk about. Now, how do you know how to implement them correctly? Well, there's two questions. What does it mean to be correct? And then how do you get there? What it means to be correct is always the same. It's a homomorphism. OK, So what homomorphism you have to talk about? How does the representation mean the things we're talking about? if you don't know what the representation means, you can't possibly address the question of how to implement operations on them because it's only the meaning that guides them. You have to know what the up the arguments mean and what the operation means.
Aris Goudouras
What is interesting here is how does it compare to Domain Driven Design. There, again, we need a shared vocabulary, but it’s closer to how the domain expert understands the domain. Here Elliott argues for a vocabulary that’s in the mathematical realm. Maybe in the cases he has in mind the language of mathematics is indeed the language of domain experts. But if not, we do have an important decision point. Do we express the problem in the language of mathematics & categories or using a vocabulary that’s closer to how domain experts communicate things?
Aris Goudouras
Milewski makes an interesting association between the declarative vs imperative way of thinking and the global vs local way of attacking problems in physics, as described by Feynman.
Aris Goudouras
Conal Elliott refers to a John Backus lecture (his Turing award lecture and the corresponding paper) where we renounces imperative (Von Neumann) style programming and argues for functional programming (or something close to it), although he received the award because of his work in Fortran - an imperative language. Elliott also says that Von Neumann’s style was mean to be a shortcut / experiment for pragmatic reasons and then we got stuck with it as the dominant paradigm. (starts around 54:28)
Aris Goudouras
Interesting discussion on programming paradigms.
Mental models and thinking in Functional Programming
There is one area in which knowing something about category theory actually changes the way you program in Haskell. I think that’s once you learn about algebras and coalgebras, because that gives you this level of abstraction in which you stop doing things using recursion. The first big problem when people learn Haskell or other functional language is recursion, right? Especially when you come from C++ or traditional languages, right? You always write loops, right? And you think in terms of loops, I have to iterate, iterate, right? How do I iterate in Haskell? Well, you do recursion, but recursion is kind of like a weird way of thinking when you have to sort of assume that you already solved a problem and then you have to do one more step to finish it. And it’s not natural. In iteration, you just do one step at a time, and then next step, and then next step, and then step. In recursion, you have to assume that you’ve done it already, right? I haven’t done it yet, but let’s assume that I’ve done it and now I need one more final step, right? So this is hard. But then when you learn about algebras and things like catamorphisms, anamorphisms, all this stuff, you suddenly think, “Oh, I don’t have to think recursively. I can think holistically.” It’s a data structure. I’m looking at this whole data structure and I’m applying a transformation to this whole data structure all at once. And this is so powerful. Bartosz Milewski, https://haskell.foundation/podcast/30/, 27:14
However, much in the same way that music is not a set of instruments, functional programming is not a set of abstractions that we need to learn and memorize. Functional programming is an approach to solving computational problems. Many of the abstractions that you do read about are ways to apply this approach to new problems, or problems that were difficult to solve without reaching for more traditional programming methods. But the essence, the core, of what functional programming is about is mostly unchanged. In the preface to the book Burge writes (emphasis mine): "The main emphasis [of this book] is placed on those parts of the language, namely expressions, that denote the end results sought from the computer, rather than on the instructions which the machine must follow in order to achieve the results. The main thesis of this book is that, in many cases, this emphasis on expressions as opposed to mechanisms simplifies and improves the task of programming.” The above will be familiar to anyone who has had the good fortune of taking a well designed functional programming course. This is the paradigm shift that sometimes makes functional programming hard to learn for those that are used to other methods of programming. Expressions over mechanisms. Jose Calderon, http://jmct.cc/intro/index.html
Even if you are using functions/methods that are usually met in the functional programming paradigm, if you think about the implementation details (e.g. mentally iterating over arrays or worrying about how map executes under the hood), you are in the imperative mindset. With functional programming and a declarative mindset, the focus should be on the intent of the operation applied.
Aris Goudouras
Importantly, it’s not that in functional programming we don’t deal with mechanisms or low-level constructs. It’s the emphasis that changes in how we attack the problem (from the instructions to the end results) that makes the difference.
Aris Goudouras
Interestingly, Burge’s and Calderon’s perspective is much different than Milewski’s, who focuses on the abstractions and category theory part.
Aris Goudouras
I think Burge (and Calderon) here endorse the declarative style manifested in the expressions. But I don’t really understand at this point why FP expressions are declarative in FP (especially with ones like Do notation, we really have the opposite).
Aris Goudouras
Haven’t read this yet but seems relevant