Invariants.jl
Invariants.jl copied to clipboard
Basic interface discussion
I just pushed some preliminary docs, @rafaqz would be great to get some feedback on the high-level API before I move the core pieces to a base package :)
See https://lorenzoh.github.io/Invariants.jl/dev/documents/docs/guide.md and https://lorenzoh.github.io/Invariants.jl/dev/documents/docs/examples.md
Lgtm
Only thing I wondered is if there should be a check(Bool, invariant, x)
that returns a Bool
instead if throwing the error.
Will release a first version then 👍
There is, see https://lorenzoh.github.io/Invariants.jl/dev/sourcefiles/Invariants/src/check.jl
Now that inv(input)
and convert(Bool, inv(input))
can be used to check in an invariant, the last remaining part of the interface left to resolve is how to check an invariant and throw an error if it is violated. There is a check_throw
function right now, but maybe a cleaner usage that doesn't introduce a new symbol is possible.
One way could be throw(inv(input))
that throws an InvariantException
only if the invariant is violated.
Thoughts @rafaqz ?
EDIT: adding a method to throw
isn't possible since it's built-in. But happy to hear other suggestions
Yes exactly, I was wondering how to handle this. You can probably use rethrow
? and locally packages can then define:
_throw(name, ::Bool) = error("$name failed)
_throw(name, x) = rethrow(x)
Then
convert(Bool, result) || _throw(testname, result)
And that will handle Bool
or more complex objects that define their own rethrow
method.
Edit: this could also allow you to use try ... catch
in invariant
to e.g. catch method errors then rethrow them after printing the invariant messages. I've actually been wondering how to do this too.
check_throw
already does this and throws a InvariantException
which ensures that the error messages are displayed as rich text. I was thinking more of an alternative syntax that doesn't require importing check_throw
.
How about check(Exception, invariant, input)
/invariant(Exception, input)
?
What does check_throw
already do? Sorry I don't understand.
I can only call base methods from Interfaces.jl because I intend to not depend on Invariants.jl - keeping things decoupled is best. I'm trying to facilitate users using invariant
if they want to.
So check
and check_throw
are not options I have available. Nor is passing Exception
to anything.
Simply defining rethrow(::CheckReturn)
may be enough for my needs without an Invariants.jl dep.
Although not hard to understand, the "Combining invariants" part in the docs seems not that intuitive to me (perhaps it's because the codes has 4 levels of identation)
Instead of
inv_indexing = invariant(
"`xs` is indexable",
[
Invariants.hasmethod_invariant(length, :xs),
invariant("`xs is not empty`") do (; xs)
isempty(xs) ? "`xs` is empty!" : nothing
end,
Invariants.hasmethod_invariant(Base.getindex, :xs, :idx => 1),
],
inputfn = xs -> (; xs)
)
check(inv_indexing, [1, 2, 3])
how about flattening the codes by overriding &&
and ||
?
has_length = Invariants.hasmethod_invariant(length, :xs)
has_getindex = Invariants.hasmethod_invariant(Base.getindex, :xs, :idx => 1)
is_array_like = invariant("is arraylike", has_length && has_getindex)
non_empty = invariant("`xs is not empty`") do (; xs)
isempty(xs) ? "`xs` is empty!" : nothing
end
inv_indexing = invariant("`xs` is indexable", non_empty && is_array_like)
check(inv_indexing, [1, 2, 3])
The existence of inputfn
seems not very friendly to composibility at first glance, but I'm not sure how to make it better.
Splitting it up into multiple lines makes it much more readable, that's a good idea!
How does inputfn make it less composable?
How does inputfn make it less composable?
We might need to configure inputfn
for each invariant, is it possible to compose multiple invariants together without messing up with inputfn
?
You can wrap any invariant in a ˋinvariant(inv; inputfn)ˋ and then compose those. Does that solve what you're trying to do?