By default, a function is an instance of a singleton type, meaning that type only has 1 instance, the function itself. However, that is not necessarily the case; if a function were to contain data from a local scope, then it would not have a singleton type. The non-singleton function factory example nsajko provided is a bit indirect, it’d be clearer to write out the underlying functors with a type that subtypes Function
, making them functions. There would still only be 1 method table associated with the functors’ type, but the functors would act differently because of the data they contain. Of course, this still restricts the functors to do almost the same thing, so it might not be applicable to your use case. FunctionWrappers.jl lets the multiple input functions have different method tables and types as a tradeoff for attaching a call signature restriction. (Normally, a function has multiple methods, and each method can be compiled for multiple call signatures; a call is dispatched to a method based on the function’s type as well as the arguments’ types).
To expand on the explicitness, unlike writing out a functor type to contain data in manually annotated fields, closures handle it implicitly so it’s not visible in one place:
julia> f(n, m) = let n = n, m = m
foo(x) = x+n # 1st method captures n
# maybe a lot of other code
foo() = m # 2nd method captures m
end
f (generic function with 1 method)
julia> f(1, 1.0) # type parameters means function contains both n and m
(::var"#foo#1"{Int64, Float64}) (generic function with 2 methods)
Instantiating functors also don’t have to deal with variable scoping rules; closures are local scopes that technically capture variables, not data at instantiation, and any reassignment prevents the compiler from inferring the variable’s type well, even if unnecessary and accidental.