module ClassName # class Name {
using # include<> // should be outside
import # include<>
export function # public function;
var = 1 # private static var;
end # }
I’ve never seen a problem that foo.bar(x,y)
can solve, but bar(foo, x, y)
can’t.
There are things, however, that are pretty easy to do in OO languages that can’t be done in Julia; one such thing is adding/changing behavior through inheritance. That’s pretty easy to work around, though - instead of the following (which won’t work in Julia, but using Julia syntax to make comparison easier)
immutable Foo
x
end
foo(f::Foo) = f.x)
bar(f::Foo) = 2 * f.x
immutable Bar <: Foo
y
end
bar(b::Bar) = b.y * b.x
where Bar
has an x
field inherited from Foo
, and overrides behavior for the bar
function, you can easily do this:
abstract AbstractFoo
immutable Foo
x
end
foo(f::Foo) = f.x
bar(f::Foo) = 2 * f.x
immutable Bar
y
f::Foo
end
foo(b::Bar) = foo(b.f)
bar(b::Bar) = b.y * b.f.x
In other words, by using composition instead of inheritance, you can extend and amend the behavior of any object with all the power you have in inheritance. (You could also create an abstract type that both Foo
and Bar
inherits, to make them part of the same type tree and make dispatch a little simpler.) This is heavily relied on in a couple of recent additions to Interpolations.jl, that provide axis scaling and extrapolation through composition - had I written that code in Java or C# (or C++), I probably would have used inheritance instead.
But I agree with Tobias and Simon - there is very little reason to try to make Julia more object-oriented. All the power you get from OO is still there, but you might need to think a little differently to harness it. If you have a concrete problem, along the lines of “this is how I’d do it in OO, but I don’t know how to do it in Julia”, the community is usually pretty good att figuring something out. A recent example was that someone wanted man.eat(food)
rather than eat(man, food)
. A little thinking outside the box turned out the IMO superior solution - instead of changing the language to make it read better, just change the verb: feed(man, food)
.
Julia isn’t object-oriented, and will probably never be. But that doesn’t make it less powerful :)
// T
Besides the current flaws,
I think it's a better model for numerical computing,
it yields concise code, easier maintenance
and it's easier to add feature to an existing code base than it is in OOP.
I think Julia should move forward by not mimicking OOP further, but instead improve the current model - there's lots to do already.
Wasting time by chasing after features from other languages will make it much harder to turn Julia into a well rounded, sound language. (this only applies to feature that get chased without a concrete problem)
But I agree with Tobias and Simon - there is very little reason to try to make Julia more object-oriented. All the power you get from OO is still there, but you might need to think a little differently to harness it. If you have a concrete problem, along the lines of “this is how I’d do it in OO, but I don’t know how to do it in Julia”, the community is usually pretty good att figuring something out. A recent example was that someone wanted
man.eat(food)
rather thaneat(man, food)
. A little thinking outside the box turned out the IMO superior solution - instead of changing the language to make it read better, just change the verb:feed(man, food)
.Julia isn’t object-oriented, and will probably never be. But that doesn’t make it less powerful :)
Going a bit on a tangent, I claim that object-oriented programming, at least as embodied in C++, is not really suitable for scientific computing and in fact has led certain codes to go astray.
To give a simple example, suppose you are told to write a 'matrix' class in C++ that contains all the usual operations. You finish writing your class, and a week later, you are told to derive 'symmetric matrices' as a subclass of 'matrices'. This is trouble! Unless you wrote your original matrix class in a very particular way, you will have a hard time writing a symmetric matrix class that inherits from a matrix base class. This is not just a theoretical example; I saw someone try to do this (unsuccessfully) in a code he had written.
The problem with object-oriented programming is that you have make design decisions when you write the base classes that lock you into a certain path. This path may not be compatible with the flexibility needed for scientific software projects.
I for one am glad that the designers of Julia decided not to make Julia an object-oriented language, at least not in the C++ sense of the term.
module ClassName # class Name {
using # include<> // should be outside
import # include<>
type TypeName
export TypeName, fun # public fun;
var = 1 # private static var;
function fun(obj::TypeName, arg)
end # }
abstract Shape
type Rectangle
width::Int
height::Int
end
type Square
width::Int
end
Base.size(r::Rectangle) = (r.width, r.height)
Base.size(s::Square) = (s.width, s.width)
type Wheel
radius::Int
end
type Frame
length::Int
end
type Seat
cushiness::Int
end
@compose Unicycle
Wheel...
Frame...
Seat...
end
I am confident that a new OOP language ScalaJulia is possible integrating syntax of both functional languages Scala and Julia.Thanks,SS
Here's an example where there would normally be a strict hierarchy in classic OOP: Square <: Rectangle <: ShapeBut it actually makes much more sense in Julia (why have 2 fields for Square??):
Now I completely agree that ensuring a compile-time agreement of interface is a valuable part of the language (but should ideally be optional). People do see value in this, and it's an active design discussion. (search for "traits")
For composition vs inheritance, I completely agree that sometimes it's nice to save code/typing when you have structures with a bunch of fields that are mostly the same. I'd argue you don't actually care about inheritance, you just want composition but without the extra layer of structure.
Above, a relatively simple macro can replace all the "Type..." with the fields of the composed types, applying multiple inheritance of the structures without the baggage required in classic OOP. Then you can compose your type from other types, but without having to write "unicycle.wheel.radius"... you can just write "unicycle.radius".
ScalaJulia is a skunkworks project Martin and I have been working on for a while now. The hardest part so far has been deciding between whether to call it ScalaJulia or JuliaScala. Other names we considered: Julala, Scalia.
The US Supreme Court may get involved if you call it Scalia. Hard to know in which direction.
> Above, a relatively simple macro can replace all the "Type..." with the fields of the composed types, applying multiple inheritance of the structures without the baggage required in classic OOP. Then you can compose your type from other types, but without having to write "unicycle.wheel.radius"... you can just write "unicycle.radius".
As Stefan mentioned, Go is not traditional OO. This "composition-based" approach is part of Go's OO approach. In go, a type can have named fields of different types (like most OO languages), but also has "embedding". For example, if one declared
type Unicycle struct {
Wheel
Frame
Seat
}
then you could access the individual fields directly. So, if "u' is of type Unicycle, then you could access/set u.Cushiness directly. Similarly, if the embedded types had methods, i.e. frame.Color(), they can be called directly through unicycle, u.Color(). This is similar to, but distinct from, inheritance (the difference is that the Unicycle type doesn't actually have these fields, just the ability to call them easily, which is significant because of interfaces). In the case of clashes, the program must specify which is meant, so if both Seat and Frame had a Color() method, the user would have to specify u.Frame.Color vs. u.Seat.Color. In Go, this is handled by the compiler, and would need to be handled somewhere in the Julia runtime/REPL. It's a very nice paradigm, but would have to be made to fit within the broader Julia context.
# julia allows Unicode names (in UTF-8 encoding) # so either "pi" or the symbol π can be used return 4/3*pi*r^3 end
object Julia { def sphere_vol(r : Int) { return ( 4/3*pi*r^3 )
} }
I do like this approach to composition/delegation, but it requires automatically adding a lot of methods to the delegator, i.e. Unicycle in this example, from all of its components, which feels kind of nasty and dangerous, especially since we allow dynamic addition of methods after-the-fact. In other words, you might add a method to Wheel, Frame or Seat at any time, which would presumably then also apply to Unicycle objects, potentially with unexpected consequences. It would be nice if the delegation was more limited than that. Go doesn't have this problem since you can't dynamically add methods – everything is known at compile time.
I think this is generally in-line with what I've been advocating for, and not that different from Scala's mix-ins:trait Wheel {// ...}trait Frame {// ...}class Unicycle extends trait Wheel, Frame {// ...}is essentially doing the same thing (i.e. copying the member variables and methods over).
Ah, okay, that is different then. What's the advantage of creating a new method versus copying the fields? I would imagine there is a penalty with each deference you have to follow in order to make that work.
An OO approach is really just specifying an interface in a formal manner. The second you write any type of interface, you always risk making a choice that will haunt you down the road. I don't see the difference between:class Foo {float getX() { ... }float getY() { ... }}and:type Foo { ... }function getX(f::Foo) -> float { ... }function getY(f::Foo) -> float { ... }except that an instance of `foo` is being passed implicitly in the first case. To that extent, I would say Julia OO (i.e. on the dispatching). Where it is not OO is in the layout of the data.
It is true that with a more OO language, because of its formalism, you run the risk of declaring variable types in a parent class that might not be correct in the child classes, so some planning is required. However, what you gain is better code in the long run. For example, I might have:abstract Baztype Foo <: Baz {x::Float}type Bar <: Baz {x::Float}and an interface:getX{T <: Baz}(b::T) -> Float = b.xwhich works fine, except maybe someone else comes along and writes:type Blob <: Baz {myX::Float}Now to fix your interface, you have to write a separate `getX` for `Blob`. This might not seem like a big deal, except you might not catch this issue until run time (I don't think there is a way a static-checker could identify the problem). Imagine a large library or base of code, with many people working on the code, and you suddenly are exposed to a large number of issues.