Youssef Shoaib [MOD]
11/13/2022, 11:19 AMFoo<Foo<T>> -> Foo<T>
? That proof is valid in that it's not cyclical, but if you're working backwards from knowing that we need a Foo<T>
then I can't see how to ascertain that this proof isn't applicable (because the proof is applicable if e.g. there's a Foo<Foo<Foo<T>>>
that can be created). One can obviously look at other proofs that provide a Foo of something and use those as a starting point, but what if there's a proof Bar<T> -> Foo<T>
and the only proof returning Bar<T>
requires a Foo<Foo<T>>
(akin to mutual recursion)
Also will arrow-proofs support something like
@Inject fun <A, B> test(@Given bar: Bar<A, B>, @Given foo: Foo<A>): B = bar.someFunctionThatTakesFooAndReturnsB(foo)
As in, will type parameter be properly substituted in like that and allowed to influence the resolution of what proof to use for foo
, or will the caller have to specify A
and B
for the call to resolve correctly? Looking around the plugin code I can see that for every Inject
function you generate one with default arguments, so I can't really see how this use case could be supported.Javier
11/13/2022, 12:26 PM@Inject fun <A, B> test(@Given bar: Bar<A, B>, @Given foo: Foo<A>): B = bar.someFunctionThatTakesFooAndReturnsB(foo)
We are dropping value arguments injections as we are going to center on Context Receivers. So it would be written so:
class Bar<X, Y>
class Foo<Z>
fun <A, B> Bar<A, B>.someFunctionThatTakesFooAndReturnsB(foo: Foo<A>): B = TODO()
context(Bar<A, B>, Foo<A>)
fun <A, B> test(): B = someFunctionThatTakesFooAndReturnsB(this@Foo)
And where you need to consume test
, or in other words, resolve everything, you need to provide the final types for Bar
and Foo
context(Bar<String, Boolean>, Foo<String>)
@ContextResolution
fun consumer() {
test()
}
Then you get a generated consumer
// generated
@JvmName("test_generated")
fun consumer(
bar: Bar<String, Int> = provideBar(),
foo: Foo<String> = provideFoo(),
): Int = with(bar) { with(foo) { test() } }
Which you can use in, for example, main
fun main() {
consumer()
}
But in order to resolve it, it is necessary to have all Contextual
, now we would got an IDE/Compile error indicating that all proofs are missing, so we need to add something like:
@Contextual fun <X, Y> provideBar(): Bar<X, Y> = Bar()
@Contextual fun <Z> provideFoo(): Foo<Z> = Foo()
@Contextual fun provideString(): String = "baz"
@Contextual fun provideInt(): String = 42
Javier
11/13/2022, 12:27 PMFoo<Foo<T>>
and so on, I am not sure if we are covering that use case yet, but if we are not doing or it is not working we will do if it is possibleraulraja
11/13/2022, 12:33 PMtype
that you need to get injected we provide all proofs as candidates for that resolution from which the most specific and concrete one is selected.
For proofs that have injectable arguments we recurse over their resolution.
I'm not positive in this case because we don't have a test for it but it should resolve it or we should be able to fix it if it doesn't. It'd be a bug if it does not resolve
inject<Foo<Foo<Int>>() == Foo(Foo(0))
for exampleYoussef Shoaib [MOD]
11/13/2022, 12:39 PM@Contextual
fun
for it. That makes sense.
I guess for the Foo<T>
thing, let me word it instead as this:
@Contextual
context(Foo<Foo<T>>) fun <T> provideFoo(): Foo<T> = this@Foo.extractInternalFoo()
@Contextual fun <T> provideNestedFoo(): Foo<Foo<Foo<T>>> = TODO()
context(Foo<T>)
@ContextResolution
fun consumer() {
// Do something with Foo
}
fun main {
consumer()
}
This should resolve normally obviously by using provideFoo twice and provideNestedFoo once. Now imagine if provideNestedFoo didn't exist. How would the plugin know in that case that recursively trying to find @Contextual
functions for provideFoo
will never terminate?raulraja
11/13/2022, 12:42 PMYoussef Shoaib [MOD]
11/13/2022, 12:43 PMFoo<T>
example just keep on resolving forever without provideNestedFoo?raulraja
11/13/2022, 12:44 PMprovideNestedFoo
as most specific and at some point if there is a cycle it will detect itraulraja
11/13/2022, 12:45 PMYoussef Shoaib [MOD]
11/13/2022, 12:45 PMraulraja
11/13/2022, 12:46 PMraulraja
11/13/2022, 12:47 PMYoussef Shoaib [MOD]
11/13/2022, 12:51 PMFoo<Foo<Foo<Foo<T>>>>
), and without provideNestedFoo then each call to provideFoo is requesting a more-nested version of Foo, so the "request" is different each time.
I'm just wondering algorithmically if there is a way to differentiate the first example (that has provideNestedFoo) from one that doesn't have provideNestedFoo without having to work forwards from all contextuals that don't have context
arguments themselves. If you prohibit a @Contextual
function from using itself you break the example I mentioned, and if you don't then you risk stack overflows whenever a provideNestedFoo
doesn't exist.raulraja
11/13/2022, 12:58 PMYoussef Shoaib [MOD]
11/13/2022, 12:59 PMraulraja
11/13/2022, 12:59 PMraulraja
11/13/2022, 1:00 PMYoussef Shoaib [MOD]
11/13/2022, 1:12 PMConeCallConflictResolverFactory
into the session so then the FirCallResolver
uses me for conflict resolution and so I can inject my own `Candidate`s. I then use FirTowerResolver
with a custom CandidateCollector
which replaces the CheckContextReceiver
resolution stage with a custom one that tries out my `@Concept fun`s and sees if any provide the right return type.Youssef Shoaib [MOD]
11/13/2022, 1:20 PMarrow-proofs
is that it works without a ContextResolution
annotation or a context<TypesNeeded>()
call, and hence it doesn't need concrete types to be specified. I'm also planning to introduce certain plugin-provided `Concept`s like Kind<A, B>
which only has an instance available if A is a type that came from a type constructor applied to B (e.g. Kind<List<Int>, Int>
exists, so does Kind<Map<String, Boolean>, String>
but Kind<List<String>, Int>
doesn't). Then with those pieces you can practically make Type classes actually a thing in Kotlin, which is what I'm trying to achieve.
I think that arrow-proofs can actually do this rn but you'll have to call context<Functor<List<String>, String>>()
before a function that requires a Functor in its context.raulraja
11/13/2022, 1:27 PMcontext
functionraulraja
11/13/2022, 1:28 PMraulraja
11/13/2022, 1:28 PMconsumer
example.raulraja
11/13/2022, 1:29 PMraulraja
11/13/2022, 1:33 PMraulraja
11/13/2022, 1:35 PMpackage foo.bar
import io.github.kyay10.koncept.Concept
@Contextual class A
class B(val message: String)
context(A, B) fun myFun(): String {
return message
}
@Contextual
fun gimmeB() = B("OK")
@ContextResolution
context(A, B)
fun box(): String = myFun()
raulraja
11/13/2022, 1:36 PMbox
without the context(A, B)
raulraja
11/13/2022, 1:37 PMmyFun
with nested A().run { gimmeB().run { ...
raulraja
11/13/2022, 1:39 PMbox
with different impls of A and B that may be used for testingYoussef Shoaib [MOD]
11/13/2022, 1:47 PMDisplay
declaring its required contexts inside of its type parameters (it's a trick I came up with so that everything can be an object
and so no allocations would be needed).
Also issue with proofs is that it still requires the user to manually specify all contexts that are used inside the function. Not only will it get very long and unmaintainable if we're using arrow-proofs for type classes, but also possibly contexts could have some interaction with others. (e.g. a List<String>
could be coming from a library, while a List<CharSequence>
is coming from an internal declaration. A user might add the List<String>
context first, then silently the function that wanted a List<CharSequence>
will use it, but if using automatic injection, both functions will receive the most specific value. It's a small difference, but it can change behaviour in unexpected ways)Javier
11/13/2022, 1:52 PMinternal
So it is not possible to provide List<String>
outside the current module as it will be @Contextual internal fun foo(): List<String>
= …
Youssef Shoaib [MOD]
11/13/2022, 1:57 PMList
in my explanation with a library-defined type. So the library provides a MyList<String>
and the user internally provides a MyList<CharSequence>
.raulraja
11/13/2022, 1:58 PMraulraja
11/13/2022, 1:59 PMYoussef Shoaib [MOD]
11/13/2022, 2:02 PMConeCallConflictResolverFactory
for arrow-proofs if you'd like to pursue automatic injection because frankly it feels like I'm reinventing the wheel and I don't think I have the compiler expertise to continue on solo with such a mammoth task.raulraja
11/13/2022, 2:05 PMAlso issue with proofs is that it still requires the user to manually specify all contexts that are used inside the functionIf you look in the same repo you will find everything we discussed for context also working over regular value arguments. In that case we resolve injection on the body as you do there. We found resolving injection on the body it's problematic because then it resolves to the provider in the module but it cannot be replaced for testing or other purposes. In order to work similar to scala implicits you have in that case a function like:
fun <A> given(): A = TODO("replaced by compiler plugin")
val x = given<Int>()
raulraja
11/13/2022, 2:05 PMraulraja
11/13/2022, 2:06 PMraulraja
11/13/2022, 2:07 PMraulraja
11/13/2022, 2:07 PMYoussef Shoaib [MOD]
11/13/2022, 2:07 PMYoussef Shoaib [MOD]
11/13/2022, 2:08 PMJavier
11/13/2022, 2:09 PMraulraja
11/13/2022, 2:09 PMJavier
11/13/2022, 2:10 PM// generated
@JvmName("test_generated")
fun consumer(
bar: Bar<String, Int> = provideBar(),
foo: Foo<String> = provideFoo(),
): Int = with(bar) { with(foo) { test() } }
fun sometest() {
consumer(bar = anotherBar())
}
raulraja
11/13/2022, 2:11 PMWhen it comes for replacing for testing, one can obviously just turn it into a context receiver if it makes sense to replace that argument for testingThe issue is that we want to support compiled providers and we scan all the class path not just the local module. Functions with implicit arguments in our case need to survive compilation so they can be replaced when you have a library of providers
raulraja
11/13/2022, 2:12 PMYoussef Shoaib [MOD]
11/13/2022, 2:14 PMraulraja
11/13/2022, 2:15 PMraulraja
11/13/2022, 2:15 PMYoussef Shoaib [MOD]
11/13/2022, 2:15 PMKind
and such. That's probably the best of both worlds. I'll have a look to see if the visible API surface of proofs is enough to do that, and if not I might make a PR exposing some of it.raulraja
11/13/2022, 2:16 PM