New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
arrow.generic.Coproduct
arities
#954
Changes from 23 commits
3df4dff
b547976
8b37abd
7231626
12a5894
8bec622
887a8d0
0dab2ea
cb1dac3
664e7df
34fb264
dc23394
c452385
ab6a34a
15406ca
3bd508c
6f4a33f
469cab3
9e33461
8ab2d98
5fcc872
8c62b46
d97ccc1
f25abc1
569e6fd
87ba31c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
package arrow.generic | ||
|
||
import com.squareup.kotlinpoet.AnnotationSpec | ||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.KModifier | ||
import com.squareup.kotlinpoet.LambdaTypeName | ||
import com.squareup.kotlinpoet.ParameterSpec | ||
import com.squareup.kotlinpoet.ParameterizedTypeName | ||
import com.squareup.kotlinpoet.PropertySpec | ||
import com.squareup.kotlinpoet.TypeSpec | ||
import com.squareup.kotlinpoet.TypeVariableName | ||
import java.io.File | ||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
|
||
private val genericsToClassNames = mapOf( | ||
"A" to "First", | ||
"B" to "Second", | ||
"C" to "Third", | ||
"D" to "Fourth", | ||
"E" to "Fifth", | ||
"F" to "Sixth", | ||
"G" to "Seventh", | ||
"H" to "Eighth", | ||
"I" to "Ninth", | ||
"J" to "Tenth", | ||
"K" to "Eleventh", | ||
"L" to "Twelfth", | ||
"M" to "Thirteenth", | ||
"N" to "Fourteenth", | ||
"O" to "Fifteenth", | ||
"P" to "Sixteenth", | ||
"Q" to "Seventeenth", | ||
"R" to "Eighteenth", | ||
"S" to "Nineteenth", | ||
"T" to "Twentieth", | ||
"U" to "TwentyFirst", | ||
"V" to "TwentySecond" | ||
) | ||
|
||
fun generateCoproducts(destination: File) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class uses KotlinPoet to generate It packages with Questions: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also feel like I'm doing something wrong with the generics here too: val coproduct2: Coproduct2<Long, String> = 6L.cop<Long, String>()
coproduct2.map<String, String, Int> { it.length } shouldBe 6L.cop<Long, Int>() This appears to be valid in the editor, but the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generating up to 22 is fine, that is what we do for the rest at this time. We don't use Kotlin Poet and do the code the generation manually as in the other processors to avoid introducing extra deps at compile time that users may already have. Having said that I have no strong opinions but would be nice to be consistent with the rest and avoid parts of code written in different styles since we have no plans to migrate all of our processors to KotlinPoet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no biased side in Coproduct and there should be no right biased map or other biased methods. A map over this Coproduct would have to contemplate all cases so it's esentially a specialization of fold that preserves the container. For example: https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#coproducts-and-discriminated-unions The poly function there contemplates all cases in which the Coproduct may find itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm with @raulraja in not bringing KotlinPoet for now. It's all a bit stringly typed, but it's been good enough for our needs so far :) |
||
for (size in 2 until genericsToClassNames.size + 1) { | ||
val generics = genericsToClassNames.keys.toList().take(size) | ||
|
||
FileSpec.builder("arrow.generic.coproduct$size", "Coproduct$size") | ||
.apply { | ||
addCoproductClassDeclaration(generics) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to move some of these functions down the road to a common place for other processors to have them available. |
||
addCoproductOfConstructors(generics) | ||
addCopExtensionConstructors(generics) | ||
addSelectFunctions(generics) | ||
addFoldFunction(generics) | ||
} | ||
.build() | ||
.writeTo(destination) | ||
} | ||
} | ||
|
||
private fun FileSpec.Builder.addCoproductClassDeclaration(generics: List<String>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice use of receiver extension functions |
||
addType( | ||
TypeSpec.classBuilder("Coproduct${generics.size}") | ||
.addModifiers(KModifier.SEALED) | ||
.addTypeVariables(generics.map { TypeVariableName(it) }) | ||
.build() | ||
) | ||
|
||
for (generic in generics) { | ||
addType( | ||
TypeSpec.classBuilder(genericsToClassNames[generic]!!) | ||
.addModifiers(KModifier.INTERNAL, KModifier.DATA) | ||
.addTypeVariables(generics.toTypeParameters()) | ||
.superclass(parameterizedCoproductNClassName(generics)) | ||
.addProperty( | ||
PropertySpec.builder(generic.toLowerCase(), TypeVariableName(generic)) | ||
.initializer(generic.toLowerCase()) | ||
.build() | ||
) | ||
.primaryConstructor( | ||
FunSpec.constructorBuilder() | ||
.addParameter(generic.toLowerCase(), TypeVariableName(generic)) | ||
.build() | ||
) | ||
.build() | ||
) | ||
} | ||
} | ||
|
||
private fun FileSpec.Builder.addCoproductOfConstructors(generics: List<String>) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please type all functions regardless of them being private or overrides. We prefer not to rely on type inference in Arrow since typed functions serve as documentation and are explicit as to what they do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh gotcha, yeah I can add the |
||
for (generic in generics) { | ||
val additionalParameterCount = generics.indexOf(generic) | ||
|
||
addFunction( | ||
FunSpec.builder("coproductOf") | ||
.addAnnotations(additionalParameterSuppressAnnotation(additionalParameterCount)) | ||
.addTypeVariables(generics.toTypeParameters()) | ||
.addParameter(generic.toLowerCase(), TypeVariableName(generic)) | ||
.addParameters(additionalParameterSpecs(additionalParameterCount)) | ||
.addStatement("return ${genericsToClassNames[generic]}(${generic.toLowerCase()})") | ||
.returns(parameterizedCoproductNClassName(generics)) | ||
.build() | ||
) | ||
} | ||
} | ||
|
||
private fun FileSpec.Builder.addCopExtensionConstructors(generics: List<String>) { | ||
for (generic in generics) { | ||
val additionalParameterCount = generics.indexOf(generic) | ||
|
||
addFunction( | ||
FunSpec.builder("cop") | ||
.addAnnotations(additionalParameterSuppressAnnotation(additionalParameterCount)) | ||
.receiver(TypeVariableName(generic)) | ||
.addTypeVariables(generics.toTypeParameters()) | ||
.addParameters(additionalParameterSpecs(additionalParameterCount)) | ||
.addStatement("return coproductOf<${generics.joinToString(separator = ", ")}>(this)") | ||
.returns(parameterizedCoproductNClassName(generics)) | ||
.build() | ||
) | ||
} | ||
} | ||
|
||
private fun FileSpec.Builder.addSelectFunctions(generics: List<String>) { | ||
addImport("arrow.core", "Option") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For commonly used package like
|
||
addImport("arrow.core", "toOption") | ||
|
||
for (generic in generics) { | ||
val receiverGenerics = generics | ||
.map { if (it == generic) TypeVariableName(generic) else TypeVariableName("*") } | ||
.toTypedArray() | ||
val additionalParameterCount = generics.indexOf(generic) | ||
|
||
addFunction( | ||
FunSpec.builder("select") | ||
.addAnnotations(additionalParameterSuppressAnnotation(additionalParameterCount)) | ||
.addTypeVariable(TypeVariableName(generic)) | ||
.receiver(ClassName("", "Coproduct${generics.size}").parameterizedBy(*receiverGenerics)) | ||
.addParameters(additionalParameterSpecs(additionalParameterCount)) | ||
.returns(ClassName("arrow.core", "Option").parameterizedBy(TypeVariableName(generic))) | ||
.addStatement("return (this as? ${genericsToClassNames[generic]})?.${generic.toLowerCase()}.toOption()") | ||
.build() | ||
) | ||
} | ||
} | ||
|
||
private fun FileSpec.Builder.addFoldFunction(generics: List<String>) { | ||
addFunction( | ||
FunSpec.builder("fold") | ||
.receiver(parameterizedCoproductNClassName(generics)) | ||
.addTypeVariables(generics.toTypeParameters() + TypeVariableName("RESULT")) | ||
.addParameters( | ||
generics.map { | ||
ParameterSpec.builder( | ||
it.toLowerCase(), | ||
LambdaTypeName.get( | ||
parameters = listOf(ParameterSpec.unnamed(TypeVariableName(it))), | ||
returnType = TypeVariableName("RESULT") | ||
) | ||
).build() | ||
} | ||
) | ||
.returns(TypeVariableName("RESULT")) | ||
.apply { | ||
beginControlFlow("return when (this)") | ||
for (generic in generics) { | ||
addStatement("is ${genericsToClassNames[generic]} -> ${generic.toLowerCase()}(this.${generic.toLowerCase()})") | ||
} | ||
endControlFlow() | ||
} | ||
.build() | ||
) | ||
} | ||
|
||
private fun List<String>.toTypeParameters() = map { TypeVariableName(it) } | ||
|
||
private fun parameterizedCoproductNClassName(generics: List<String>): ParameterizedTypeName = | ||
ClassName("", "Coproduct${generics.size}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find the indentation excessive. Not sure if it's your IDE or detekt configuration. |
||
.parameterizedBy(*generics.map { TypeVariableName(it) }.toTypedArray()) | ||
|
||
private fun additionalParameterSuppressAnnotation(count: Int): List<AnnotationSpec> = | ||
if (count > 0) { | ||
listOf( | ||
AnnotationSpec.builder(Suppress::class) | ||
.addMember("\"UNUSED_PARAMETER\"") | ||
.build() | ||
) | ||
} else { | ||
emptyList() | ||
} | ||
|
||
private fun additionalParameterSpecs(count: Int): List<ParameterSpec> = List(count) { | ||
ParameterSpec.builder("dummy$it", Unit::class) | ||
.defaultValue("Unit") | ||
.build() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package arrow.generic | ||
|
||
import com.google.auto.service.AutoService | ||
import arrow.common.utils.AbstractProcessor | ||
import arrow.coproduct | ||
import java.io.File | ||
import javax.annotation.processing.Processor | ||
import javax.annotation.processing.RoundEnvironment | ||
import javax.lang.model.SourceVersion | ||
import javax.lang.model.element.TypeElement | ||
|
||
@AutoService(Processor::class) | ||
class CoproductProcessor : AbstractProcessor() { | ||
|
||
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported() | ||
|
||
override fun getSupportedAnnotationTypes(): Set<String> = setOf(coproduct::class.java.canonicalName) | ||
|
||
override fun onProcess(annotations: Set<TypeElement>, roundEnv: RoundEnvironment) { | ||
val generatedDir = File(this.generatedDir!!, "").also { it.mkdirs() } | ||
|
||
generateCoproducts(generatedDir) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at other things as examples for this, I'm not sure if this is correct. Questions: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Annotations are used to find target elements on the source code that you end up validating and generating code related to those. But here we don't have any targets, but just generic generation. Ideally It should work and be okay to not add any supported annotations to the processor, but I think that's not working? It's okay to add a dummy annotation if you need to. You can also just use one of the ones we already have for the generics module. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't able to see any way to do it without the annotated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we annotate the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I follow entirely, I tried |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package arrow | ||
|
||
annotation class coproduct |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package arrow.generic | ||
|
||
import arrow.coproduct | ||
|
||
/** | ||
* This annotated class exists to trigger the code generation. | ||
*/ | ||
@coproduct | ||
private object Coproduct |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package arrow.generic | ||
|
||
import arrow.core.None | ||
import arrow.core.Option | ||
import arrow.core.Some | ||
import arrow.core.some | ||
import arrow.generic.coproduct2.Coproduct2 | ||
import arrow.generic.coproduct2.cop | ||
import arrow.generic.coproduct2.coproductOf | ||
import arrow.generic.coproduct2.fold | ||
import arrow.generic.coproduct2.select | ||
import arrow.generic.coproduct22.Coproduct22 | ||
import arrow.generic.coproduct3.cop | ||
import arrow.generic.coproduct3.fold | ||
import arrow.generic.coproduct3.select | ||
import arrow.test.UnitSpec | ||
import io.kotlintest.KTestJUnitRunner | ||
import io.kotlintest.matchers.shouldBe | ||
import org.junit.runner.RunWith | ||
|
||
@RunWith(KTestJUnitRunner::class) | ||
class CoproductTest : UnitSpec() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added some basic tests here, with code generation I really wasn't super sure of what to test. Questions: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's supporting features like map, flatMap and fold shouldn't it at least be tested under Functor, Monad and Foldable laws probably? |
||
init { | ||
"Coproducts should be generated up to 22" { | ||
var two: Coproduct2<Unit, Unit>? = null | ||
var twentytwo: Coproduct22<Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit>? = null | ||
} | ||
|
||
"select should return None if value isn't correct type" { | ||
val coproduct2 = "String".cop<String, Long>() | ||
|
||
coproduct2.select<Long>() shouldBe None | ||
} | ||
|
||
"select returns Some if value is correct type" { | ||
val coproduct2 = "String".cop<String, Long>() | ||
|
||
coproduct2.select<String>() shouldBe Some("String") | ||
} | ||
|
||
"coproductOf(A) should equal cop<A, B>()" { | ||
"String".cop<String, Long>() shouldBe coproductOf<String, Long>("String") | ||
} | ||
|
||
"Coproduct2 fold" { | ||
val coproduct2 = 100L.cop<Long, Int>() | ||
|
||
coproduct2.fold( | ||
{ "Long$it" }, | ||
{ "Int$it" } | ||
) shouldBe "Long100" | ||
} | ||
|
||
"Coproduct3 should handle multiple nullable types" { | ||
val value: String? = null | ||
val coproduct3 = value.cop<Long, Float?, String?>() | ||
|
||
coproduct3.select<Long>() shouldBe None | ||
coproduct3.select<Float?>() shouldBe None | ||
coproduct3.select<String?>() shouldBe None | ||
|
||
coproduct3.fold( | ||
{ "First" }, | ||
{ "Second" }, | ||
{ "Third" } | ||
) shouldBe "Third" | ||
} | ||
|
||
"Coproduct3 should handle multiple types with generics" { | ||
val value: Option<String> = "String".some() | ||
val coproduct3 = value.cop<Option<Long>, Option<Float>, Option<String>>() | ||
|
||
coproduct3.select<Option<Long>>() shouldBe None | ||
coproduct3.select<Option<Float>>() shouldBe None | ||
coproduct3.select<Option<String>>() shouldBe value.some() | ||
|
||
coproduct3.fold( | ||
{ "First" }, | ||
{ "Second" }, | ||
{ "Third" } | ||
) shouldBe "Third" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compileOnly
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it and it didn't generate the files, I'm not sure, I could have something setup incorrectly too because I have to manually add the generated dirs as source dirs still
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's fine to be
compile
because this is the annotation processor and itself it's compile only throughkapt
to it's dependent projects