Designing abstract type and related abstract functions

I want to implement a type from where i would like to build other subtypes.

The supertype would be something similar to this.

Abstract type A end
function process end
@keywdef mutable struct B<:A
         a
         b
    end
function B(items::AbstractArray)
      a= items
      b=Dict()
     
     for (i,x) in enumerate(items)
            b[x]= i
     end

      B(a,b)
end
function process(object::B)
     object.b
 end

What I want to achieve is a sort of blueprint in a supertype which can later be customized for any subtype.

The idea here is that I want a type A which would define a set of functions (optional and required). Later I can create subtypes of A implementing methods to the functions of A.

For example the I can define an optional empty function process for types A and then later for B (subtype of A) I can implement the method.

I am still learning the ropes in julia so I wanted to know if the example that I have given is the correct way of doing what I have explained.

Hard to say, because what you posted isn’t valid Julia code, and it is unclear to me what you want to do specifically.

Note that abstract types are useful mostly for dispatch. If you just want to define methods for something, you don’t need an abstract type.

I have edited the code I shared . I think I have made some mistake in some keywords. I am comparatively new to Julia, so I think I have made some mistakes in the sample code.

Specifically I want to do something similar in Julia like the sample python code below–

class A:
   def __init__(self, items:list):
         self.items = items

   def process(self):
         b=Dict()
         for (i,x) in enumerate(items):
               b[x]=i
         return b
             

Then I any other class can inherit the class B.

class C(B):
#further operations

Sorry, its a long time since I used Python, so I cannot help you as I still don’t know what you are trying to do.

no issue @Tamas_Papp. Maybe I will come up with a better example.

As @Tamas_Papp mentioned, abstract types (and types in general)) are primarily useful for dispatch (i.e. so you can get a method that works differently on types A and B). Yes, this could be a reasonable way to get what you want. Note, however, that types are not an analogue of classes. They are essentially completely different paradigms.

You could define:

abstract type Processable end

# Should this give an informative error? 
# A fallback method? A high level interface?
function process(p::Processable)
    return Dict(p.items .=> eachindex(p.items))
end

struct A <: Processable
    items::Vector{Int}
end

# Let's say we have some other processable, that 
# guarantees unique-ness by construction.
struct UniqueProcessable <: Processable
    items
end

function process(b::UniqueProcessable)
    # This one can do something different from A
    # and other generic Processables
    # (maybe more efficient) because we know it's
    # guaranteed unique!
end

The above would work and could be a good idea, in case that’s what you intended. Not every problem is best solved in this way though. It makes sense to go this route if multiple Processables should handle the same method (the same abstract operation) in a different way. For example, arithmetic on Float64, vs Int64 works differently, but they both are Real numbers, and we can write generic code for them this way, that we can specialize as needed.
If you’re trying to design an “interface” (rather than sort out dispatch) you can often do so in other ways that may be equally good (sometimes better). For example, instead of the above, you could do:

# this part is generic:
function process(p)
    return Dict(reverse_pairs(p))
end

# this part relies on types and dispatch:
reverse_pairs(a::AbstractVector) = a .=> eachindex(a)
reverse_pairs(a::AbstractDict) = values(a) .=> keys(a)
# you can add more methods to `reverse_pairs` to get
# `process` to work like it should.

The above is how generic code must be implemented. For example, plenty of code (in Base and other places) that works on iterators must work on anything iterable, and yet there is no “AbstractIterator” (it would be too all-encompassing). Therefore it uses the “iterator interface”, functions like iterate, length/size, etc. to accomplish this.

Note that in the second example, reverse_pairs depends on types and dispatch to work in each case, but process itself is generic.

There’s a lot more to be said on the topic, really, but I think the best thing is just to continue increasing familiarity with the language. All of this isn’t unique to julia, but manifests itself a bit idiosyncratically.

3 Likes

@tomerarnon Thanks a lot. You had explained this very clearly.

Your first example is closer to what I want .

I want multiple processables to handle the same abstract operation in a different way.

For example process(b::UniqueProcessable) can guarantee uniqueness as you said. But I can have some other processable which can for example make a copy of the items(just an example).

In other words I wanted an abstract type and then other types belonging to the same hierarchy would be able to do something more customized.

1 Like

What you are asking here is how to implement object-oriented (OO) design paradigm called inheritance in Julia.
However, Julia is not an OO language so you cannot directly map OO paradigms here. Rather than that Julia is using multiple dispatch which is a different concept and usually requires different approach (however in the end there is some similarity).

Perhaps the best reading for the beginning is this SO thread: https://stackoverflow.com/questions/33755737/julia-oop-or-not

Basically, the replacement for OO programming in Julia is multiple dispatch combined with polymorphism - and here is a useful tutorial how to do it: https://github.com/ninjaaron/oo-and-polymorphism-in-julia

4 Likes

@pszufe that’s a very nice tutorial you shared.