Inherit.jl

I’m happy to announce the first release of Inherit.jl. The package provides a simple way to inherit fields and interface definitions in Julia. it is currently available in the general registry (] add Inherit).

Example:

using Inherit

"
Base type of Fruity objects. 
Creates a julia native type with 
	`abstract type Fruit end`
"
@abstractbase struct Fruit
	weight::Float64
	"declares an interface which must be implemented"
	function cost(fruit::Fruit, unitprice::Float64) end
end

"
Concrete type which represents an apple, inheriting from Fruit.
Creates a julia native type with 
	`struct Apple <: Fruit weight::Float64; coresize::Int end`
"
@implement struct Apple <: Fruit 
	coresize::Int
end

"
Implements supertype's interface declaration `cost` for the type `Apple`
"
function cost(apple::Apple, unitprice::Float64)
	apple.weight * unitprice * (apple.coresize < 5 ? 2.0 : 1.0)
end

println(cost(Apple(3.0, 4), 1.0))
6.0

It works with multiple levels of inheritance and across different modules. The package grew out of a simple idea but implementing was quite tricky and was not really mature for several months. This version I believe is finally usable for most people. Feedback and contributions are welcome!

Kirby

13 Likes

Is this (partially) redundant with many other OOP packages (there’s also OOPMacro.jl and CBOO something):

I’m not sure what you’re getting here that you don’t get from Mixers.jl, which is only 100 lines, and is independent from the inheritance mechanism.

Hey, guys, let’s keep it positive.

First of all, congratulations on your package!

Maybe, could you elaborate a bit more on the differences between your package and those mentioned?

Thanks, and congrats again!

6 Likes

The main difference I can see is they don’t seem to verify method signatures. Their interface seems to be only checking for a function name?

Found this post about ObjectOriented.jl

This problem is avoided in Inherit.jl,

a = Apple(1,2)
a isa Apple && a isa Fruit
true

Before I developed Inherit.jl I had looked at Classes.jl. One of the issues was they used a parallel type hierarchy to what the user actually typed. It seems ObjectOriented.jl has a similar problem. Inherit.jl uses native Julia type hiearchy. It introduces no new types. There is no dispatch overhead at all.

This also restricts the current system from having multiple inheritance. MI will use parameterized container types and introduce the <-- operator. But the main object hierarchy is still the Julia one. MI introduces Traits that decorates on this hierarchy.

2 Likes

Mixers.jl only inherit fields not interfaces.

Thanks. This has method signature verification. You can always dispatch to the signature you see.

1 Like

Sorry to be negative, there are a lot of attempts at these packages and mostly they don’t end up being used. I don’t users Mixers.jl at all. The solution ends up being use composition 99% of the time.

I also couldn’t understand how you were enforcing interface compatibility, but I see that its using eval during __init__. That’s really discouraged from a precompilation standpoint these days. Maybe you could do it during precompilation?

1 Like

Nothing wrong with negative feedback when it contains valid points.

Inheritance offers more natural, cleaner syntax than composition in many cases. It also naturally has zero dispatch overhead. It’s bad if not used well, but obviously no OOP package stops user from using composition.

I wouldn’t use Classes.jl or Mixers.jl myself. ObjectOriented.jl is newer and seems people are excited about it, but it sounds like some true use cases are not presented clearly.

Inherit.jl does several things practically:

  • native Julia types, what you type is what becomes a Type.
  • no dispatch overhead.
  • guaranteed interfaces
  • allows docstrings

It should be a lot closer to Java than Python mentally. Single inheritance, multiple interfaces (eventually when traits are implemented).

Another difference is being the latest OOP package, it relies on an important Julia 1.9 feature to allow you to evaluate a subtype in a supertype’s module context. So this would not have been possible pre 1.9. In fact after 1.9.0 was released I put more time into releasing the package, which I was developing under 1.9.0-RC2.

ps. The init it generates doesn’t do anything during precompilation. It only goes to work during runtime. Package level tests are included. With more users we can discover if there remains to be eval problems.

p.p.s there were several reasons to do verification during runtime, one of them was docstrings. But if eventually everything can be made to work during precompilation, that would actually be better.

2 Likes

I like the look of this. Julia’s lack of enforcing interfaces is IMO one of its greatest obstacles to writing reliable code.

4 Likes

Init actually is run during precompilation :wink:

Putting code in init means longer load times if all interfaces need verification. Imagine something complicated like AbstractArray.

The interface idea is great (I had a go in Interfaces.jl) but Im not sure about runtime checks. You may as well do them in precompile if you have them in init.

You can generally define docs in your macros, although yes some things are difficult to generate if you e.g. need functions defined on the structs

To solve the loading time issue. You can SkipInitCheck globally, and change to ThrowError only during tests.

Would be nice to see a short example where benefits of this approach are highlighted, compared to regular Julia composition. Is it just the interface checking, or something else?

Trying to understand potential usecase, where/when should I reach for Inherit.jl?

3 Likes

The doc right now is hosted on github (I’m not sure how to get it into Juliadocs): Home · Inherit.jl (mind6.github.io). If you look at the Fruit, Berry, Apple, BlueBerry examples, composition would lead to less natural syntax and also make it hard to publish what the interface really is.

For me I use it when I’m repeating field definitions, or when it’s not clear which method calls are still valid on an object.

I for one do not actually know how to achieve this with “composition”, as some suggest. Could someone write up the example in the OP using composition?

1 Like

So a problem is, artifial examples used for illustrating inheritiance work great.
but that’s cos they are artificial, and so use things with strong “isa” relationships.
“Is-a” relationships in practical code tend to actually be rare,
and when they do occur often do not cooccur with having identical “hasa” relationships (i.e. fields) to the parent type.
For example: TreeDict and HashDict both have an isa relationship with Dict but totally different set of fields.

Anyways we can write this code from the OP with composition if we like.
It looks a bit clunky because it is.
This is the code that is ideal for this kind of inheritiance – otherwise it would be poor to put in the example.
It is just questionable how common this kind of code occurs in the wild.

abstract struct Fruit end
struct GenericFruit <: Fruit
	weight::Float64
end

cost(fruit::GenericFruit, unitprice::Float64) = fruit.weight * unitprice

struct Apple <: Fruit
    base::GenericFruit
	coresize::Int
end
# Convienience Constructor
Apple(weight, coresize) = Apple(GenericFruit(weight), coresize)

function cost(apple::Apple, unitprice::Float64)
    basecost = cost(apple.base, unitprice)
    return basecost * (apple.coresize < 5 ? 2.0 : 1.0)  # small cored apples are 2x typical cost
end

Then we use test suites to check interaces

using Test
function test_interface(fruit::Fruit)
    @testset "Fruit Interface: $fruit" begin
        @testset "Must implement Cost" begin
            @test cost(fruit, 1.0) isa Any  # make sure it doesn't error
            # But can test more sophisticated
            @test cost(fruit, 1.0) isa Real  # check return type
            
            @test cost(fruit, 1.0) <= cost(fruit, 2.0) # Check monotonicity
            
            
            # Check linearity if that's wanted:
            @test iszero(cost(fruit, 0))
            @test cost(fruit, 1.0) + cost(fruit, 2.0) = cost(fruit, 3.0)
            @test 2*cost(fruit, 10.0) = cost(fruit, 20.0)
        end
    end
end


test_fruit(Apple(3.0, 4))
test_fruit(Apple(5.0, 1))

No criticsm to the OPs package.
I am sure it does exactly what it sets out to do and is very useful in the circumstances where it is is useful.

6 Likes

We can try to build a collection of useful demo classes in future releases. Demo’s are not complete enough to warrant their own package, but have useful code and ideas in them. For example I have wanted to extend SparseMatrix without using composition which requires replicating the entire huge interface on matricies. Using Inherit most of the functionality can be in an @abstractbase, with simple end user concrete implementations that can be extended with just a few extra methods.