A Julia package providing functionality for annotating arbitrary callables with type signature data.
Might help with bad inference.
Provides somewhat of a restricted way of expressing a type signature for a callable in Julia's type system.
The package exports the following bindings:
-
CallableWithReturnType-
Throws after calling the underlying callable if the return type does not match.
-
Lacks constructor methods.
-
Not a newly defined type, just a nice interface over functionality provided by
Base. In particular, for anyReturn::Typewe have:CallableWithReturnType{Return} == ComposedFunction{Base.Fix2{typeof(typeassert), Type{Return}}}
-
-
CallableWithTypeSignature-
Throws after calling the underlying callable if the return type does not match.
-
Throws before calling the underlying callable if the argument types do not match.
-
Subtypes
CallableWithReturnType. -
Lacks constructor methods.
-
-
typed_callable- Use
typed_callableto constructCallableWithReturnTypeorCallableWithTypeSignaturevalues.
- Use
julia> using EnforcedTypeSignatureCallables
julia> typed_callable(sin, Float32)(0.3f0)
0.29552022f0
julia> typed_callable(sin, Float32)(0.3)
ERROR: TypeError: in typeassert, expected Float32, got a value of type Float64
[...]
julia> typed_callable(hypot, Float64, Tuple{Int, Int})(3, 4)
5.0
julia> typed_callable(hypot, Float64, Tuple{Int, Int})(3, 4.0)
ERROR: TypeError: in typeassert, expected Tuple{Int64, Int64}, got a value of type Tuple{Int64, Float64}
[...]As discussed in Julia issue
#42372, a type constructor is
not required to return a value of the given type: the return value of a constructor
can technically be of any type! Thus, in the worst case, the compiler is not able
to infer the return type of a constructor like, for example, Int.
This package presents a workaround (actually merely a nice interface over
functionality already provided with Julia Base):
julia> using EnforcedTypeSignatureCallables
julia> naive(x) = map(Int, x)
naive (generic function with 1 method)
julia> improved(x) = map(typed_callable(Int, Int), x)
improved (generic function with 1 method)
julia> Base.infer_return_type(naive, Tuple{NTuple{5, Any}}) # pessimistic type inference result
NTuple{5, Any}
julia> Base.infer_return_type(improved, Tuple{NTuple{5, Any}}) # the return type is now known concretely
NTuple{5, Int64}NB: typed_callable(Int, Int) actually only consists of types already present in
Base Julia, so it's just a nicer interface for functionality that already comes
with Julia:
julia> typed_callable(Int, Int)
Base.Fix2{typeof(typeassert), Type{Int64}}(typeassert, Int64) ∘ Int64The three-argument version of typed_callable depends on a type defined in this
package, though.
This is what many newcomers to Julia ask for, especially when coming from a statically typed language: being able to express the type signature of a "function" in the type system.
Suppose one is writing a method which takes, among other arguments, a function
(callable object). Further suppose the method needs to be constrained to only apply
when the callable argument has a certain type signature. One way to achieve this is
to restrict the allowed types of the callable argument to chosen subtypes of
CallableWithReturnType. This includes CallableWithTypeSignature, so the entire
type signature, except any keyword arguments, may be accounted for.
For example, a CallableWithReturnType{Float32} is guaranteed to return a
Float32 value, if a value is returned. A
CallableWithTypeSignature{Float32, Tuple{Float32, Float32}} additionally
guarantees to only accept exactly two Float32 values as positional arguments.
For example, suppose your package has a method that accepts a function from the
user. Further suppose the method code expects the user-provided function to only
ever return Float64. Instead of sprinkling typeasserts all over your code, it
suffices to call typed_callable once:
function accepts_a_function_from_the_user(func, other_arguments...)
func = typed_callable(func, Float64)
# any call of `func` is now guaranteed not to return anything other than `Float64`
endFurthermore, dispatch can also be used to achieve type safety in this regard:
function accepts_a_function_from_the_user_type_safe(func::CallableWithReturnType{Float64}, other_arguments...)
# any call of `func` is guaranteed not to return anything other than `Float64`
end
function accepts_a_function_from_the_user(func, other_arguments...)
func = typed_callable(func, Float64)
accepts_a_function_from_the_user_type_safe(func, other_arguments...)
endCreating a new local function with a typeassert in the method body would work as intended for both:
-
helping the compiler achieve good inference
-
wrapping a user-provided function into a type-safe wrapper function
However using typed_callable instead is slightly better:
-
avoiding the creation of a new function is slightly friendlier to the compiler, giving it less work to do
-
using a standardized type may allow the ecosystem to converge on a single type to dispatch on when a callable with a certain type signature is required
- This is more so appropriate as
CallableWithReturnTypeis just a type alias for a type already provided byBase. Thus a package doesn't even need to depend on this package to dispatch onCallableWithReturnType{ReturnType}.
- This is more so appropriate as