Is Julia's way of OOP superior to C++/Python? Why Julia doesn't use class-based OOP?

OK, so I guess I need to repeat my example. Perhaps not the best example but I won’t change so as not to introduce even more perturbation.

Suppose you want to work with a hierarchy of objects (say abstract type foo and abstract subtypes foo1 and foo2) which implement a method for ‘+’ (not your work so you don’t own these classes and subclasses). You notice some objects in foo1 (not all) and some objects in foo2 could use a more efficient algorithm for + since the field coeff which needs to be sorted during addition is already sorted in them.
To solve such a problem by composition, you would make a new type which has a field coeff which is sorted This is not possible here since the field coeff is owned by the hierarchy and you cannot hijack it.

A natural solution to the problem would be an Interface based on the propriety: coeff is sorted.
So let’s try to make one with a trait. To express it as a trait you do

abstract type CoeffSortedStyle end
struct CoeffSorted<:CoeffSortedStyle end
struct CoeffNotSorted<:CoeffSortedStyle end

Then comes the bad step: you need to make your supertype foo aware that there is a trait below it (of which there is no logical need — it is just the implementation of traits which forces you to do that):

CoeffSortedStyle(X::foo)=CoeffNotSorted
Base.:+(x::foo,y::foo)=+(::CoeffSortedStyle(x),x,y)

And, as observed by Mason, you cannot do this if you have no ownership of foo.
Only then you can define what you really want:

Base.:+(::CoeffSorted,x::foo,y::foo)=# routine which takes in account that coeff is already sorted

In a language with interfaces, you would do something like

Interface CoeffSorted end
Base.:+(x::{foo,CoeffSorted},y::{foo,CoeffSorted})=# routine which takes in account coeff is sorted

and that’s all. No need to interfere with addition for foo or subtypes of foo . I do not think any clever macros can bring us there without some change to the language. But I would be happy to be contradicted on that.

6 Likes

Can this be solved with Base.:+(::CoeffNotSorted, x::foo, y::foo) = invoke(+, Tuple{foo, foo}, x, y) ?

I do not know if it is your code or my mind which goes in an infinite loop but one of them does…

Well, maybe I just mistunderstood you problem originally. Effectively, you want to override + behaviour for some values of types foo1 and foo2, right? Then it makes sense this is not allowed without type piracy (your +(foo, foo) method) because what you want is piracy by itself.

In this particular example, I would replace CoeffSortedStyle with sorted_coefs, a function that returns the sorted coefficients.

1 Like

One of the challenges with describing a new feature is that the proposer necessarily relies upon simplified problem description(s) to get their point across without burdening readers with complexity. However, by doing this, they open themselves up to critiques which refute the simplified example, rather than discussing the general pattern. It’s a hard challenge to see the world differently. Anyway, I still don’t grok the issue… but, perhaps it’ll emerge. Perhaps it’s worthy of a PR that’s focused on describing it in a “pattern” style, that is the challenge faced, the forces that guide/restrict solutions, various options with their positive/negatives, which includes both existing work-around and the proposed improvement. The existence of a workaround for a set of cases shouldn’t negate the proposal though. I mean, anyone could implement object oriented programming by adding to each structure a pointer to a function table, or refute object oriented by showing how an enum + switch covers 80% of the cases (and the other 20% are rejected as being “so clever, you’d never want to do it in a production system”).

6 Likes

Sure, and “maybe there’s a slower algorithm that works with unsorted coefs, but is faster than sorting the coefs”.

This actually reminds me of the earlier comment:

Putting the challenges of fair benchmarks aside, the reason speed is emphasizes isn’t because it is the ultimate advantage, but because it’s the most concrete, the easiest to understand and explain. The easiest to map towards past experience: we’ve all waited on code to finish running, but we haven’t all built a large project following the design principles and idioms of different languages to see their strengths and weaknesses. And new comers to Julia in particular don’t have those experiences within Julia itself.
So, how can these advantages and principles be conveyed?

I actually agree with @Jean_Michel’s gripe. It’s a problem I’ve had recent in VectorizationBase.jl, which defines AbstractSIMD{W,T} <: Real, which represents SIMD elements that contain W instances of T.
These are supposed to act like single elements rather than collections, so aside from not supporting branching, they try to mimic a T.
However, this breaks down because for Vec{4,Int} <: AbstractSIMD{4,Int}, unfortunately Vec{4,Int} <: Integer and Vec{4,Int} <: Signed return false, and Vec{4,Float64} <: AbstractFloat returns false as well.

Makes me wonder if I should instead define many separate struct types:
SignedVec <: Signed, UnsignedVec <: Unsigned, FloatVec <: AbstractFloat for better compatibility with user code.
But Vecs aren’t the only type, there is also a VecUnroll type used for representing a set of unrolled vectors; these must now also be replicated for each separate instance that they’re supposed to fit in.

Then I’d need to define

const Vec{W,T} = Union{SignedVec{W,T},UnsignedVec{W,T},FloatVec{W,T}}
const VecUnroll{N,W,T,V} = Union{SignedVecUnroll{W,T},UnsignedVecUnroll{W,T},FloatVecUnroll{W,T}}

for conveniently using these in generic method signatures myself, with of course the note that this isn’t easy to extend.

Maybe I should do this, but it certainly is messy, and definitely looks like a symptom of sub-optimal design.
In contrast, I currently have just the Vec and VecUnroll, but need to deliberately extend any functions with specific ::Integer type signatures. Beyond method signatures, sometimes packages, like MultiFloats, provide restrictions on the types of their fields.

At the moment, I am seriously contemplating making this switch.

Traits are better than these type hierarchies because they’re orthogonal, which makes them more composable. But type hierarchies are still pervasive throughout code, and I confess to still making a lot of use of themself, because they – the worse solution in effect – are better in terms of convenience and ease.

9 Likes

This is a nice example @Elrod. This is one of the situations where it’d be quite handy to be able to make Foo{T} <: T. The other ones I’m aware of off the top of my head are

  • Symbolic types: when making a symbolic programming system that models julia code, it’d be really nice if you could have say Symbolic{Integer} <: Integer and Symbolic{DenseArray} <: DenseArray.
  • Autodiff: in automatic differentiation via operator overloading, you want a Dual{T} <: T so that you make sure you go down the right code-pathways for e.g. Real versus Complex primals.

A trait based approach would be quite nice for this.

6 Likes

I asked for an MWE since it is not clear what “classes” are in Julia. Abstract types? With what part of the API exposed? Or are foo1 and foo2 concrete? [Also, using standard Julia conventions (Foo etc) would help, but are not essential.]

Also, it is not clear to me why a package that owns neither the foo types (foo, foo1, foo2) nor + should be defining methods for them. Perhaps I misunderstood what’s where. In any case, I reiterate that an MWE would really help, eg along the lines

module WeDontOwnA
export Foo
abstract type Foo end
end
...

Sorry. foo, foo1 and foo2 are of course abstract types. I fixed my post.
My actual MWE came while trying to port to Julia the Chevie GAP package. It is full of mathematics (Coxeter groups, complex reflections groups, Hecke algebras) so I tried to transpose.
As for not-ownership, it is not uncommon for a package to be there just to improve the performance of another one. In my actual case, I have ownership, but it is a maintenance problem that all classes must be aware of traits below them.

2 Likes

On the specific issue of speed, I believe it’s worth advertising that Julia shines relative to Python + Numpy, etc. in the many, even typical, cases where we’re mixing looping and complex logic. I like the examples given here and the quote “However, to take full advantage of Numpy functions, you have to think in terms of vectorizing your code. And it is not easy at all to write complex logic in a program in the form vectorized code all the time. Therefore, the speed comparison with Julia should be done for situations where somewhat complex logic is applied to an array for some kind of processing.” Maybe someone can suggest a universal-enough example to be included as a benchmark…

4 Likes

That’d be a cool feature for all the times I made a New{T} heavily based on an existing type T and just wanted the existing methods for T to just work on New{T}. But it seems like a source of dispatch ambiguities: f( New(0) ) couldn’t pick between the methods f(::Integer) and f(::New). I’m imagining that the only way this can be avoided is if we somehow promise f(::New) won’t exist, that we designate New{T} as an imposter type of T that borrows all its methods except critical Base methods like (+).

I hope I’m not unfairly picking quotes out of context but in arguing the benefits of multiple dispatch I thought that there was a contradiction in

and from @oxinabox’s JuliaLang: The Ingredients for a Composable Programming Language is

“Name collisions make package authors come together and create base packages (like StatsBase ) and agree on what the functions mean.”

Happy new year to all and thanks for being such a supportive and stimulating community.

No that’s a misunderstanding. Stefan is saying that it’s possible for people to extend someone’s code without them knowing. What Lyndon is then saying is that people tend to actually do that (people actually do extend each other’s code), and then there starts to be more and more functionality that gets shared between packages, and so then people tend to make Base packages so that way different parts of the ecosystem can depend on each other without having to depend on each other.

For example StatsBase.sample is used all over the place, but the authors of Distributions.jl probably don’t know how Turing.jl has changed StatsBase.sample. It all works together though. This function is then in a Base package so that way anyone who wants to extend sample doesn’t need to have a big dependency like Turing.jl or Distributions.jl, they can have a simpler more direct dependency that just defines some simple statistics functions. Another great example of this is Tables.jl which defines the table interface that everyone extends. Tables.jl doesn’t know all of the table packages, nor does it need to, but they all work together by extending this common base. An example of a table that would never want to inherit from a table class is the ODESolution.

So the existence of Base packages is really showing that there are very deep connections throughout the ecosystem, so much that without Base packages llike ArrayInterface.jl you’d have that every package depends on DifferentialEquations.jl, Turing.jl, DataFrames.jl, etc. which would just be an insane dependency tree. Instead, there are small little packages for “here’s the names we agree on, go hog wild!” which then have thousands of packages extending the interface.

8 Likes

While I agree with your comments about the general principle, I don’t think this is a particularly good example — the uses of sample in Turing and StatsBase don’t have anything to do with each other, so it is just recycling the symbol for something completely different, not an example of generic programming. Technically fine, but not good style. See this topic.

Generally, defining & exporting a function f in a ...Base package is useful if it is part of a generic interface other packages may extend, or just use without restricting to a particular type in mind. Nice examples include

4 Likes

Both are sampling points according to a distribution, one is just implicitly defined and the other is explicitly defined. I think more an more, a Turing model itself should be treated as a distribution. If the abstraction is done right then you should be able to use a Turing model as a prior distribution for another Turing model. The connection isn’t there yet but I don’t see a reason why it shouldn’t be in the future.

1 Like

Is Julia’s way of OOP superior to C++/Python? Why Julia doesn’t use class-based OOP?

@vedasulo Your question hits the nail on the head – Java, C++ and (weakly) Python are Class-Oriented Programming (COP) languages, and COP requires multiple-layer inheritance with concrete class sub-classing to resolve the problems discussed above. Julia’s OOP is superior precisely because it’s not COP. If I am allowed to say that a struct + methods = object (and let’s not worry that an object is an instance of a class), in COP methods belong to a struct, whereas in Julia’s Method-Oriented version of OOP, the structs belong to the methods. Both are OOP. Both use a combination of methods and structs to describe behaviour of objects.

I :heart: Julia.

2 Likes

Oh and Happy New Year everybody! :clinking_glasses:

4 Likes

Sure, but the signatures are so different that with the current design, it is very unlikely that one could use sample in generic code, like +. Which is what you would expect, as StatsBase’s is sample methods are low-level utility functions, while in Turing they are part of the high-level API.

Note that I don’t indend to single out Turing here — other packages do similar things, and I have done this myself. The idea that recycling a symbol for a completely different, non-conflicting signature may not be a good design for Julia evolved with practice and I think is a good principle to follow.

:slight_smile: I loved the syntax as easy as possible. Also I was quite learning about Meta programming and things, also found in lisp and like computerphile telling why was its popular language.
I was quite surprised the power of Metaprogramming , data as code and things and also easy to write and know things.
Only hurdle is , How fast is industries adopting julia knowing these powerful things?