Julia equivalent of class methods?

I’m translating some Python code to Julia and am trying to figure out how to map the usage of class methods to Julia. See below for a Python-style outline of what I’m porting. There’s a single abstract class with one or more concrete subclasses. Each concrete subclass implements an action that is applied to a state (it’s code from a game simulation). One particular thing is that each subclass can generate actions of its type from the current game state, which is what generate() does. It returns a list of one or more actions that can later be applied to a state by calling execute() on the instance, passing the state to apply to.

ACTION_CONCRETE_A = 1
ACTION_CONCRETE_B = 2
...

class AbstractAction:
    def __init__(self, type, ...):
        # Hold some general fields
        self.type = type
        ....
        
class ConcreteActionA(AbstractAction):

    @classmethod
    def generate(cls, state, ...):
        # Returns a list of one or more instances
        # of ConcreteAction possible in this state
        return [ConcreteActionA(...), ...]
        
    def __init__(self, ...):
        AbstractAction.__init__(self, ACTION_CONCRETE_A, ...)
        # Action specific fields
        ...
        
    def execute(self, state):
        # Apply the action to a state
        ...

state = <game state>
actions = ConcreteActionA.generate(state)
# Pick a random action and execute it
action = random.choice(actions)
action.execute(state)

Porting the class structure doesn’t seem to be too hard (apart from the composition versus forwarding choice), including the constructor and execute() method, something like this:

@enum ActionType
    ACTION_CONCRETE_A
    ....
end

abstract type AbstractAction end

mutable struct ConcreteActionA <: AbstractAction
    type::ActionType
    # ...more action-specific fields

    function ConcreteActionA(...)
        obj = new(ACTION_CONCRETE_A)
        # Initialize other fields....
        return obj
    end
end

function execute(action::ConcreteActionA, state::State)
    # ....
end

But what’s a good way to port generate()? Ideally I would be able to call

actions = generate(ConcreteActionA, state)

but I’m having trouble figuring out how to dispatch on a struct type (in contrast to a struct value), as I have many different forms of generate(), one for each action type. I looked into symbols to see if those could be usable here (e.g. :ConcreteAction), but I’m not clear whether that is a good method.

Any hints?

1 Like

I think you’re looking for the Type{...} singleton type. Illustrating the concept with an example

julia> struct Foo
          val :: Int
       end

julia> foo = Foo(42)
Foo(42)

julia> fun(x::Foo) = "dispatch on the type of a value"
fun (generic function with 1 method)

# This might be compared Python's class methods
julia> fun(::Type{Foo}) = "dispatch on a type itself"
fun (generic function with 2 methods)

julia> fun(foo)
"dispatch on the type of a value"

julia> fun(Foo)
"dispatch on a type itself"
3 Likes

Slight tangent, but I think the enum is superfluos. Multiple dispatch already handles execute(::ConcreteActionA, state) vs. execute(::ConcreteActionB, state), without the need to access any additional type field in that Action object :slight_smile:

If you’re looking for performance, you might even get away with having each action be immutable as well. You’d move the new to the end of the constructor and pass it all fields directly in that case. This is very useful if you don’t plan on modifying existing actions after they have been created (and even if you do, creating new immutable objects from existing ones (which is how you get modification on immutables) can usually be avoided by the compiler).

4 Likes

It seems to me that you want Val(:SymbolForAction)? This will increase the compilation time (but possibly improve the running times), and you will be able to dispatch in the actions.