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

kunzaatko / InterfaceFunctions.jl / 16776634775

06 Aug 2025 12:15PM UTC coverage: 98.438% (-1.6%) from 100.0%
16776634775

push

github

kunzaatko
chore: Bump patch version

63 of 64 relevant lines covered (98.44%)

78.42 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

5
module InterfaceFunctions
6
using MacroTools
7

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

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

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

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

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

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

44
```jldoctest
45
julia> IF.param_names(:(A{B, C{D}}))
46
4-element Vector{Any}:
47
 :A
48
 :B
49
 :C
50
 :D
51
```
52
"""
53
function param_names(type_exp, params=[])
229✔
54
    if @capture(type_exp, T_{P__})
291✔
55
        push!(params, namify(T))
65✔
56
        for p in P
39✔
57
            param_names(p, params)
48✔
58
        end
48✔
59
    elseif @capture(type_exp, T__)
60
        append!(params, namify.(T))
111✔
61
    else
62
        return []
×
63
    end
64
    return params
150✔
65
end
66

123✔
67
"""
246✔
68
    where_param_names(where_exps)
246✔
69
Determine the names of the type parameters for every parameter in `where_exps`
70

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

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

129
    return quote
69✔
130
        $type_exp where {$(interface_whereparams_names...)}
69✔
131
    end
132
end
133

134
"""
135
    @interface fn_expr
136
Define an interface function for the abstract type in the first argument of the function signature in `fn_expr`.
137

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

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

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

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

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

199
    length(fdict[:args]) > 0 || throw(
84✔
200
        ArgumentError(lazy"In `@interface`, an interface must have atleast one argument."),
201
    )
202

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

209
    interface_arg = MacroTools.splitarg(first(fdict[:args]))
69✔
210
    t, T = interface_arg[[1, 2]]
69✔
211

212
    T_interface = interfacetype(T, fdict[:whereparams])
69✔
213

214
    if interface_arg[1] === nothing
69✔
215
        t = Symbol(lowercase(first(string(T))))
3✔
216
        fdict[:args][1] = :($t::$T)
3✔
217
    end
218

219
    signature = string(fcall)
69✔
220
    fdict[:body] = if obligatory
69✔
221
        :(return throw($UnimplementedInterface{$T_interface,typeof($t)}($signature)))
60✔
222
    else
223
        body = fdict[:body]
9✔
224
        logmsg = "Calling default implementation of `" * signature * "`"
9✔
225
        MacroTools.@q begin
78✔
226
            @debug $logmsg * " for type `$(typeof($t))`" _group = :interfaces
227
            $body
228
        end
229
    end
230

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

253
export @interface
254

255
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