How do I add a function/method to an already defined module

question
#1

This is a follow up question relating to my question on sub modules:
Discourse question on submodules

Okay. My issues seems to be around circular dependencies. I have an engine that wants to call methods that some “other” code defined outside of the engine. The problem is that that other code also needs to reference “things” defined in the engine. For example, I have an AbstractNode defined in the engine and so my client (aka the game) needs to reference it. The engine—when it is running— will then call these “generic” methods.
The issue is I don’t want the engine “hard coded” on the game methods because that would negate the generality of the engine. The engine should server any game.

Any ideas on how to solve this? :expressionless:

I put together a simple demonstration of attempting to do such a thing. The code is also at Repl.it code.

Naturally I get the error:

ERROR: LoadError: UndefVarError: transition not defined
Stacktrace:
 [1] getproperty(::Module, ::Symbol) at ./sysimg.jl:13
 [2] top-level scope at none:0
 [3] include at ./boot.jl:326 [inlined]
 [4] include_relative(::Module, ::String) at ./loading.jl:1038
 [5] include(::Module, ::String) at ./sysimg.jl:29
 [6] include(::String) at ./client.jl:403
 [7] top-level scope at none:0
in expression starting at /home/runner/main.jl:44

Example code:

# Game is the module I want to augment with a new
# function called `transition()` in which the engine will call
# But I can't define transition yet because AbstractNode isn't
# defined yet, hence the chicken/egg conundrum.
module Game end

module Ranger
  module Nodes
    using ...Game

    # Simple demo node for example ----------
    abstract type AbstractNode end

    mutable struct Node <: AbstractNode
      x::Float64
    end

    crossnode = Node(1.0)
    # -------------------------------------

    function visit()
      Game.transition(crossnode)
    end
  end
end

# Demo engine
module Engine
  using ..Ranger.Nodes:
    visit
    
  function run()
    visit()
  end
end

# ------------------
# Attempt to extend Game module
using .Ranger.Nodes:
  AbstractNode
using .Game

# Attemping to add `new` function to Game module.
function Game.transition(node::AbstractNode)  # <-- UndefVarError: transition not defined
  println("game transition")
end

# -------------------------
using .Engine:
  run
  
function go()
  run()
end
  
go()

Thanks

#2

The answer to your title question is:

julia> module A
       end
Main.A

julia> A.eval(:(f() = 1))
f (generic function with 1 method)

julia> A.f()
1
4 Likes
#3

I was afraid I would need to resort to something along the lines of eval() :anguished:

Is there any other idiomatic way?

#4

As a slight quality of life improvement, you can use the @eval macro,

module A end

@eval A begin
    f() = 1
    g() = 2
end

But fundamentally you need to evaluate code in the existing module.

2 Likes
#5

I suppose the more standard way to do this would maybe be to define an interface in Engine, and then have your AbstractNode subtypes in Game implement that interface (by overloading functions defined in Engine), something like

module Engine
abstract type AbstractNode end

function transition end

function foo(node::AbstractNode)
    # 'generic' behavior here
    transition(node)
    nothing
end
end # module

module Game
using ..Engine

struct GameNode <: Engine.AbstractNode end

Engine.transition(foo::GameNode) = @show foo
end # module

using .Game, .Engine
Engine.foo(Game.GameNode())

Alternatively, you could have the ‘generic’ functions in Engine take a function argument that acts as a callback for implementation-specific behavior.

6 Likes
#6

Nice. I will give that approach a try. Thanks. :slightly_smiling_face:

#7

I would like to thank mauro3, tkoolen for helping me solve my issue. Here is the simplified version on Repl.it.

I also experience some weird issues with importing. For example, I have two include files:

include("splash_scene.jl")
include("game_scene.jl")

both perform the same import statements:

import .Ranger.Nodes:
    transition, get_replacement, visit

If I forget to import visit in either one of the files I get this warning:

WARNING: import of Nodes.visit into Main conflicts with an existing identifier; ignored.

And as a result, the abstract overload is called instead. This is unexpected and in addition I don’t know why Julia issues this. It is true that I should only have only one import for both files—which is what I did to make sure there isn’t an inconsistency—but still it would be nice if Julia could somehow give a better clue to the problem. It took me 20 minutes to realize what the bug was. Basically, if you have this:

import .Ranger.Nodes:
    transition, get_replacement, visit

... more code and some includes()

import .Ranger.Nodes:
    transition, get_replacement       #<--- missing visit import

You will get a warning.

Anyway, I am still learning Julia and I enjoy the language even with a few errors on my part.
The code-in-progress in on Github if you are interested.

Below is a stripped down example with two lines marked important:

module Ranger
  module Nodes
    # This is important piece #1
    function transition end

    # Simple demo node for example ----------
    abstract type AbstractNode end

    mutable struct Node <: AbstractNode
      x::Float64
    end

    crossnode = Node(1.0)
    # -------------------------------------

    function visit()
      transition(crossnode)
    end
  end
end

# Demo engine
module Engine
  using ..Ranger.Nodes:
    visit
    
  function run()
    visit()
  end
end

# ------------------
using .Ranger.Nodes:
  AbstractNode, Node

# This is the important piece #2
import .Ranger.Nodes:
  transition

function transition(node::AbstractNode)
  println("abstract node transition")
end

function transition(node::Node)
  println("node transition")
end

# -------------------------
using .Engine:
  run
  
function go()
  run()
end
  
go()
Sub module accessing a global method
#8

Could you clarify how the simplified example relates to the problem you were having? The simplified example has only one file and only one using ..Ranger.Nodes: visit, and everything seems to be working as intended, no?

By the way, for clarity I personally prefer being very explicit when it comes to adding methods to functions from other modules, i.e.

import A
A.foo(x::MyType) = x

instead of

import A: foo
foo(x::MyType) = x
2 Likes
#9

My original issue was a chicken/egg problem. Your example showed how to handle it:

function transition end

I didn’t know you could declare empty functions like that.

The second part of the solution was then importing correctly. I didn’t realize that importing needed to be very specific otherwise what you are trying to extend may not be what you expected.

The other part of your example completed the solution and that was importing the empty function, and as I learned the hard way, you need to make sure you are extending the correct function—which you do explicitly and I do more verbosely. I tend to hang on to particular styles and I admit I should use the more explicit style.

Doesn’t your style import in more than wanted at times?

#10

Somewhat confusingly, no. While using A: foo is in a sense more conservative than import A: foo in that the latter allows you to extend A.foo without qualification while the former doesn’t, import A is more conservative than using A, in that import A only makes A visible, while using A also makes functions exported from A visible. See https://docs.julialang.org/en/v1/manual/modules/#Summary-of-module-usage-1.

1 Like
#11

Yeah, I saw the chart and sort of gave it a glance-over when I was learning :wink: But now I have more respect for it.

After thinking about your style question I started thinking about mine and realized that I wouldn’t have experienced my import problem had I used A.foo approach, so I am going back and refactoring based on that realization. :smirk: