Classes are a way of writing higher order functions
stopa.io
stopa.io
Nope, really they are not. Rather, objects are run-time, publicly accessible environments (bindings of names to slots).
Since functions also have environments, we can come up with some contrived use cases whereby a rigorously encapsulated OOP example revolving around a class that is sealed against extension and uses no inheritance or other OOP mechanisms is nicely mimiced by a vector of functions that share the same environment.
But imitating some facets of a thing is not being that thing. A dog standing on hind legs isn't a man.
A more fully featured OOP can be built up, which yet keeps using lexical variables for slots. Public access to slots by name can be implemented by having a standard function supported at the same position in the function table, which takes a symbol argument, and switches on it to map it to a variable in its environment.
Providing possible implementation mechanisms for thing is also not being that thing.
We can easily reverse this, too, and claim that functions are just objects. Compilers for lexical closures do things like flatten the environment into an array in which items are accessed by position. So, first class functions are, under the hood, nothing but code, plus an array, in machine language. And since data structures are also some code offsetting into arrays, it's all the same.
You know, the moment I saw a NAND gate schematic, my mind opened to an amazing revelation: we don't need both zeros and ones, just zeros and NAND gates! The number one is nothing but a zero, duplicated into the two inputs of a NAND gate.
This is not as absurd as you think it is. The most commonly used definition of the natural numbers (https://en.wikipedia.org/wiki/Peano_axioms#Formulation) is structured in a way similar to your description: we have a single explicitly declared natural number (zero), and all other natural numbers are defined as a sequence of function applications on zero (ie 4 is defined as S(S(S(S(zero))))).
Our notation for numbers is just "syntactic sugar" on top of this representation: useful when doing practical work with numbers, but only a hindrance when you're studying the concept of "number" itself.
Analogously, in programming language theory (studying the properties of programming languages moreso than trying to do program things in them), we often prove properties of languages by studying extremely frugal, tiny languages (often called "calculi"), that only have the absolute minimum primitive constructs.
Realizing that a construct can be expressed as "syntactic sugar" on top of a simpler one (objects on top of closures, for instance), is useful in this setting because it reduces the number of cases in proofs.
Those definitions were developed after we had something useful in order to 'codify' it and make it more precise. They are not, in my opinion, a basic blocks that the rest is built from but an added layer of abstraction on top of whathever it is you're defining.
I don't mean I don't understand set theory and the role it has, or advocating for any other system, I absolutely love it.
What I mean is, in practical things (programming languages etc.), it feels a bit counter-productive to me to reduce them to the simplest possible definitions/terms (that have to fit to the particular use case by definition), declaring that a building block and then explaining every other concept in those terms as if it was a proof that we have trully found a foundation and the rest is just a 'syntactic sugar'
edit: You all probably know this famous gif of a ballerina that can rotate to the left or to the right depending on how you think about it. After writing the above post the notion of S(S(S(S(0)))) keeps flipping in my brain between being an abstraction of and simplification of natural numbers. It's weird.
I think that numbers, set theory, programming languages, etc, are all abstractions. Forming abstractions is a mental tool associated with our ability for language: we spot a pattern and we give that pattern a name. Once we've given it a name, we can talk _about_ the abstract concept as a subject of our statements.
Perhaps the simplest form of abstraction we're capable of is "categorization". Ie we see a pointy-eared small animal that makes meowing sounds, we another one, and another one, ... and even though none of them are identical, our brain picks up on certain patterns in these creatures. We know that when we next see one of them, we can expect certain behaviors. So we give it a name, let's say, "cat", and now we can talk about the abstract concept of "cat" without referring to any specific individual cat. To see how big a deal this is, I find it useful to remember that cats aren't explicitly defined in the fabric of the universe, like they would be in a computer program (where we'd have a "Cat" data structure or object, usually), they're just emergent behavior of interacting molecules.
But we can do more, we can also abstract out properties of objects. We can see that some frogs have a similar color to the leaves of most trees, so we can give this leaf-color a name, "green", and talk about the abstract concept of "green" without referring to a specific green thing. You can just say "I like green" (no one will ask you "a green what?"), or "mixing green and blue makes cyan" (no one will ask "mixing green and blue what makes cyan what?").
And then we also have "chunking", where we can talk about sets of objects as objects themselves. So we can talk about "a herd of sheep" as a singular thing. Our ability to abstract out properties of objects also applies to these chunked concepts. One property of a set of objects is "quantity". Speculation ahead. Initially we probably only had fairly vague terms to talk about quantity, perhaps individual words for small quantities (one, two and three, maybe), and after that it's just "many". But it seems we figured out pretty fast that counting sheep, counting apples, or counting pebbles are really in some sense "the same thing", so we might have first learned to count through some unary "notation" even though we had no names for quantities. We'd make carvings into bone or carry a bag of pebbles, one carving or one pebble for each sheep in our herd. Then we notice that certain operations on these quantities are also all "the same": it doesn't matter whether you're adding pebbles, sheep, carvings, ... you're always doing the same thing. So after a while we start using these operations to define bigger numbers (the French still call the number 80 "quatre-vingts" or "four twenties"). Now all numbers become nameable, and eventually we streamline and standardize these names.
So now we have a name for every number, and we can talk about "thirty-two" without getting back a confused reply "thirty-two what?". And we can talk about operations on these numbers, like addition, multiplication, etc, but our hunger for abstraction isn't satiated at all. Since now we can talk about operations, we can also talk about properties of operations, so we notice that you can flip the arguments of additions and multiplications, and we give that property a name, commutativity, and we go on to invent abstract algebra.
After doing maths for a few thousand years we start looking into patterns into the different, disparate branches of maths at the time (geometry, number theory, logic, etc), and notice that the common pattern is that all of them are doing deductive reasoning, and that should be modellable by logic. So we abstract out the common bits and come up with set theory and proof theory and the like. And in that process we notice that number theory has unnecessarily many axioms and Peano figures out we can just use induction to define the set of natural numbers.
So yes, our intuition for numbers precedes Peano's definition by millennia or far more than millennia. But I'd say that's how it goes for _every_ definition. We always start by noticing some pattern, which initially we can't quite put our finger on (for example, Leibniz had a vague intuition that a lot of work in mathematics was in some sense so mechanical that one should be able to have a machine do it; he couldn't quite define computation yet but his hunch was spot-on), and then we try to refine our thoughts and come up with a definition.
In that sense, I don't think it's more meaningful to worry about the "true nature" of a number (the Peano definition, or some other notation?) than it is to worry about the "true nature" of the color red (is it its wavelength on the electromagnetic spectrum? but a wavelength can be expressed as a number; so is the essence of red just a number? or its RGB value, which is linked to how humans perceive color?). Both numbers and colors are abstractions, and abstractions are mental tools. They are a means to an end: to make sense of the world and communicate about it to other humans. And we use just use the tool that is most helpful given the problem at hand. For most practical applications we're going to use a notation that's compact and easily manipulated (Arabic numerals), but when we're studying the foundations of mathematics, it's more sensible to reduce everything to its smallest, most elegant definition possible. Neither view is "more true" than the other, in both cases you're doing no more than naming patterns in herds of sheep and piles of apples.
The function that produces these namespaces is usually the `new` operator, its argument, the "constructor", just allows to set up the common partially applied value (and run arbitrary code along the way).
This partial application is not a serious problem; the mutability and the inheritance are.
Closures are just a poor mans object.
(Not sure where this quote is from originally? Maybe here: http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/m...)
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
(define (cdr z)
(z (lambda (p q) q)))
Here, cons, instead of returning an object with two values, returns a closure that captures the two values. car and cdr call that closure, passing it another closure. The cons closure calls that closure, passing the the two values.Explanation at https://stackoverflow.com/questions/21769348/use-of-lambda-f....
Of course, it completely kills (or "stress-tests", if you will) the branch predictor in your CPU.
Link because this describes it more thoroughly and more precisely than I can.
The compiler will generate a class with fields for whatever variables are captured, and then instantiate it for you, assign the fields etc.
If you define your function reference as a type (as it's called), you can even inherit[1] from it and add additional overloads to the inherited class, to make it do multiple things.
[1]: https://delphisorcery.blogspot.com/2015/06/anonymous-method-...
As far as I know, yes, that's the oldest I've been able to find.
My exact quote was, While closures may look like a poor man's object, I think the reverse is closer to the case.
function Person(firstName, lastName) {
let fName = firstName;
let lName = lastName;
public getFullName = function() {
return fName + ' ' + lName;
};
public setFirstName = function(firstName) {
fName = firstName;
};
return this;
}
("return this" is probably not the best syntax at the end there, especially if bolting this onto javascript.)I would use “return scope”.
You can do it with metatable manipulation as well(changing the functions which map to specific table operators which lets you change how unmapped keys are handled) but I found the lexical closures to make a lot more sense.
Modern computers are inherently concurrent and mutable, and these fundamental realities have been shown over and over again to cause hard-to-understand issues in sufficiently complex codebases. In the vast majority of OOP languages, instantiated classes are mutable-by-default and can be mutated by multiple concurrent operators (threads, coroutines, etc). This makes them actually quite similar to the underlying computing model - even though they're a high-level abstraction, they force you to deal with the knowledge that the memory at any address can be modified at any time with no warning. [1]
Classes in most languages also offer implementation inheritance, which boils down to an approach to "rebundling" the constituent functions in ways that are arguably simple enough, but generally in practice it has been found to be hard for programmers to reason about. Dynamic dispatch is very powerful, and the way that most inheritance mechanisms expose this seems to be a model that programmers understand in theory but fail to consistently remember in practice.
In my opinion, the fundamental difficulty with classes is that in most languages, you get all of these tools bundled (complected) into the same toolbox without any say in the matter. If you didn't want inheritance, too bad. If you didn't want concurrent mutability, you're now being asked to design a complex state machine to prevent it, rather than it being excluded off the bat by most functional approaches. Functional approaches force you to opt in to most of the footguns that are included "out of the box" in OOP.
Classes will remain the best approach in many OOP languages for RAII-shaped problems, where some part of an application needs to encapsulate and manage a limited, shared, and potentially mutable resource (such as a file handle, a database connection, or an in-memory cache), because classes are an abstraction that maps cleanly to shared mutable state with locking mechanisms.
[1] You can of course also shoot yourself in the foot with closures that themselves contain mutable state (e.g. some kind of mutable container), and I think this is a rarely-considered danger for functional paradigms in languages that default to mutable data structures.
Yes classes are closer to the physical reality of modern computer (and that is one reason I prefer C++), but they are still the same construct as a closure and vice-versa. Everything is just implementation details.
Again I grant that languages designed around classes (or really in my opinion, around the physical reality of the device implementing them) are more useful in practice. But from a conceptional abstraction point the post is true.
Taking the example from the article:
function Person(firstName, lastName) {
{
type: "Person",
fName: firstName,
lName: lastName
}
}
function getFullName(this) {
switch(this.type) {
case "Person":
return this.fName + ' ' + this.lName;
default:
throw("TypeError getFullName is undefined for " + this.type ".")
}
}
function setFirstName(this, firstName) {
switch(this.type) {
case "Person":
this.fName = firstName;
break;
default:
throw("TypeError setFirstName is undefined for " + this.type ".")
}
}
From here whenever a "method" is called, the program inspects the "type" attribute (this process is abstracted and hidden in OOP languages), and uses that to invoke the correct implementation of the method. let person = Person("John", "Foo");
getFullName(person);It's one of the first things you're taught if you are learning a functional-at-its-roots language. Not JavaScript, which is mixed-paradigm at its roots. For me it was Scheme in college.
If you're learning a functional language, combining closures and functions that return functions to carry around "instance fields" and "instance methods" is what you learn right after you've gotten over the basics like map/filter and tail recursion. The equivalence of this pattern to classes is something you'll understand intuitively, even if you haven't put words to it yet.
Funny enough, I actually had the referenced epiphany while learning JavaScript, since this idiom is so common in it.
Higher order functions are common in JavaScript, and closures are as well, but I don't recall seeing them used as substitutes for classes or vice versa for years at this point. With JavaScript you don't have to use closures and higher order functions to do modularization because you have classes, and you don't have to use the functor pattern (like Java before Java 8) because you have higher order functions.
ES5's syntax for object types looked a lot more like "closures with syntax sugar" than ES6 and later JavaScript syntax with explicit class declarations.
> I don't recall seeing them used as substitutes for classes or vice versa for years at this point
I had a fairly large school project developing a JavaScript application back before ES6, and kept searching for a better way to structure my code because I found it awkward using prototypes + binding this for events. Eventually I found a hidden golden nugget of a tutorial that showed me how to do it with closures and I was in awe that it somehow wasn't the norm (and obviously how cool it was to build objects out of functions!)
A more useful analysis in my opinion is to focus on the developer ergonomics of the language. That includes how the construct is used or can be used, any properties it exhibits or guarantees, what it prevents you from doing, etc.
From that lens, the ergonomics of a Class and a Function Closure are not at all the same.
Consider replacing:
return function(method) {
switch (method) {
case 'getFullName':
return getFullName;
case 'setFirstName':
return setFirstName;
}
}
With: return {
getFullName,
setFirstName,
}
And leaving client code unchanged.This leaves callers with a struct but is cleaner and more direct. So far as I can tell, modules and closure usage are objects in JavaScript.
I'm curious if you might have insight into what this approach might block other than exclusively using functions?
The reason I chose the first option, was to really bring home the point that all of this could be expressed with closures.
In the functional example, where does Person store the fName and lName variables? Is there an intrinsic/hidden 'this' pointer? If functions persist data, are they really classified as functions anymore?
The message passing part isn't very relevant, as the concept of calling a function pointer via string lookup is present in many dynamic languages (Ruby, etc).
With a closure? And you get state hiding too.
> If functions persist data, are they really classified as functions anymore?
Sure, since otherwise returning a function from a function wouldn't be very interesting.
They are called closures. They are "environments" in which variables live, each of which can have a parent environment dictated by whatever rules there are. (Some programming languages have it where every use of curly braces creates a sub-environment; in JavaScript originally only the function keyword would introduce a sub-environment and if you write `if (something) { var x = 123; }` then `x` belongs to the parent closure and it might contain `undefined` if the branch was not triggered. Since then JS has introduced let/const which are scoped to curly brackets.)
When you declare a function it still has access to the full space of variables which it was lexically defined with. If you mutate those with one function then another function which has access to that environment can see those changes, etc.
There is no intrinsic "this", the code as presented is using a closure. Closures are typically considered a standard part of functional programming.
The author conveniently leaves out the differences: classes yield objects, whereas functions yield any kind of value. Classes are second-class citizens, whereas functions (usually) are first-class. Functions have a type, whereas classes (often) are (or are isomorphic to) one.
Functional programming actually brings some benefits to backend code. To take advantage of the extra cores in a CPU you add threads, but if these threads are continually needing to lock in order to serialize access to shared state then concurrency is diminished. The more cores you have, the more threads you have, and then the more you have to lock. Functional style solves this problem by not having mutable state.
But when it comes to JavaScript these benefits don't exist. That's not to say there are no benefits at all to incorporating some functional style into JavaScript code, but wholesale replacement of OOP with functional style is not warranted.
You say that as if it's the truth. It's not at all clear to me that's the reason OOP "won".
> These days people are taking a second look at functional because functional avoids state, which makes it easier to write concurrent code with no locks.
I think that's only a small part of the reason for the interest in functional programming.
I think there's a lot of value in the concepts of OO's message passing, and exposing an interface that encapsulates some internals. It's a tool I regularly use in FP even with immutable types
Imperative OO to me though is plain difficult to do correctly, and shouldn't be the primary way that your encouraged to write code. It's much more difficult to track dependencies and who touches what in an imperative class, and you end up digging around the file before you can even know what you have to keep in your head.
With FP, you generally have pure functions that take in some state and dependencies, and produce a new state. Everything you need to know related to how that function behaves is explicitly given to you, and it's trivial to test the code. You can do this in imperative OO too, but you're nudged in that direction by default.
I do think there are cases when imperative code and imperative OO make a problem easier to represent/understand and faster to execute, but I like to keep those sections small and easy to fit in your head, delegating most of the logic to functions. The imperative OO is mostly some glue to bring the business logic to life.
FP: 'state is hard, let's not do it unless we absolutely have to'
What's really interesting is Rust arrived at a similar destination, I assume as a natural byproduct of pushing for memory+thread safety. State isn't discouraged, but you get explicit owner(s) of data who lend it out to others who are readonly by default, and explicit when they will modify the reference you gave them.
I do, though, I think, find OO projects easier to understand. There's an opinionated convention about what code to save in which file (one file for interface of a class, one for implementation).
Sometimes I think it's this, more than anything to do with language semantics, that actually drives OO adoption. When you sit down with an empty folder and a text editor, it's really easy to know where the files go.
If GC was the distinguishing factor, OOP would have never gotten off the ground.
I know a JS dev who gets paid good money to develop software, who thought the reason you shouldn't use floating-point to deal with currency was because "javascript is bugged".
GUI programming is traditionally retained-mode. OOP is traditionally state encapsulation and mutability. OOP and retained-mode GUIs are conceptually similar, and Functional Programming and immediate-mode GUIs are conceptually similar.
Here's a good explanation of this: https://medium.com/@puppybits/immediate-mode-reactjs-7a6302c...
GUI programming surely wasn't retained-mode on 16 bit home computers, in fact that was one of the issues getting used to GUI based OSes event queues back in the day.
Being stateless and side-effect free is pretty fundamental to functional programming. If you can't replace a function call with its resulting value (without changing the meaning of the program) then it is not functional programming.
React is neither stateless nor side-effect free. Mapping it to concepts from functional programming is counter-productive. You get something that is neither fish nor fowl.
How is OOP easier to understand exactly?
If you are debugging the code that looks something like this:
class Child extends Parent {
launchRockets() {
this.launch(this.rockets);
}
render() {
this.score = this.getScore();
return `<span>${this.score}</span>`;
}
}
How do you know which part of your code contains implementation of this.getScore method? How do you know which part of your code is responsible for the number of this.rockets and how many methods in how many classes can change it? How do you find out which part of your code is going to call the launchRockets method, and whether this will even be this class's method or one of its children's? How many hops across your codebase do you need to make to figure all this stuff out?How is this style any simpler than functional?
Do you really never need to see what a passed-in function actually does in your FP code? Never need to see what type an argument is in our fancy Haskell or Hindley-Milner-typed language?
I'm hesitant to say always, but I can say:
- in imperative code I never feel comfortable without inspecting the passed in function
- in pure functional code, I rarely feel uncomfortable taking a pure function at it's word
In FP, I tend to look at implementations to understand what they'll produce
In OO, I do this, but I also need to know _how_ since they might be changing the current context I'm working in. It's not enough to know that X method produces Y, I also need to remember that calling X implicitly modifies Z when I'm referencing Z
In an FP environment X can't modify Z outside of his context, so I don't have to care about how he produces Y. The cost of this is that someone needs to own Z and functions need to additionally return the new Z, but I think that explicit hierarchy helps in guiding towards a more understandable architecture
You place the cursor at 'getScore' and press F12. This problem (calling functions defined god-knows-where) is not unique to OOP as opposed to FP.
> How do you know which part of your code is responsible for the number of this.rockets and how many methods in how many classes can change it?
You place the cursor at 'rockets' and press Shift+F12. This problem (modifying/passing data around god-knows-where and what parts) is not unique to OOP as opposed to FP.
> How do you find out which part of your code is going to call the launchRockets method,
See above.
> and whether this will even be this class's method or one of its children's?
Well, there is Ctrl+F12 that shows you different (re)implementations of the method.
> How many hops across your codebase do you need to make to figure all this stuff out?
About three to four. Again, this problem is not unique to OOP as opposed to FP: with FP, when your 'foo' function gets a 'frob' function passed to it as a parameter, it's anyone's guess what actual function it may be. To find all possible cases requires you to find all call sites of your 'foo' function, but that's non-trivial since it is most likely has also been passed around as an argument under different names.
Well, part of that problem is solved by explicitly importing functions from individual modules, at which point you know where exactly your function is coming from.
> when your 'foo' function gets a 'frob' function passed to it as a parameter, it's anyone's guess what actual function it may be
Part of it is probably solved by static typing. The other part is that it shouldn't pose a problem, much like you probably don't have a problem passing all sorts of frobs to your map-s, and your filter-s, and your forEach-es, and your then-s
There are still certainly benefits to using FP in JavaScript from a design, readability, maintainability sense, even if you are not concerned with concurrent code with no locks. A good FP design can be easier to reason about than an OOP graph of objects.
OOP as we know it today, which is not what the inventor of the term had in mind, likely won because it was easy to add it to C in a manner compatible with low-level optimizations that were critical in application code at the time, and less so now.
I dispute that one style is inherently easier to understand than the other, though each has certain problem domains where it maps particularly cleanly.
A couple points:
I'm not sure whether OOP really won: pretty much any language now has taken lambdas & higher-order functions from FP; before those parametric polymorphism (ML was there before C++ templates and Java generics); many sources will tell you to limit mutable state; and even algebraic data types are starting to get mainstream traction (Rust). Speaking of which, I believe Rust does not sell itself as an OOP language?
It is not at all obvious that OOP is easier to understand. Those animal/shape hierarchies are indeed very easy to grasp, and they are also useless. Even games have moved away from class hierarchies, which are both slow and too inflexible. They're more about ECS/data orientation now.
I'm not sure that "easier to understand" does lead to more maintainable code either. If you're talking about daily use, why not. If this is a learning curve thing (and in practice I suspect it is), then I think ease of learning and code quality won't correlate with each other — provided only people who learned the stuff get to program with it of course.
That's a wonderful description.
Actually, OOP won because OOP is easier for non-developers to understand -- those who were making policy and purchasing decisions back in the 1990's.
And the OOP we now call "OOP" (as realized in languages like Java and C++) is not at all the OOP that was first envisioned by Alan Kay and implemented in languages like Simula and SmallTalk.
I don't think anybody had compared maintainability between similar projects in OO and functional style. Functional was not really a contender in this space.
Almost everybody who adopted OO migrated from imperative. The popular OO languages were imperative languages adapted to OOP - Visual Basic, C++, Delphi etc.
If by functional you mean higher order as in the title and not what is commonly known as FP - it's never been true. For example, a function that has no outside side effect and that calls an anonymous function provided as an argument is way easier to understand, reason about and maintain than its OOP equivalent of creating an object and calling three methods in specific order mixed with your code.
> These days people are taking a second look at functional because functional avoids state, which makes it easier to write concurrent code with no locks.
Avoiding state doesn't make it easier to concurrently touch shared memory without locks. Event loop style concurrency simply doesn't touch shared memory concurrently, so it doesn't need locks and doesn't have all those multithreading problems. It can still mutate state left and right, higher order programming just allows to map intent better to the code, making "sequences look sequential" and all that.
Actually solving problems concurrently with high performance today generally requires ditching shared memory based synchronously communicating concurrency models, leaving async communications of isolated entities where locking problems and overhead don't exist. But it has nothing to do with functional programming, OOP, higher order programming.
Let me make a meta-point. It's natural to find yourself with a thought of this form: I don't get why so many think ___ is cool.
There are two answers to this:
1. None of them know something you know, and you are right.
2. At least some of them know something you don't, and you are wrong.
Statistically speaking, 2 is much more likely to be true. So when you find yourself having these kinds of thoughts, which are entirely natural, you'll make better progress if you can formulate it as a good-faith search for truth and approach it with an open mind.
I don't get why so many think cigarettes are cool.
You're telling me they know something I don't about cigarettes?
* Cigarettes are stimulants and feel great when they first kick in, like a good cup of coffee.
* Smoking gives you a culturally-approved reason to take breaks throughout the day and go outside.
* Smoking is a social activity that provides an easy way to meet new people.
Of course, it is strongly chemically addictive too with serious negative health consequences, but the reason people smoke isn't only addiction or no one would ever start. I learned the above list by once genuinely asking someone why they smoke.
60+, and OOP has been around for 50+.
> OOP won against functional because OOP is easier to understand and leads to more maintainable code.
OOP didn't “win”, it had a period of intense popularity starting in the late 80s and starting to fade around the mid-00s. And that was largely path dependence and building on C; had someone put the kind of effort into C-with-lambdas instead of Stroustroup's C-with-objects, we might have had a couple decades where the most popular languages were all C-descended impure functional programming languages rather than C-descended class-based OOP languages.
With the revival of dynamic languages though, people heavily programming in JS, Ruby, Python, etc. Performance is no longer a concern of people. So now people are motivated by what makes the act of programming as convenient to the programmer.
When it comes to that, it turns out a lot from FP does make things simpler for the programmer. It started with data transformation, I think helped by the appearance of LINQ in C#, which first exposed people to functional data transformation. Then Java got Streams, and everyone kind of followed suit in introducing some higher order functions and map/reduce/filter and all that.
Also: callbacks. As asynchronous behavior became more popular, there was a real need to lower the amount of verbosity and just code you needed to type to program in an async style. So creating a class and an object for every little event in your app, to pass as callbacks to all async functions just got super annoying. So people introduced anonymous functions and lambdas, and added support for higher order functions that could be used as callbacks.
Once you've got callbacks and higher order functions, closures are a natural extension, you often want to pass a callback that will set some state elsewhere when done or other such things. So Closures were also added.
And here we are, with a full FP toolkit due to the fact that performance isn't a concern anymore and async programming is more convenient when your language has support for first class functions with closures and anonymous functions and lambdas.
At the same time, like you said, people started squeezing more out of their cores and their seperate IO controllers, so concurrent and parallel programming became more common. That forced people to look at the immutable characteristics of some FP langs, and so support for immutability was also added to a lot of language.
Once you're in the realm of immutability, Objects also become less useful, since there's no need to encapsulate and protect modifications to something you can't modify anyway. Thus once again, creating a class and a constructor and all that just becomes unneeded verbosity and extra code to write. So people wanted ways to go back to immutable value records and just write functions over them, etc.
All these things in my opinion combined for what is today the "resurgence" of FP.
I often think of functions as intentionally "stateless", NOT persisting any value. For that a class is used, if anything for safety.
It's javascript, where all functions are closures.
In the closure-based version, getFullName and setFirstName are defined anew for each instance of Person, using more memory each time. Typically isn't an issue unless you're creating a massive number of them, but the class-based version doesn't have this problem.
Also, just to throw it in the mix as well, the class-based version I believe is largely just syntactic sugar over the traditional way of creating objects in javascript, using prototype-based methods:
function Person(firstName, lastName) {
this.fName = firstName;
this.lName = lastName;
return this;
}
Person.prototype.getFullName = function() {
return this.fName + ' ' + this.lName;
}
Person.prototype.setFirstName = function(firstName) {
this.fName = firstName
}
const person = new Person("Ben", "Bitdiddle")
person.getFullName()
Prototypical inheritance is interesting in that an instance of Person could be used as the class for another object, ad infinitum. // optimized
function constructor(...) {
let this = {...}
function method(this, ...) {...}
return function (name, ...) {
switch (name) {
case 'method': return method(this, ...)
// or return a temp closure
// like it does in tfa
constructor()('method', 42)
And vice versa, on a computer that does closures in hardware better than objects for some unrealistic reason.That's why both are just abstractions, whose semantics may better fit one or another implementation, but it really doesn't matter for maths.
class Foo(Baz):
def foo(self, bar):
return self.baz(bar + 1)
Notice that we're not calling the 'baz' method via some fixed lexical variable; we're looking it up in the 'self' object, which has been passed in (implicitly) as an argument ('self' will also implicitly be passed into 'self.bar' as its first argument). Java's 'this' serves a similar role, except even more things are done implicitly.One slight complication: 'self' should be lazy, so we can take the fixed point (so every method sees all of the overrides). This is easy in lazy languages (e.g. Nix); in strict languages we can just make 'self' a function: it looks up a given name in the object's properties/methods, and if not found it invokes the superclass (AKA 'super').
That reminds me of Steve Yegge's "Execution in the Kingdom of Nouns", actually.
Although some people seem to like pre-history languages without modules that require workarounds like using prefixes everywhere.
The main advantage of classes, I think, is that they define one underlying principle for all these data transformer nodes: that each node has a few binding points with certain interface: named methods. The nodes don't have to look this way, but restricting the freedom of design choices to this pattern makes it easier to compose many nodes together. It's like setting the expectation that all cars are squarish: it's easier to build the infra around cars when you know how they can look in general.
Classes are in no way functions in the FP sense because classes mutate state.
Classes are in no way functions because calling methods requires the instantiation of unrelated state. Example:
class A
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
self.banana = create_banana(create_gorilla(create_jungle()))
def process_a(self):
return process(self.a)
process_a cannot be used unless a, b, c and the banana plus the gorilla holding the banana and the entire jungle the gorilla lives in is instantiated.Let's talk about terminology. There are three that often get mixed up.
Procedure = a set of instructions that could encompass mutating and changing state.
Function = a box that when given input produces an output. The box cannot modify the universe. It can only produce something new when given an input.
Object = A grouping of a set of procedures and state that can mutate.
The javascript programmer OP is mixing up function and procedure. He defines a procedure and says that it can be a class. A procedure can be a class as it's just a set of instructions which can encompass defining scope, state and more procedures restricted to scope and state (aka an object).This thing right here, written by the OP:
function setFirstName(firstName) {
fName = firstName
}
Is not a function... it is a method or a procedure that mutates state.A function can do none of the above.
That's it.
All functional languages need to bend the rules a bit in order to have side effects but in general the person is not writing functions and therefore is not getting the benefits of using functions.
FP is about reducing and restricting these functions that break the rules. Defining a class in terms of mutations and side effects is going against the philosophy of FP.
Which mathematical definition? A "box" is not a mathematical definition. There are multiple mathematically precise definitions of "function" possible. Yours is none of them. Some of them accommodate objects that model mutable state.
I don't think many (any?) PL theorists or functional programming practitioners would agree with your dogmatic stance. Where are you coming from with all this?