I think you may have a hard time avoiding const.
Closures are wonderful, but, if you define closures as global variables:
const a = 1
foo(x,a) = a*x
fuu = (x) -> foo(x,a)
fii(x) = foo(x,a)
They are just that, variables, whose types cannot be inferred.
julia> @code_warntype fuu(2) # good
Body::Int64
1 ─ %1 = Main.a::Core.Compiler.Const(1, false)
│ %2 = (Base.mul_int)(%1, x)::Int64
└── return %2
julia> @code_warntype fii(2) # good
Body::Int64
1 ─ %1 = Main.a::Core.Compiler.Const(1, false)
│ %2 = (Base.mul_int)(%1, x)::Int64
└── return %2
julia> bar(x) = fuu(x) # trying to use fuu from another module
bar (generic function with 1 method)
julia> @code_warntype bar(2) # yikes
Body::Any
1 ─ %1 = (Main.fuu)(x)::Any
└── return %1
(Note that you can just copy and paste the above code into the REPL; Julia will automatically delete the julia> s.)
You could do
julia> mutable struct Foo{T} <: Function
a::T
end
julia> (f::Foo)(x) = f.a * x
julia> const foo_instance = Foo(1)
(::Foo{Int64}) (generic function with 1 method)
julia> foo_instance(2)
2
julia> @code_warntype foo_instance(2)
Body::Int64
1 ─ %1 = (Base.getfield)(f, :a)::Int64
│ %2 = (Base.mul_int)(%1, x)::Int64
└── return %2
julia> bar2(x) = foo_instance(x)
bar2 (generic function with 1 method)
julia> @code_warntype bar2(2)
Body::Int64
1 ─ %1 = Main.foo_instance::Core.Compiler.Const(Foo{Int64}(1), false)
│ %2 = (Base.getfield)(%1, :a)::Int64
│ %3 = (Base.mul_int)(%2, x)::Int64
└── return %3
julia> foo_instance.a = 4
4
julia> bar2(2)
8
But then your program is still dependent on global state. Common blocks are bad.
They make managing things more complicated. Especially multithreading, if you ever plan on using @threads.