If you want to really see OOP in action study Erlang. As the late great Joe Armstrong says in his introductory book, Erlang is a real OOP system - objects can only communicate through message passing. There is no notion of visibility, friendship, inheritance, etc. Objects can hold state and pass messages and that's it - which coincidentally makes concurrency dead simple.
What most people think of when they think OOP is C++, Java, and Python which got OOP completely wrong and ruined our perception of OOP. They also tried to use their misshapen OOP hammer on every problem they could find to the point that it’s an overused meme. These days you see languages actively trying to distance themselves from OOP as a form of enticement (Rust and Go being the two prime examples).
> These days you see languages actively trying to distance themselves from OOP as a form of enticement (Rust and Go being the two prime examples).
They definitely distance themselves from inheritance. However both Rust traits and Go interfaces look to me like they're there to appease people who like the .method() calling syntax. (I know there's more to the story than that, particularly for Rust and its type system...) I think either could've gone with multimethods or overloaded functions and have been better for it, but a lot of people seem to really like the object.method() look.
> a lot of people seem to really like the object.method() look.
There’s a pretty simple reason for that: autocomplete.
In most halfway decent dev environments, typing “object.” will present you with a list of possible operations on that object. OTOH, if you want to do “method(object)” you need to know the method name (in all current scopes, including globals).
I’m not saying this is the only or even best way to write code, but it’s definitely a factor IMHO.
You're almost certainly right, this is pretty compelling. It's not the way I work, but I'm sure lots of people really like their IDEs. Despite that, I recently changed one of my APIs from being:
result foo(bar b, other stuff)
result foo(baz b, other stuff)
result foo(bum b, other stuff)
To:
result r = bar.foo(other stuff)
result r = baz.foo(other stuff)
result r = bum.foo(other stuff)
Not because of the IDE, but because the compiler error messages are horrible for the ones above when you make a mistake. If you pass the type as the first argument, the compiler "helpfully" tells you 3 pages of information about all of the overloads. However, if you use method syntax, it only tells you overloads for the one object.
It's a little frustrating that the hammer is changing the shape of our hands, and not vice versa.
The world has been changing the shapes of our hands since they were fins.
The problem I have with most language's function call syntax (except for point-free stack based languages like FORTH and PostScript) is that you can have multiple fingers on your "in" hand, but only one finger on your "out" hand. C#'s in/out/ref modifiers and Lisp's multiple-value-bind are hacks.
FORTH's /MOD ( numerator denominator -- remainder quotient ) naturally takes two integer inputs and returns two integer outputs, and it doesn't use need any special clumsy syntax to express that.
> The problem I have with most language's function call syntax (except for point-free stack based languages like FORTH and PostScript) is that you can have multiple fingers on your "in" hand, but only one finger on your "out" hand.
I dunno, in many modern languages that aren't point-free stack-based languages you can (and the difference between these is one of perspective more than concrete substance, arguably) either have multiple “fingers” on either hand or only one on each, but the one value each touches can be arbitrarily structured and destructured.
That same logic can be used to argue that functions should only support one input argument, too. If you can have multiple inputs, then what's wrong with multiple outputs? And if multiple outputs are so bad, then why have multiple inputs?
There's a big difference between simply and efficiently returning multiple values on the stack without generating intermediate garbage and memory references, and packing multiple values up into a single tuple, polymorphic array, structure, or class, and then destructuring it later, or passing input parameters that are indirect pointers to temporary output locations in linear memory.
Using indirect pointers for output parameters can also cause bugs and performance optimization problems with aliasing.
To elaborate what I said: C#'s in/out/ref and Lisp's multiple-value-bind syntax are clumsy, inelegant, and inefficient hacks.
And languages like Java that don't have pointers can't even do that, and just have to proliferate pointless container classes and generate garbage intermediate objects. Quick: How do you implement swap(a, b) in Java? (Without using an IntermediatingObjectSwapperDependencyInjector- ProxyThunkingShadowEnumeratorComponentBeanAdaptor- DecoratorReferenceRepositoryServiceProviderFactoryFactory!)
With WebAssembly, for example, returning multiple values has a significant effect on performance and code size, because it's much more costly to return multiple values indirectly through linear memory than on the stack. (The stack is in a separate address space that you can't point to like linear memory.)
That's why there's an active multiple value return proposal for WebAssembly, which is implemented in Chromium release 80:
>There are a few scenarios where compilers are forced to jump through hoops when producing multiple stack values for core Wasm. Workarounds include introducing temporary local variables, and using local.get and local.set instructions, because the arity restrictions on blocks mean that the values cannot be left on the stack.
>Consider a scenario where we are computing two stack values: the pointer to a string in linear memory, and its length. Furthermore, imagine we are choosing between two different strings (which therefore have different pointer-and-length pairs) based on some condition. But whichever string we choose, we’re going to process the string in the same fashion, so we just want to push the pointer-and-length pair for our chosen string onto the stack, and control flow can join afterwards. [...]
>This encoding is also compact: only sixteen bytes!
>When we’re targeting core Wasm, and multi-value isn’t available, we’re forced to pursue alternative, more convoluted forms. We can smuggle the stack values out of each if and else arm via temporary local values: [...]
>This encoding requires 30 bytes, an overhead of fourteen bytes more than the ideal multi-value version. And if we were computing three values instead of two, there would be even more overhead, and the same is true for four values, etc… The additional overhead is proportional to how many values we’re producing in the if and else arms. [...]
>Returning Small Structs More Efficiently
>Returning multiple values from functions will allow us to more efficiently return small structures like Rust’s Results. Without multi-value returns, these relatively small structs that still don’t fit in a single Wasm value type get placed in linear memory temporarily. With multi-value returns, the values don’t escape to linear memory, and instead stay on the stack. This can be more efficient, since Wasm stack values are generally more amenable to optimization than loads and stores from linear memory.
> That same logic can be used to argue that functions should only support one input argument, too.
Some languages do (more if you consider things that support what looks like multiple arguments but which the complete set of arguments that can be passed to a function corresponds directly to a single data structure in the language.)
> There's a big difference between simply and efficiently returning multiple values on the stack without generating intermediate garbage and memory references, and packing multiple values up into a single tuple
Fundamentally, there's not, since you have to represent both the number of items and each item either way. It's true that there are more and less efficient means of performing the task, but the information required is identical, so it is quite possible for any method capable of doing what looks like one to a user of the language to implement what looks like the other from the same perspective.
The obvious implementation when conceptualized each way given other elements of a language or it's implementation design may be different, but that's not an inherent difference, and implementing efficiencies in the implementation has no necessary reflection in language-level features (and supporting any particular language-level feature isn't a guarantee of efficient implementation.)
Yes, I think how Forth and PostScript does that is good, compared to the other ways (there are other good things about Forth, too). And then, in assembly language, it may depend what instruction set is used and how call frames are organized; Glulx allows only one return value, and same with Z-machine code.
I don't understand what you mean by "causal" in this context. A.f(B) is just a syntax sugar around f(A, B) unless dynamic dispatch comes into play. Then it's usually syntax sugar around something like:
A._vtable[f](A, B)
It really is just a function of two arguments, even when the privileged first argument looks like it's outside of the parens.
> I don't understand what you mean by "causal" in this context.
I mean that if you want contextual autocompletion (e.g. have only the relevant functions being shown given the type of an argument) then at some point you have to give your IDE this type information before typing the function - however you look at it it will always be [context] [function] instead of [function] [context].
> A.f(B) is just a syntax sugar around f(A, B) unless dynamic dispatch comes into play.
this is a complete implementation detail of your operating system ABI and the way OO languages compile down to it, and in no way a "general truth" (of course there is an equivalence between both). There used to be some CPUs with hardware instructions for OO method calls in the 80s for instance - you may want to call that syntax sugar around electrons, but I would respectfully disagree :-)
(even then, for instance Microsoft has the __thiscall calling convention for C++ methods which differs from __cdecl used for C functions - so even there there are fundamental differences between a.f(b) and f(a, b) as arguments will be passed differently to the method.)
What language? Unless it's Ocaml, I'll be surprised if addition isn't overloaded for ints and floats at the least. Most people are fine with some overloading... To each their own though.
Yes but that doesn't really apply to your point. Addition is built in. It doesn't affect completion, and barely affects type checking. The "overload" is fixed and generally well understood. It's almost not an overload at all if you view it as an operation on the set of all integers. There is only a clamping in the end that depends on the size of the type (unless it's unlimited precision arithmetic on BigNums)
I’d say infix syntax and somewhat asymmetric method lookup/overloading rules are the bigger factor. Notwithstanding that it matches the implementation of virtual dispatch.
Yeah, both Rust and Go also use the method syntax to provide dynamic dispatch, and it's simpler for them since this dispatch is chosen (presumably vtable style) based on the privileged first operand.
However, I think using methods to provide infix syntax is a real mistake. For instance, if I want to make an infix operator which allows me to subtract mytype and yourtype, I can do the following:
mytype - yourtype; // mytype.sub(yourtype)
However, the opposite requires me to add methods to yourtype:
yourtype - mytype; // yourtype.sub(mytype)
Python gets around this by having 'r' versions of functions which get invoked if yourtype refuses to acknowledge my type. However Go and Rust could resolve which binary function to use statically, but they don't want to support overloading unless it's a method on the first object.
I misunderstood then. However, if you just meant that people prefer method syntax because method syntax is infix, then it seems to come full circle to asking why they like infix syntax for methods... Doesn't really matter - it is what it is.
Well, if you imagine some prefix method syntax -- let's say, foo[a](b, c, d) instead of a.foo(b, c, d) -- it misses out on one of the advantages, in that method chaining is easier to read. I mean this is a nice feature for function call syntax in general and the benefits of infix syntax is not specific to methods. This is one reason why some new languages make f(a, b, c) and a.f(b, c) produce the same AST. Anyway, I wanted to separate the effects of infix positioning of the method name from those of having distinguished syntax for method invocations, which with static dispatch still carries other advantages.
I think your point is to emphasize there is no additional cost to method syntax. I wasn't trying to imply there was, only that when you want dynamic dispatch, you can only get it from method syntax.
Another interesting asymmetry, which I think shows a weird favoritism for method syntax is that Rust allows both of these:
object_of_type_A.foo()
object_of_type_B.foo()
But not both of these:
foo(object_of_type_A)
foo(object_of_type_B)
Ignoring dynamic/static dispatch for the moment, you can overload if you use method syntax, but not if you use function syntax. For me, I would prefer the latter, but I think I'm clearly in the minority.
In a dynamically-typed message-passing system, inheritance is just automatically delegating messages to another actor and possibly modifying the results. Erlang may not have explicit inheritance, but people almost certainly implement inheritance-like actor structures to achieve extensible message dispatch.
> inheritance is just automatically delegating messages to another actor and possibly modifying the results.
I don't think this is true. Objects can message themselves as part of base-class code; implementation inheritance means that this has to involve a level of indirection and dispatch, not just explicit delegation.
> completely wrong and ruined our perception of OOP.
or maybe Erlang is its own thing and OOP -as what most people are thinking when talking about it- is the set of practices followed in languages descended from the cross between C and Smalltalk.
This explains how every introductory course in OOP using either Java, C#, Python, etc. will list some sort of OOP principles always including something along "objects talk to each other though message passing" which is hand-waved simply as something to do with taking other objects by ref
// taking a reference
bar.someMethod(foo)
Or some form of import method
import Foo;
class Bar {
someMethod() {
// init the import
foo = new Foo()
// ... do something with it
foo.someMethod()
}
}
which is worse since you need a huge disclaimer to explain that in practice you need to account for Dependency Injection and something for Mocking.
Go and Rust OOP view of the world are also possible in C++, Java, Python, C#.
It is a matter of actually learning about OOP concepts in general, and Kant features in particular.
Swift's protocol oriented programming actually goes back to Objective-C protocols, Java's inspiration for interface types or Smalltalk traits (post-Smalltalk-80).
What most people think of when they think OOP is C++, Java, and Python which got OOP completely wrong and ruined our perception of OOP. They also tried to use their misshapen OOP hammer on every problem they could find to the point that it’s an overused meme. These days you see languages actively trying to distance themselves from OOP as a form of enticement (Rust and Go being the two prime examples).