• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kunzaatko / InterfaceFunctions.jl / 17150979300

22 Aug 2025 09:00AM UTC coverage: 98.438%. Remained the same
17150979300

push

github

kunzaatko
docs: Add `favicon.ico` to assets and rephrase first sentence of index.md

63 of 64 relevant lines covered (98.44%)

168.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.44
/src/InterfaceFunctions.jl
1
# TODO: Allow interfaces for types which are not in the first position <29-07-25> 
2
# TODO: Documenter.jl extension to generate an AbstractType tree with the interfaces. It should be a mermaid diagram. It
3
# will be used for generating developer documentation. <29-07-25> 
4
# TODO: Add a test extension that allows to check that all obligatory interfaces are implemented <08-08-25> 
5

6
module InterfaceFunctions
7
using MacroTools
8

9
# TODO: Differentiate between interfaces that have default implementations and those that don't. There has to be
10
# a difference for how the interfaces can be accessed in the `@docs` section of the documentation pages such that we can
11
# separate the obligatory interfaces and those that aren't obligatory. See how storing the defined units is this is done
12
# by the register function in the `Units` package. <14-07-25> 
13

14
# TODO: Add a similar documentation constant for all the interfaces such as are the defined constants in
15
# DocStringExtensions.jl <23-06-25> 
16

17
# TODO: Add a default value signature example and test <14-07-25> 
18
# TODO: `@interface` uses covered add to documentation
19
# - `@interface f(t::T)`
20
# - `@interface f(t::T1, ::T2)`
21
# - `@interface f(t::Type{<:T})`
22
# - `@interface f(t::T1, ::Type{<:T2})`
23
# - `@interface function f(t::T1) #=default_body=# end`
24

25
# TODO: Add the source of the default interface call to the @debug log message <14-07-25> 
26

27
"""
28
    UnimplementedInterface <: Exception 
29
Thrown when a subtype does not implement an obligatory interface.
30
"""
31
struct UnimplementedInterface{T,X} <: Exception
32
    signature::String
144✔
33
end
34
function Base.showerror(io::IO, err::UnimplementedInterface{T,X}) where {T,X}
4✔
35
    return print(
6✔
36
        io,
37
        "UnimplementedInterface{$(nameof(T))}: `$(nameof(X))` does not implement the obligatory interface `$(err.signature)`",
38
    )
39
end
40

41
"""
42
    param_names(type_exp)
43
Determine the names of the type parameters needed for a correct type definition from the type expression.
44

45
```jldoctest
46
julia> IF.param_names(:(A{B, C{D}}))
47
4-element Vector{Any}:
48
 :A
49
 :B
50
 :C
51
 :D
52
```
53
"""
54
function param_names(type_exp, params=[])
495✔
55
    if @capture(type_exp, T_{P__})
633✔
56
        push!(params, namify(T))
150✔
57
        for p in P
90✔
58
            param_names(p, params)
117✔
59
        end
117✔
60
    elseif @capture(type_exp, T__)
61
        # NOTE: Some parameters are non-symbols for example `AbstractArray{T, 2}` and `namify` would not work on those. But also, we do not care about them. <19-08-25> 
62
        filter!(Base.Fix2(isa, Symbol), T)
240✔
63
        append!(params, namify.(T))
240✔
64
    else
65
        return []
×
66
    end
264✔
67
    return params
858✔
68
end
528✔
69

70
"""
71
    where_param_names(where_exps)
72
Determine the names of the type parameters for every parameter in `where_exps`
73

74
```jldcotest
75
julia> IF.where_param_names((:(T <: A{B, N}), :(A <: AbstractArray), :(B <: Real), :(N)))
76
Dict{Symbol, Vector{Any}} with 4 entries:
77
  :T => [:A, :B, :N]
78
  :N => []
79
  :A => [:AbstractArray]
80
  :B => [:Real]
81
```
82
"""
83
function where_param_names(where_exps)
108✔
84
    return Dict(map(where_exps) do e
192✔
85
        if @capture(e, T_ <: V_)
96✔
86
            namify(T) => param_names(V)
36✔
87
        elseif @capture(e, T_)
88
            namify(T) => []
24✔
89
        end
90
    end...)
91
end
92

93
"""
94
    interfacetype(type_exp, where_exps)
95
Determine the exact defined interfacing type from the signature of the function and the type expression of the first
96
argument.
97
"""
98
function interfacetype(type_exp, where_exps)
144✔
99
    interface_param_names = param_names(type_exp)
144✔
100
    where_param_names_dict = where_param_names(where_exps)
180✔
101
    interface_whereparams_names = []
144✔
102
    needed_param_names = [interface_param_names...]
144✔
103
    """
104
        collect_needed(added, needed, needed_dict)
105
    Collect the names of the needed type parameters starting with `added` and `needed` and going throught the
106
    `added` to recursively add the necesary type parameter names from `needed_dict[k]` to `needed` for every `k` in
107
    needed.
108
    """
109
    function collect_needed(added, needed, needed_dict)
444✔
110
        if isempty(added)
300✔
111
            return needed
144✔
112
        else
113
            for n in added
156✔
114
                append!(needed, get(needed_dict, n, []))
190✔
115
            end
238✔
116
            new = mapreduce(vcat, added) do n
156✔
117
                get(needed_dict, n, [])
186✔
118
            end
119
            empty(added)
156✔
120
        end
121
        return collect_needed(new, needed, needed_dict)
156✔
122
    end
123
    needed_param_names = collect_needed(
144✔
124
        interface_param_names, interface_param_names, where_param_names_dict
125
    )
126
    for w in where_exps
144✔
127
        if namify(w) ∈ needed_param_names
36✔
128
            push!(interface_whereparams_names, w)
18✔
129
        end
130
    end
36✔
131

132
    return quote
144✔
133
        $type_exp where {$(interface_whereparams_names...)}
144✔
134
    end
135
end
136

137
"""
138
    @interface fn_expr
139
Define an interface function for the abstract type in the first argument of the function signature in `fn_expr`.
140

141
There are two situations in which you would want to use this macro. Both define an interface. The difference is whether
142
you would want to define the interface as obligatory for the implementer or as an optional interface with a default
143
implementation. The use for these two situations differs only in the object on which you apply this macro. If the
144
interface should be obligatory, you call `@interface` on a function signature. For optional interfaces call the
145
interface macro on the default implementation of the interface.
146

147
# Obligatory interfaces
148
> An obligatory interface on an abstract type is a way to indicate that every subtype has to provide a custom method for
149
> this function. Interfaces are useful for code re-usability in higher order methods. Higher order methods of abstract
150
> types can use the interfaces defined on these types to provide implementations for their subtypes.
151

152
It is common to define empty functions for obligatory interfaces
153
```julia
154
function obligatoryfn(a::A, b::B) end
155
```
156
This macro creates the function and adds an informative error message to the user so that he knows what went wrong and
157
how to fix it (what method he has to implement to fix the issue)
158
```julia
159
"Documentation"
160
@inteface obligatoryfn(a::A, b::B)
161
```
162
roughly translates to 
163
```julia
164
"Documentation"
165
function obligatoryfn(a::A, b::B)
166
    throw(UnimplementedInterface{A}(#=signature=#))
167
end
168
```
169
# Optional interfaces 
170
> An optional interface on an abstract type signifies to the developer working with the abstract type that he has the
171
> option to change the behaviour of the higher order functions by defining a specialized method for his subtype.
172

173
The benefit of using the `@interface` macro for defining such a function is that it can be then used in the
174
documentation as a group of interfaces that are optional for a type and there are `@debug` messages added automatically
175
to the calls indicating that the default implementation for the interface is used.
176

177
If you want to create an optional interface you have to add the `@interface` annotation in front of the default
178
implementation 
179
```julia
180
@interface optionalfn(a::A, b::B) = :default
181
# OR
182
@interface function optionalfn(a::A, b::B) 
183
    return :default
184
end
185
```
186
These translate roughly to
187
```julia
188
function optionalfn(a::A, b::B)
189
    @debug "Calling default implementation of \$(#=signature=#) at \$(#=source=#) for type \$(#=caller=#)"
190
    return :default
191
end
192
```
193
"""
194
macro interface(ex)
162✔
195
    obligatory = !isdef(ex)
162✔
196
    fcall = isdef(ex) ? ex.args[1] : ex
306✔
197
    if obligatory
162✔
198
        ex = MacroTools.@q $ex = begin end
144✔
199
    end
200
    fdict = MacroTools.splitdef(ex)
162✔
201

202
    length(fdict[:args]) > 0 || throw(
174✔
203
        ArgumentError(lazy"In `@interface`, an interface must have atleast one argument."),
204
    )
205

206
    isexpr(first(fdict[:args]), :(::)) || throw(
156✔
207
        ArgumentError(
208
            lazy"In `@interface`, the first argument `$(first(fdict[:args]))` of the interface function must have a known type otherwise the interfacing type is unknown.",
209
        ),
210
    )
211

212
    interface_arg = MacroTools.splitarg(first(fdict[:args]))
144✔
213
    t, T = interface_arg[[1, 2]]
144✔
214

215
    T_interface = interfacetype(T, fdict[:whereparams])
144✔
216

217
    if interface_arg[1] === nothing
144✔
218
        t = Symbol(lowercase(first(string(T))))
6✔
219
        fdict[:args][1] = :($t::$T)
6✔
220
    end
221

222
    signature = string(fcall)
144✔
223
    fdict[:body] = if obligatory
144✔
224
        :(return throw($UnimplementedInterface{$T_interface,typeof($t)}($signature)))
126✔
225
    else
226
        body = fdict[:body]
18✔
227
        logmsg = "Calling default implementation of `" * signature * "`"
18✔
228
        MacroTools.@q begin
162✔
229
            @debug $logmsg * " for type `$(typeof($t))`" _group = :interfaces
230
            $body
231
        end
232
    end
233

234
    interface_expr = combinedef(fdict)
144✔
235
    return Expr(
144✔
236
        :block,
237
        esc(
238
            quote
239
                isabstracttype($T_interface) || throw(
240
                    ArgumentError(
241
                        "In `@interface`, the interfacing type " *
242
                        string(nameof($T_interface)) *
243
                        " must be abstract.",
244
                    ),
245
                )
246
            end,
247
        ),
248
        esc(
249
            quote
250
                Core.@__doc__ $interface_expr
251
            end,
252
        ),
253
    )
254
end
255

256
export @interface
257

258
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc