Open jkrumbiegel opened 1 year ago
Thanks these are interesting ideas!
Here I'm making types construct mock objects for testing, to get around _return_type
like problems, but it does feel a bit clunky.
Your idea is interesting in that the tests are actually running at compile time. I'm not sure how far that will scale, previously I was running tests in precompile
statements and got complaints even about that being too heavy, so removed it and rely on packages running interface tests in their test suite. For me that was a little disappointing in terms of how concrete and reliable the interface is.
Having the RunTimeInterface
is a good workaround. We would still probably want to be able to run the tests for it separately to checking the trait so performance is decoupled from proof, was that the idea in your example?
The ability to compose And/Or interfaces is also cool. Especially Or, having a way to specify multiple lower level pathways that lead the same high level behavior seems pretty powerful.
I think this will at some point need the optional components as defined here, there is often just this one method in one of the interfaces that a specific type doesn't define.
Your idea is interesting in that the tests are actually running at compile time. I'm not sure how far that will scale, previously I was running tests in
precompile
statements and got complaints even about that being too heavy, so removed it and rely on packages running interface tests in their test suite. For me that was a little disappointing in terms of how concrete and reliable the interface is.Having the
RunTimeInterface
is a good workaround. We would still probably want to be able to run the tests for it separately to checking the trait so performance is decoupled from proof, was that the idea in your example?
Interesting, indeed!
@rafaqz Are you suggesting that definitions of CompiletimeInterface
and RuntimeInterface
allow users (interface definitions) to opt-in to compile-time checks? That would be nice, as long as it would not be a barrier for actually defining interfaces.
We would still probably want to be able to run the tests for it separately to checking the trait so performance is decoupled from proof, was that the idea in your example?
Yes that was the idea, that some things you're only ever going to be able to check at runtime. So they can be used for dynamic dispatch. But never for JET-style testing.
More like some interface checks might be too expensive to use compile-time checks and would need to work like they do currently in this package, with traits being separated from tests.
But if we actually were to do anything at compile time, we could just revert you last PR so that the mock object constructors are known at compile time, and go back to doing tests in precompile to avoid the overheads.
(Maybe Im not clear but a lot of my thinking and design here is around reducing the costs to a minimum so there is no performance or ttfx related resistance in the ecosystem)
More like some interface checks might be too expensive to use compile-time checks and would need to work like they do currently in this package, with traits being separated from tests.
But if we actually were to do anything at compile time, we could just revert you last PR so that the mock object constructors are known at compile time, and go back to doing tests in precompile to avoid the overheads.
(Maybe Im not clear but a lot of my thinking and design here is around reducing the costs to a minimum so there is no performance or ttfx related resistance in the ecosystem)
I think that was reasonably clear, but thanks for emphasizing your thinking - I completely agree that costs should be minimized.
One could also say, that one would expect the cost of both defining an interface and stating that an interface is implemented should be purely compile time and not a lot of it (close to zero).
JET-style static testing would also be an expectation (I have at least seen JET complain about a Duck not being able to dig, so I guess that already works? 😄).
Wrt. compile-time checks and compile-time test objects, I can see how that would need compile-time test objects for the predicate tests currently in this package, but I am wondering whether some tests could be done at compile-time even without test objects - e.g. just checking existence of methods and their signatures...
One could also say, that one would expect the cost of both defining an interface and stating that an interface is implemented should be purely compile time and not a lot of it (close to zero).
Totally, otherwise it needs to be done during testing.
Yes that was the idea, that some things you're only ever going to be able to check at runtime. So they can be used for dynamic dispatch. But never for JET-style testing.
@jkrumbiegel I'm not sure if I totally understand this... the interfaces defined in this pacakges currently are always used for compile time dispatch, rather than dynamic, its just that the testing of the trait is separated from the use of the trait to remove the runtime overheads. (with implements(Interface, Type)
and test(Interface, Type, mocks)
Do you mean some interfaces can't be known at compile time at all?
Well you had this in your readme which looked like runtime to me:
x -> age(x) >= 0,
That was the only reason why I included it, in principle I like compile time much more. Or to phrase it differently, type-based traits and not value-based traits.
Yeah I can see how it looks like that! But that runs in the tests (originally in precompile), not actually when you check a trait. The idea of putting it in the macro is to tie the trait directly to the test.
I will have to make all of these things clearer in the readme. See discussion in https://github.com/rafaqz/Interfaces.jl/issues/12
An idea might be to have separately specified type traits/predicates and value traits/predicates.
That was the idea of CompileTimeInterface
vs RunTimeInterface
in the code snippet above :) I just didn't flesh out RunTimeInterface
at all. Although I'm pretty sure one can go far without touching values at all. At some point you move from "interface" to just "code" that does something else based on values it encounters.
But does it make sense now that x -> age(x) >= 0
does not actually run at runtime?
One of the main questions with designing this thing is how to separate trait definitions from tests, and how object types that an interface is defined for are mapped to constructed objects that can be run in tests.
But does it make sense now that x -> age(x) >= 0 does not actually run at runtime?
I have not understood that part yet, no. Do you mean you want to check instances of structs at compile time by creating and testing them inside the macro expansion step? So it's still value based, just run at a different time?
But does it make sense now that
x -> age(x) >= 0
does not actually run at runtime?
Assuming "runtime" should have been "compile-time".
I would think there could be lots of type-traits and/or type-predicates that could be checked at compile-time - without incurring too much overhead, e.g. checking implements(interface_type, type)
or something like test_implements(x, m::HasMethodInterface) = hasmethod(x, m.signature)
in the first post.
Would that work?
That was the idea of
CompileTimeInterface
vsRunTimeInterface
in the code snippet above :) I just didn't flesh outRunTimeInterface
at all. Although I'm pretty sure one can go far without touching values at all. At some point you move from "interface" to just "code" that does something else based on values it encounters.
Excellent idea! 😄
Though it might be true that something is "interface" and something is "code", I do think that both type-traits and value-predicates have a place in an interface definition for Julia.
Do you mean you want to check instances of structs at compile time by creating and testing them inside the macro expansion step? So it's still value based, just run at a different time?
So how Ive done it here is that all of the checks actaully happen during testing, not when you define the Interface with the macro. The macro just makes the function that can run the tests.
So they can be either compile compile time or runtime checks, but it doesnt really effect (runtime) usage (of the traits) much either way.
So the traits you get are backed by tests that run at a different time. Maybe a bit weaker than you are going for. The reason was to reduce complaints about ttfx or runtime cost of using interfaces, so more a social/practical thing than the best design.
I really wanted to do the checks during precompilatiion.
Also FYI
https://github.com/Keno/InterfaceSpecs.jl
Keno doesn't seem to care about using return_type
and similar things either
As far as I can tell it doesn't produce no-cost traits, it just the tests of the interface. But going for something a lot more advanced than I was.
I think it's common enough in generic programming that one works with different layers of objects. For example some iterator of objects, where the iterator could be an array, a tuple, etc. and the object anything that serves some further purpose. That further purpose could again be locked down by an interface I thought. Here I jotted down some code where I made a
ReturnInterface
that should guarantee a given signature returns an object that conforms to a given interface (that interface can simply be that it should be a certain type, or whatever more complex thing).I also had the idea of splitting run time and compile time traits and only allowing composition of compile time traits into new compile time traits, but I haven't followed that here. Just including it for inspiration. I bet that relying on
_return_type
has all sorts of problems, I already hit some uninferred cases during casual testing. But it seems like the only thing we got after all?