How to use a return type annotation in a function declaration before the type is defined (dependency injection)


#1

I hope I’ll be able to explain clearly enough what I’m looking for – it’s a bit tricky without getting into a lot of details about the use case. It’s basically about dependency injection.

Say that I have a module and within it, I define a type, let’s call it Foo. However, Foo is not defined in the source of the module, but rather the declaration is done using eval (it’s basically done in a module configuration step, as the actual definition of Foo depends on external factors). This Foo is an alias of another type (one of many possible types which don’t share a common supertype) which is provided in the configuration step.

Now, let’s say that I have a function foo() which returns an instance of Foo. If I declare it like: function foo() Julia has no problem compiling the code – and assuming that I properly set up the type, everything works when I invoke the function.

However, if I declare function foo()::Foo Julia gets upset because Foo is not defined when the source file is parsed. This makes sense, obviously – but it’s not very valuable. I would like to still be able to use the type hint - both for better understanding the code later, and for counting on Julia’s checks at runtime.

One thing that worked was to define Foo as a Union of all the possible types. But that requires including and loading all the other libraries, even though I only need one of them (the one passed in the configuration step).

So can I tell Julia “let me annotate this function with the return type ::Foo even if it doesn’t exist yet – by the time I’ll use the function, ::Foo will be set, trust me, I’m a human and I know what I’m doing”?


#2

What about:

function foo(..., return_type::Type{T}) where {T}
   return ans::T
end 

then always call foo with an additional parameter? Or even better if you don’t want that, maybe T already exists in the types of one of the existing arguments, or can be derived from it, so you can capture it from there? Of course the most ideal is probably just to write the function to be type stable (but it sounds from your question maybe this isn’t easy/possible in this case).


#3

Thanks.

Oh yes, I see what you mean - similar to how DataStreams takes a type as a param. This sounds like a great approach, thanks, I’ll give it a try :smiley:

Hmmm…, I haven’t considered type stability. The Union approach would be problematic - but the current one, by defining the type via eval is type stable (if I read correctly the output in v0.7). Once configured, it always returns the configured types. But this is definitely something to keep an eye on.

julia> @code_warntype SearchLight.Database.connect()
Body::LibPQ.Connection
48 1 ─ %1 = SearchLight.Database.SearchLight::Core.Compiler.Const(SearchLight, false)                                                                                                                                        │
   │   %2 = (Base.getfield)(%1, :config)::SearchLight.Configuration.Settings                                                                                                                                                 │╻ getproperty
   │   %3 = (Base.getfield)(%2, :db_config_settings)::Dict{String,Any}                                                                                                                                                       ││
   │   %4 = invoke SearchLight.Database.PostgreSQLDatabaseAdapter.connect(%3::Dict{String,Any})::LibPQ.Connection                                                                                                            │╻ connect
   └──      return %4

#4

Maybe Marius answer can be “simplified” a bit since the type is a constant after configuration? How about just declaring it as a const?

function foo(x)
    x :: Foo
end

foo(1) ## UndefVarError

const Foo = eval(:Int)

foo(1) ## 1

foo(2.0) ## TypeError

#5

Interesting, that works on 0.6 (actually it works even without the const), which I hadn’t realized. It doesn’t work on 0.7 though (where I think OP was asking), I wonder whether this was an intended change or not, or what caused it?


#6

The code works for me either from REPL or command line on julia v0.6 and v0.7 beta2 on my mac and is type stable.
Without const it also seems to work, but has type instability issue.

               _
   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: https://docs.julialang.org
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.7.0-beta2.0 (2018-07-13 19:54 UTC)
 _/ |\__'_|_|_|\__'_|  |  Official http://julialang.org/ release
|__/                   |  x86_64-apple-darwin14.5.0

julia> function foo(x)
           x :: Foo
       end
foo (generic function with 1 method)

julia> const Foo = eval(:Int)
Int64

julia> foo(1)
1

julia> foo(1.0)
ERROR: TypeError: in foo, in typeassert, expected Int64, got Float64
Stacktrace:
 [1] foo(::Float64) at ./REPL[1]:2
 [2] top-level scope at none:0

julia> @code_warntype(foo(1))
Body::Int64
2 1 ─     return %%x                                                        │

julia> @code_warntype(foo(1.0))
Body::Union{}
2 1 ─     Core.typeassert(%%x, Main.Foo)                                    │
  │       π (%%x, Union{})                                                  │
  └──     unreachable                                                       │
  2 ─     unreachable                                                       │

julia> 

#7

Weird, I thought I just tested it on 0.7 and it didn’t work but trying again you’re right, its fine there as well. In that case, @essenciary do you have a minimal working example to reproduce your error?


#8

Thanks @marius311

I didn’t make myself clear. It’s not technically an error – it’s more of a question whether or not type assertion can be delayed – or rather, configured at runtime. It’s a part of my saga of identifying valid design patterns for dependency injection in Julia. In this case, configuring modules at runtime.

But hmmm… I was just trying to come up with a simple example, so I was running this on 0.7 which does work!

module Foo 

function setup_type(typ::DataType)
  Core.eval(@__MODULE__, :(const SecretType = $typ))
end

function foofify(value)::SecretType
  value
end

end

using .Foo

Foo.setup_type(Int)
Foo.foofify(42)

In my original codebase, something like this would error out on v0.6 cause SecretType would be undefined when using .Foo. I need to get back to that and see if it’s due to 0.7 or there’s something in my code :slight_smile:


#9

Interestingly though, this does not work:

module Foo 

function setup_type(typ::DataType)
  Core.eval(@__MODULE__, :(const SecretType = $typ))
end

function foofify(value::SecretType) # moved the type assertion to the param
  value
end

end

using .Foo

Foo.setup_type(Int)
Foo.foofify(42)

This does:

function foofify(value)
  value::SecretType
end