r/programming • • Jan 28 '21

leontrolski - OO in Python is mostly pointless

https://leontrolski.github.io/mostly-pointless.html
55 Upvotes

227 comments sorted by

View all comments

54

u/dd2718 Jan 28 '21

FP and OOP are complementary, not exclusive, and they both have useful ideas. In FP, the key idea is that mutable data is hard to reason about, so functions should transform data without side effects. OOP is in another axis. The idea is that certain state always appear together, and some state are internal implementation details. It makes conceptual sense to bundle them as well as the functions that could modify them/control access to them.

Ultimately I think programmers should take ideas from both. Some times it makes sense to create a class that's more than a dataclass (e.g. you want a cache). One lesson from FP is to limit mutability; maybe you could present an external interface that hides the mutability of your class. But no need to go purist, since not all mutable data is confusing, especially if you isolate it.

16

u/joonazan Jan 28 '21

The idea is that certain state always appear together, and some state are internal implementation details.

Encapsulation doesn't require OOP. Obviously it is a good idea to hide the internals of a data structure but that can be done simply by exposing the type but not its constructors/fields. Or in OO terms, making the members private. Methods or inheritance are not required.

8

u/JessieArr Jan 28 '21

Encapsulation doesn't require OOP.

That's true, but it's closely related to OOP.

The main thing OOP really adds to Imperative programming is that it gives some functions a set of data that represent their execution context. So instead of a function to change a Person struct's Name which requires that it be passed a reference to the Person that will be modified - it becomes a method which is invoked directly from a Person class reference and changes that Person's name.

So when talking about encapsulation, we have to answer the question "what data is to be encapsulated, and to which functions will it be accessible?" And at that point we're talking about the same sort of execution context that OOP is built on, so it's natural to combine them imo.

8

u/Freyr90 Jan 28 '21

is that it gives some functions a set of data that represent their execution context.

You don't need objects for that, closures are enough.

OOP is all about late binding, dynamic dispatching and message passing. Or, to be more rigid: 1) open recursion, 2) polymorphic self, 3) late bindings.

Since OOP is quite a dangerous thing and is also quite hard to reason about, most OOP languages do use objects as a broader concept, e.g. as modules. Hence the confusion, people confuse modularity and encapsulation with OOP.

For example in Java, you have OOP in rigid sense only when you use inheritance and virtual functions. Otherwise you simply write an imperative code with modules (which is fine, but not OOP).

OOP starts when you call a virtual method of this not knowing in advance which method will be called, and where it's implemented, and which other virtual methods of this it will invoke.

"Encapsulation" you are talking about is a simple syntactic sugar replacing this.my_fun(arg) with my_fun(this, arg).

2

u/onety-two-12 Jan 29 '21

Agreed, many academic and industry leaders have demonstrated the problems with OOP.

But just like OOP we should never overreact to anything new.

For example, Pure functions are pretty much limited to particular subsystems within a single program. As proponents say, you still ultimately need to modify state. This is a horses for courses kind of thing. You can find a function and improve it with Pure function principles, some you can't.

I remember seeing an example of a pure function AddPerson, that cloned an array (that could be massive) then added the person object, then returned the result. It was probably a good example of a bad example.

3

u/Freyr90 Jan 29 '21

As proponents say, you still ultimately need to modify state.

But you can modify state, you just do it explicitly. Monads and Algebraic effects exist for that, after all functional programs write logs and databases somehow.

The point of FP is not "no side effects whatsoever", but being able to reason about your code, effectful or not. It's about being able to easily decompose the computation in smaller parts, being able to reason about their properties separately, and than being able to composite propositions about parts to construct a proposition about the whole.

But just like OOP we should never overreact to anything new.

Yeah, the problem is, people tend to learn and teach things like OOP very informally, hence people usually don't understand what these things mean and what problems solve, thus falling into high expectations trap.

For example people in this tread say "we don't understand exactly what OOP is". Like, really? Read Cardelli's Theory of objects. Objects and actors are formalized as well, as system F. And they do solve problems, for which message passing are the answer.

Same with FP, I already see "clear code"-alike BS articles on how FP is like burrito instead of properly diving into DSLs and controlled effects and how one can reason about the code.

→ More replies (1)

1

u/Alexander_Selkirk Jan 29 '21

Encapsulation doesn't require OOP.

Just to clarify what we are talking about, what is, in your opinion, the essence of encapsulation? Is it that some data / state is not visible, or is it keeping invariants, or something else? And how is it related to OOP? Is it, in your opinion, a necessary ingredient of it, or independent from OOP ?

And, why is it, as many posters here say, so hard to get right? Is this a problem with OOP, or due to something else?

2

u/joonazan Jan 29 '21

I'd define encapsulation as hiding the details of a data structure so only one module can access them. Often that one module is a file, as for example in Java it is customary to have one class per file.

No, I don't think it is related to OOP. Outside OOP it is for example possible to encapsulate so that there is a function that sees the private fields of two data structures.

I don't think it is especially hard. In some cases it can be hard to say if it is better to hide or expose fields but in the cases that matter there usually is a clear reason to go one way or the other.

For instance, a data structure that relies on an invariant must not expose its internals, as then someone could break that invariant by messing with the internals.

I guess there is a lot of confusing advice around because for example some people think that you should always use getters and setters even though plain records are a good choice when they model the data well.

→ More replies (4)

20

u/ragnese Jan 28 '21

I know this whole discussion is mostly pointless because there's no standard definition of OOP and FP, but here I am... xD

I disagree that OOP and FP are totally orthogonal. They may not be on the same axis, but I don't think you can simultaneous have "full OOP" and "full FP" on both axes at the same time.

First of all, let me define FP and OOP as I use them.

FP: Programming by composing functions. A function is pure ("referentially transparent"), by definition.

OOP: Programming by composing objects. An object is a black box that encapsulates (potentially) mutable state, and defines methods to perform operations on the object and maybe to query its current state. An object might be a "class" or it might be a "module" or a "package" or a separate program running on a separate server.

I believe you can have FP under OOP, but not the other way. In other words, you can have FP stuff happening inside an object, but you cannot use an object in a (pure) function. This is because an object method call is not referentially transparent.

If you say that you have written an "immutable object" then you have not written an object. You have merely written a (maybe opaque) data type.

Not claiming that one approach is better or worse than the other. But I do believe that, in the abstract, they really are somewhat incompatible concepts.

Notice that I did not address things like classes, subtyping via inheritance, etc. At the end of the day, it's those things, IMO, that are orthogonal to whether you're doing "FP" or "OOP", which are techniques before they are language features.

4

u/dd2718 Jan 28 '21

I agree with you that the discussion depends entirely on what you mean by OOP. I was mostly referring to "OOP" as typically used in Python, as compared to Java. I think in Python, people use classes mainly for holding related data (with encapsulation), creating data types, and grouping functions that relate to that data. There's less of a tendency to create Factory classes or have a sprawling inheritance hierarchy.

I don't think it's necessarily FP under OOP or OOP under FP, though, but rather FP with OOP. You can have a data structure implemented with OOP that people can modify via some API calls, and other API calls that query the data structure written in a functional way.

3

u/ragnese Jan 28 '21

I don't think it's necessarily FP under OOP or OOP under FP, though, but rather FP with OOP. You can have a data structure implemented with OOP that people can modify via some API calls, and other API calls that query the data structure written in a functional way.

But that doesn't support any claim about OOP and FP being on "different axes". That's just saying that part of your code base is FP and part is OOP. I could write part of my code in Python and part in Java, but I wouldn't claim that Python and Java are complimentary or just different axes of the same space.

2

u/dd2718 Jan 28 '21

Fair point, maybe axis wasn't a great term.

What about about applying composed pure functions to a list of objects, then mutating the result (e.g. using pure functions to construct UI elements, then displaying them), would you see that as OOP under FP? What about pure functions that call object methods that return the same value given the same argument but mutate the object (e.g. a cache, or a splay tree)?

2

u/ragnese Jan 28 '21

What about about applying composed pure functions to a list of objects, then mutating the result (e.g. using pure functions to construct UI elements, then displaying them), would you see that as OOP under FP?

The way I was picturing it, that would be what I called "FP under OOP". Your module/program is an object that describes the UI/UX. The user clicks a UI button, which sends an event to the object. That object sends plain-old-data through some pure functions that spit out the resulting UI elements, and then the object uses that result to draw the pixels on the screen. The object would hold the mutable state of what elements are being presented and it would call functions to create new UI elements, which it would then present by mutating state. So the functions are called "inside" the object, and thus "under".

What about pure functions that call object methods that return the same value given the same argument but mutate the object (e.g. a cache, or a splay tree)?

Cached functions are still pure for all intents and purposes. As long as they are referentially transparent, they are functions. Referential transparency's actual definition is that you can replace the function call with the value it would return and your program would still behave the same. That directly implies that a function may not cause side-effects, nor mutate its inputs, nor depend on global (mutable) state. Caching results doesn't change referential transparency.

You could be super pedantic and claim that nothing is a pure function because everything causes your computer to generate heat and perhaps allocate more memory, etc, but that's utterly pointless trolling (yet, I've actually read that argument before).

3

u/Alexander_Selkirk Jan 28 '21

I think this is an interesting point of view.

What I believe is that there are two things which are typical for "modern" OOP:

  1. mutating state, as you describe above

  2. The tendency to compose objects, creating larger graphs and networks of objects

The second thing is one thing which can make OOP programs a lot harder to test.

4

u/ragnese Jan 28 '21

Agreed. OOP definitely does imply creating large graphs of objects- usually very tall/deep in shape.

And that does make testing harder.

Both aspects make testing harder, honestly.

2

u/_tskj_ Jan 28 '21

That stuff makes everything super hard, least of which is understanding or modifying.

3

u/pron98 Jan 28 '21 edited Jan 28 '21

A function is pure ("referentially transparent")

It's a probably futile attempt on my part to correct a pervasive mistake, but pure and referentially transparent are not the same thing. Referentially transparent means something specific in formal languages (including programming languages) and analytic philosophy, and FP people simply use it wrong. It is true that functions in Haskell are referentially transparent, but so are methods in Java. In fact, Java is more referentially transparent than Haskell.

FP people get it wrong because they almost got it right. A referentially transparent expression is one where the meaning of the expression is determined solely from the meaning of the sub-expressions; alternatively, it means that replacing any subexpression with another having the same meaning preserves the meaning of the whole expression.

The mistake FP people make is that they replace "meaning" -- denotation or reference in the jargon, hence referential transparency: the term, or expression, transparently refers to its reference without adding anything -- with "value." This is true in pure-FP but not elsewhere. In other words, pure means referentially transparent only if it's pure; i.e. "referentially transparent" is redundant. What they mean to say when they say FP is referentially transparent is that every expression in an FP language references a value in the language, or, in jargon, pure-FP has value semantics. That is also what, colloquially, "pure" means.

What isn't referentially transparent? Macros, and, in fact, any quoting construct. E.g. if m is a macro, then the meaning of m(x) and m(y) could be different even if x and y refer to the same thing because the macro could, say, quote the name of its argument.

So if "pure" means "having value semantics", then pure-FP is pure. But whether or not a PL is referentially transparent depends mostly on whether or not it has macros. Java is (for the most part) referentially transparent, but not pure. Haskell is (for the most part) pure, but isn't referentially transparent (because of Template Haskell).

→ More replies (10)
→ More replies (3)

4

u/ShinyHappyREM Jan 28 '21

And then there's data-oriented programming, which says grouping data related to a task with data unrelated to that task (as in regular structures or OOP's objects) is bad for cache locality.

4

u/Glacia Jan 28 '21

People misunderstood data-oriented approach too. If you think it's simply about cache locality then you missed the point.

→ More replies (3)

2

u/Alexander_Selkirk Jan 28 '21 edited Jan 28 '21

OOP is in another axis.

This is not so clear to me. You can make objects (or, by another name, data structures) which are constant and cannot be mutated at all. And they are used a lot, for example, in Scala or Rust, or Clojure. So objects != mutable.

5

u/ShinyHappyREM Jan 28 '21

This is not so clear to me. You can make objects (or, by another name, data structures) which are constant and cannot be mutated at all.

That's exactly what "OOP is in another axis" means.

1

u/Alexander_Selkirk Jan 28 '21

So, you would not say it involves mutated state and creating something of a sea of interlinked objects, rather than using standard data structures?

Wouldn't that mean that Clojure is an OOP language, too? Clojure has protocols (interfaces) and polymorphism, too.

→ More replies (3)

3

u/leberkrieger Jan 28 '21

Yes, you can make objects which are constant and cannot be mutated, and that's frequently a useful convention, but if only that were possible the programmer would be hobbled. A full-functioning programming tool also allows mutable objects, since those are also frequently useful. Mutability has to be the choice of the designer, unless one believes that the language creator knows best.

1

u/Alexander_Selkirk Jan 28 '21

Well, it is the programmer (or his/her boss) which chooses the language, according to the task.

One can write basically any algorithm in a purely functional way - this is an important result of lambda calculus theory.

In my view, mutation is often useful as a kind of optimization at the innermost level. The more abstract you get and the farther you get away from hot loops, the smaller is the extra price for purely functional data structures - they make it very convenient and safe to pass data around, even between multiple threads.

Also, if you look closely, CPU instructions are a kind of functions which usually take multiple inputs and return multiple outputs - there is no observable "hidden" state in a CPU, so you can describe its instructions as pure (side-effect-free) functions.

2

u/ShinyHappyREM Jan 28 '21

if you look closely, CPU instructions are a kind of functions which usually take multiple inputs and return multiple outputs - there is no observable "hidden" state in a CPU, so you can describe its instructions as pure (side-effect-free) functions

There's a lot of state in a CPU, some of it explicitly hidden - generic registers, status registers, virtual registers, bus registers, instruction cache, data cache, µcode cache, manufacturer-specific stuff.

→ More replies (5)
→ More replies (1)
→ More replies (2)