Skip to content

Commit f140a62

Browse files
committed
rewrite package for v2
1 parent 738e8ed commit f140a62

File tree

3 files changed

+281
-82
lines changed

3 files changed

+281
-82
lines changed

README.md

Lines changed: 103 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,106 @@
55
[![PkgEval](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/E/EnforcedTypeSignatureCallables.svg)](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/E/EnforcedTypeSignatureCallables.html)
66
[![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)
77

8-
A tiny Julia package providing a simple wrapper type, `TypedCallable`, for enforcing the
9-
argument and return types of a wrapped callable object. The wrapper callable ensures
10-
that, for each call, the types of the positional arguments and the return type are as
11-
specified.
12-
13-
Kind of provides a way to express a type signature for a callable in Julia's type
14-
system.
15-
16-
## Why would one want to use this?
17-
18-
Two unrelated use cases come to mind. The first is helping Julia's type inference when
19-
necessary.
20-
21-
The second use case is what many newcomers to Julia ask for, especially when coming from
22-
a statically typed language: being able to express the type signature of a "function" in
23-
the type system. Suppose one is writing a method which takes, among other arguments, a
24-
function/callable object. Further suppose we want the latter to error when passed
25-
anything except an `AbstractString`, and perhaps we also want to guarantee that it
26-
returns an `Int`. One way to achieve this is to require the callable argument to be of
27-
type `TypedCallable{Tuple{AbstractString},Int}`.
8+
A Julia package providing functionality for annotating arbitrary callables with
9+
type signature data.
10+
11+
Might help with bad inference.
12+
13+
Kind of provides a restricted way of expressing a type signature for a callable in
14+
Julia's type system.
15+
16+
## Provided functionality
17+
18+
The package exports the following bindings:
19+
20+
* `CallableWithReturnType`
21+
22+
* Throws after calling the underlying callable if the return type does not
23+
match.
24+
25+
* Lacks constructor methods.
26+
27+
* Not a newly defined type, just a nice interface over functionality provided
28+
by `Base`. In particular, for any `Return::Type` we have:
29+
30+
```julia
31+
CallableWithReturnType{Return} == ComposedFunction{Base.Fix2{typeof(typeassert), Type{Return}}}
32+
```
33+
34+
* `CallableWithTypeSignature`
35+
36+
* Throws after calling the underlying callable if the return type does not
37+
match.
38+
39+
* Throws before calling the underlying callable if the argument types do not
40+
match.
41+
42+
* Subtypes `CallableWithReturnType`.
43+
44+
* Lacks constructor methods.
45+
46+
* `typed_callable`
47+
48+
* Use `typed_callable` to construct `CallableWithReturnType` or `CallableWithTypeSignature` values.
49+
50+
## Usage example
51+
52+
```julia-repl
53+
julia> using EnforcedTypeSignatureCallables
54+
55+
julia> typed_callable(Float32, sin)(0.3f0)
56+
0.29552022f0
57+
58+
julia> typed_callable(Float32, sin)(0.3)
59+
ERROR: TypeError: in typeassert, expected Float32, got a value of type Float64
60+
[...]
61+
62+
julia> typed_callable(Float64, Tuple{Int, Int}, hypot)(3, 4)
63+
5.0
64+
65+
julia> typed_callable(Float64, Tuple{Int, Int}, hypot)(3, 4.0)
66+
ERROR: TypeError: in typeassert, expected Tuple{Int64, Int64}, got a value of type Tuple{Int64, Float64}
67+
[...]
68+
```
69+
70+
## Motivation
71+
72+
### Use case 1: help the Julia compiler to achieve good inference
73+
74+
As discussed in Julia issue
75+
[#42372](https://github.com/JuliaLang/julia/issues/42372), a type constructor is
76+
not required to return a value of the given type: the return value of a constructor
77+
can technically be of any type! Thus, in the worst case, the compiler is not able
78+
to infer the return type of a constructor like, for example, `Int`.
79+
80+
This package presents a workaround (actually merely a nice interface over
81+
functionality already provided with Julia `Base`):
82+
83+
```julia-repl
84+
julia> using EnforcedTypeSignatureCallables
85+
86+
julia> naive(x) = map(Int, x)
87+
naive (generic function with 1 method)
88+
89+
julia> improved(x) = map(typed_callable(Int, Int), x)
90+
improved (generic function with 1 method)
91+
92+
julia> Base.infer_return_type(naive, Tuple{NTuple{5, Any}}) # pessimistic type inference result
93+
NTuple{5, Any}
94+
95+
julia> Base.infer_return_type(improved, Tuple{NTuple{5, Any}}) # the return type is now known concretely
96+
NTuple{5, Int64}
97+
```
98+
99+
### Use case 2: dispatch on callables with a certain type signature (kind of)
100+
101+
This is what many newcomers to Julia ask for, especially when coming from a
102+
statically typed language: being able to express the type signature of a "function"
103+
in the type system.
104+
105+
Suppose one is writing a method which takes, among other arguments, a function
106+
(callable object). Further suppose the method needs to be constrained to only apply
107+
when the callable argument has a certain type signature. One way to achieve this is
108+
to restrict the allowed types of the callable argument to chosen subtypes of
109+
`CallableWithReturnType`. This includes `CallableWithTypeSignature`, so the entire
110+
type signature, except any keyword arguments, may be accounted for.
Lines changed: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,162 @@
11
module EnforcedTypeSignatureCallables
22

3-
export TypedCallable
3+
export CallableWithReturnType, CallableWithTypeSignature, typed_callable
4+
5+
struct CallableWithArgumentTypes{Arguments <: Tuple, Callable} <: Function
6+
callable::Callable
7+
function CallableWithArgumentTypes{Arguments}(callable::Callable) where {Arguments <: Tuple, Callable}
8+
callable_type = if callable isa Type
9+
Type{callable}
10+
else
11+
typeof(callable)
12+
end
13+
new{Arguments, callable_type}(callable)
14+
end
15+
end
16+
17+
function Base.propertynames((@nospecialize unused::CallableWithArgumentTypes), ::Bool = false)
18+
()
19+
end
20+
21+
function (callable::CallableWithArgumentTypes)(args...; kwargs...)
22+
function arguments(::CallableWithArgumentTypes{Arguments}) where {Arguments <: Tuple}
23+
Arguments
24+
end
25+
args = args::arguments(callable)
26+
c = getfield(callable, 1)
27+
c(args...; kwargs...)
28+
end
429

530
"""
6-
TypedCallable
31+
CallableWithReturnType <: ComposedFunction
32+
33+
Type of callables with a guaranteed return type.
34+
35+
Has two type variables:
36+
37+
* the return type
738
8-
A simple callable type wrapping another callable.
39+
* the underlying callable type
940
10-
There are three type parameters: the first type parameter represents the allowed types
11-
of the positional arguments. The second type parameter is the allowed return type of the
12-
callable. The third type parameter is the type of the wrapped callable.
41+
`CallableWithReturnType` is not a newly defined type. It is merely a type alias based on:
1342
14-
The first type parameter, representing the allowed positional argument types, always
15-
subtypes `Tuple`. For example, to allow either a single `Int` argument or two `Bool`
16-
arguments, choose `Union{Tuple{Int},Tuple{Bool,Bool}}` as the first type parameter.
43+
* `ComposedFunction`
1744
18-
To disable argument type checking, just choose `Tuple` as the first parameter.
45+
* `Base.Fix2`
1946
20-
To disable return type checking, just choose `Any`, as the second parameter.
47+
* `typeassert`
48+
49+
For some type `Return` we have the following identity which allows dispatching on a function with a certain guaranteed return type:
50+
51+
```julia
52+
CallableWithReturnType{Return} == ComposedFunction{Base.Fix2{typeof(typeassert), Type{Return}}}
53+
```
54+
55+
Lacks constructor methods. Construct a `CallableWithReturnType` using [`typed_callable`](@ref).
2156
"""
22-
struct TypedCallable{A<:Tuple,R,F}
23-
f::F
57+
const CallableWithReturnType = ComposedFunction{
58+
Base.Fix2{
59+
typeof(typeassert),
60+
Type{Return},
61+
},
62+
Callable,
63+
} where {
64+
Return,
65+
Callable,
66+
}
2467

25-
"""
26-
TypedCallable{A,R}(f)
68+
"""
69+
CallableWithTypeSignature <: CallableWithReturnType
2770
28-
Construct a `TypedCallable{A,R}` wrapping the callable `f`.
29-
"""
30-
function TypedCallable{A,R}(f::F) where {A<:Tuple,R,F}
31-
t_r = R::Type
32-
new{A,t_r,F}(f)
33-
end
71+
Type of callables with a guaranteed return type and argument types.
72+
73+
Has three type variables:
74+
75+
* the return type
76+
77+
* the type of the positional (non-keyword) arguments, subtypes `Tuple`
78+
79+
* the underlying callable type
80+
81+
Lacks constructor methods. Construct a `CallableWithTypeSignature` using [`typed_callable`](@ref).
82+
"""
83+
const CallableWithTypeSignature = CallableWithReturnType{
84+
Return,
85+
CallableWithArgumentTypes{
86+
Arguments,
87+
Callable,
88+
},
89+
} where {
90+
Return,
91+
Arguments <: Tuple,
92+
Callable,
93+
}
94+
95+
function return_type_enforcer(::Type{Return}) where {Return}
96+
Base.Fix2(typeassert, Return)
3497
end
3598

3699
"""
37-
(tc::TypedCallable{A,R})(args...; kwargs...)
100+
typed_callable(return_type::Type, argument_types::Type{<:Tuple}, callable)::CallableWithTypeSignature{return_type, argument_types}
101+
102+
Creates a callable from `callable` with:
103+
104+
* guaranteed return type `return_type`
105+
106+
* guaranteed argument types `argument_types`
107+
108+
The return type is [`CallableWithTypeSignature`](@ref).
109+
110+
Examples:
111+
112+
```julia-repl
113+
julia> using EnforcedTypeSignatureCallables
114+
115+
julia> typed_callable(Float32, Tuple{Float32, Float32}, hypot)(3.1f0, 3.0f0)
116+
4.313931f0
117+
118+
julia> typed_callable(Float32, Tuple{Float32, Float32}, hypot)(3.1f0, 3.0)
119+
ERROR: TypeError: in typeassert, expected Tuple{Float32, Float32}, got a value of type Tuple{Float32, Float64}
120+
```
121+
"""
122+
function typed_callable(::Type{Return}, ::Type{Arguments}, callable::Callable) where {
123+
Return, Arguments <: Tuple, Callable,
124+
}
125+
ret = return_type_enforcer(Return)
126+
with_argument_types = CallableWithArgumentTypes{Arguments}(callable)
127+
ret with_argument_types
128+
end
129+
130+
"""
131+
typed_callable(return_type::Type, callable)::CallableWithReturnType{return_type}
132+
133+
Creates a callable from `callable` with guaranteed return type `return_type`
134+
135+
The return type is [`CallableWithReturnType`](@ref).
136+
137+
Examples:
138+
139+
```julia-repl
140+
julia> using EnforcedTypeSignatureCallables
141+
142+
julia> typed_callable(Int, Int)(3)
143+
3
144+
145+
julia> typed_callable(Int, Int) isa CallableWithReturnType{Int}
146+
true
147+
148+
julia> typed_callable(Float64, cos)(3)
149+
-0.9899924966004454
38150
39-
1. Enforces `args isa A`
40-
2. Calls `tc.f` with the provided positional and keyword arguments
41-
3. Enforces the return type of the above call as `R`
151+
julia> typed_callable(Float32, cos)(3.0)
152+
ERROR: TypeError: in typeassert, expected Float32, got a value of type Float64
153+
```
42154
"""
43-
function (tc::TypedCallable{A,R})(args::Vararg{Any,N}; kwargs...) where {A,R,N}
44-
args = args::A
45-
r = (tc.f)(args...; kwargs...)
46-
r::R
155+
function typed_callable(::Type{Return}, callable::Callable) where {
156+
Return, Callable,
157+
}
158+
ret = return_type_enforcer(Return)
159+
ret callable
47160
end
48161

49162
end

0 commit comments

Comments
 (0)