This isn’t abuse of the type system, sometimes it’s useful to put data into the type domain.
Some stylistic notes:
- usually the constructors would be organized in such a manner that there’s at most a single explicit inner constructor:
struct Fix{Fn,Args,KW}
Fix{Fn,Args,KW}() where {Fn,Args,KW} = new{Fn,Args,KW}()
end
Fix{Fn,Args}() where {Fn,Args} = Fix{Fn,Args,(;)}()
Fix{Fn}() where {Fn} = Fix{Fn,()}()
- The definition of the inner constructor in the above definition of
Fix
is actually redundant: leaving out the definition of the inner constructor would be preferable in this case because Julia will do the right thing by creating the default constructor. An inner constructor is most useful for checking the arguments given to the constructor and similar, which isn’t necessary here.
julia> struct Fix{Fn,Args,KW} end
julia> methods(Fix)
# 0 methods for type constructor
julia> methods(Fix{rand})
# 0 methods for type constructor
julia> methods(Fix{rand, (), (;)})
# 1 method for type constructor:
[1] (var"#ctor-self#"::Type{Fix{Fn, Args, KW}} where {Fn, Args, KW})()
@ REPL[1]:1
- The intention behind a constructor like
Fix{Fn}() where {Fn} = Fix{Fn,()}()
is to provide a default value for a type parameter. This makes sense and works, but may be problematic in some cases, because it is, in a sense, ambiguous, because it may not be clear whether an expression likeFix{some_function}
should refer to the UnionAll type (see here and here) or to the constructor method. This wouldn’t be an issue in practice forFix
, but it doesn’t seem like good style. It may be better to have the user-facing interface be a function that would wrap theFix
type, which would have only the inner constructor.