Note that ∘
is a law of composition on Function
; it is always the case that (f::Function ∘ g::Function)::Function
. The only difference between Function
and Int64
is that Function
is abstract.
It sounds like what you’re looking for is to be able to do composition, and be able to interchange results of such composition, in a performant and type stable way. This is a difficult problem in general, but a technique called “embedding” applies here. Note that I will ignore performance for simplicity; work done by Yichao Yu and other members of the core dev team is in place to make this kind of thing more performant.
The idea is to embed a particular space of functions into a single type. This enables the property that all objects inside this embedding have the same concrete type, which seems to be what you’re looking for.
Following is an example.
import Base: convert, show, ∘
struct Hom{D, R} <: Function
f::Any # performance is ignored here; this can be improved
end
show(io::IO, φ::Hom) = print(io, φ.f, " as ", typeof(φ))
show(io::IO, ::MIME"text/plain", φ::Hom) = print(io, φ.f, " as ", typeof(φ))
((φ::Hom{D, R})(x::D)::R) where {D, R} = φ.f(x)
convert(::Type{T}, f) where {T <: Hom} = T(f)
(φ::Hom{D, R} ∘ σ::Hom{R, W}) where {D, R, W} = Hom{D, W}(φ.f ∘ σ.f)
const sine = Hom{Number, Number}(sin)
const cosine = Hom{Number, Number}(cosine)
Then you can indeed have (sine ∘ cosine)::Hom{Number, Number}
. We’ve embedded a variety of other types into the set Hom{D, R}
that we’ve exhibited as a type.