Skip to content

Commit 245ad1b

Browse files
abergfeldraulraja
authored andcommitted
Update Coproducts (#1291)
* Make data classes public, deprecate old extension constructor and add new one * Update docs * Use camelcase class names for function names * Deprecate coproductOf as well * First pass at runnable docs * First pass at kdocs * Add test for type inference with ADT * Add example of how Coproducts can flatten hierarchies * Move Constructors to top level section since it's no longer an extension function * Update to use @param and @return for kdocs so the docs get generated correctly * Add String Util method and update docs
1 parent c37779b commit 245ad1b

File tree

4 files changed

+252
-57
lines changed

4 files changed

+252
-57
lines changed

modules/core/arrow-generic/src/test/kotlin/arrow/generic/CoproductTest.kt

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
package arrow.generic
22

3-
import arrow.core.None
4-
import arrow.core.Option
5-
import arrow.core.Some
6-
import arrow.core.some
3+
import arrow.core.*
74
import arrow.generic.coproduct2.Coproduct2
85
import arrow.generic.coproduct2.cop
96
import arrow.generic.coproduct2.coproductOf
107
import arrow.generic.coproduct2.fold
118
import arrow.generic.coproduct2.select
129
import arrow.generic.coproduct22.Coproduct22
13-
import arrow.generic.coproduct3.cop
14-
import arrow.generic.coproduct3.fold
15-
import arrow.generic.coproduct3.select
10+
import arrow.generic.coproduct3.*
1611
import arrow.test.UnitSpec
17-
import io.kotlintest.shouldBe
1812
import io.kotlintest.runner.junit4.KotlinTestRunner
13+
import io.kotlintest.shouldBe
1914
import org.junit.runner.RunWith
2015

2116
@RunWith(KotlinTestRunner::class)
@@ -24,8 +19,13 @@ class CoproductTest : UnitSpec() {
2419
init {
2520

2621
"Coproducts should be generated up to 22" {
27-
class Proof2(f: Coproduct2<Unit, Unit>) { val x = f }
28-
class Proof22(f: Coproduct22<Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit>) { val x = f }
22+
class Proof2(f: Coproduct2<Unit, Unit>) {
23+
val x = f
24+
}
25+
26+
class Proof22(f: Coproduct22<Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit, Unit>) {
27+
val x = f
28+
}
2929
}
3030

3131
"select should return None if value isn't correct type" {
@@ -82,5 +82,49 @@ class CoproductTest : UnitSpec() {
8282
{ "Third" }
8383
) shouldBe "Third"
8484
}
85+
86+
"cop replacement functions are equivalent" {
87+
val firstValue = "String"
88+
val secondValue = 100L
89+
val thirdValue = none<String>()
90+
91+
firstValue.cop<String, Long, Option<String>>() shouldBe firstValue.first<String, Long, Option<String>>()
92+
secondValue.cop<String, Long, Option<String>>() shouldBe secondValue.second<String, Long, Option<String>>()
93+
thirdValue.cop<String, Long, Option<String>>() shouldBe thirdValue.third<String, Long, Option<String>>()
94+
}
95+
96+
"types can be inferred with the extension constructors" {
97+
fun typeInferenceTest(input: String?): Coproduct3<String, Int, Option<String>> {
98+
return when {
99+
input == null -> none<String>().third()
100+
input.length > 100 -> input.length.second()
101+
else -> input.first()
102+
}
103+
}
104+
105+
typeInferenceTest(null) shouldBe Third<String, Int, Option<String>>(none())
106+
}
107+
108+
"types can be inferred with the ADT constructors" {
109+
fun typeInferenceTest(input: String?): Coproduct3<String, Int, Option<String>> {
110+
return when {
111+
input == null -> Third(none())
112+
input.length > 100 -> Second(input.length)
113+
else -> First(input)
114+
}
115+
}
116+
117+
typeInferenceTest(null) shouldBe none<String>().third<String, Int, Option<String>>()
118+
}
119+
120+
"Coproduct data classes should be available" {
121+
val firstValue = "String"
122+
val secondValue = 100L
123+
val thirdValue = none<String>()
124+
125+
First<String, Long, Option<String>>(firstValue) shouldBe firstValue.first<String, Long, Option<String>>()
126+
Second<String, Long, Option<String>>(secondValue) shouldBe secondValue.second<String, Long, Option<String>>()
127+
Third<String, Long, Option<String>>(thirdValue) shouldBe thirdValue.third<String, Long, Option<String>>()
128+
}
85129
}
86130
}

modules/docs/arrow-docs/docs/docs/generic/coproduct/README.md

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ compile 'io.arrow-kt:arrow-generic:$arrow_version'
2323

2424
Coproducts represent a sealed hierarchy of types where only one of the specified set of types exist every time. Conceptually, it's very similar to the stdlib `sealed` class, or to [Either]({{ '/docs/arrow/core/either' | relative_url }}) if we move on to Arrow data types. [Either]({{ '/docs/arrow/core/either' | relative_url }}) supports one of two values, an `Either<A, B>` has to contain an instance of `A` or `B`. We can extrapolate that concept to `N` number of types. So a `Coproduct5<A, B, C, D, E>` has to contain an instance of `A`, `B`, `C`, `D`, or `E`. For example, perhaps there's a search function for a Car Dealer app that can show `Dealership`s, `Car`s and `SalesPerson`s in a list, we could model that list of results as `List<Coproduct3<Dealership, Car, SalesPerson>`. The result would contain a list of heterogeneous elements but each element is one of `Dealership`, `Car` or `SalesPerson` and our UI can render the list elements based on those types.
2525

26-
```kotlin:ank
26+
```kotlin:ank:playground
2727
import arrow.generic.*
28-
import arrow.generic.coproduct3.Coproduct3
29-
import arrow.generic.coproduct3.fold
28+
import arrow.generic.coproduct3.*
3029
3130
fun toDisplayValues(items: List<Coproduct3<Car, Dealership, Salesperson>>): List<String> {
3231
return items.map {
@@ -37,6 +36,18 @@ fun toDisplayValues(items: List<Coproduct3<Car, Dealership, Salesperson>>): List
3736
)
3837
}
3938
}
39+
40+
fun main() {
41+
println(
42+
toDisplayValues(
43+
listOf<Coproduct3<Car, Dealership, Salesperson>>(
44+
Car(Speed(100)).first(),
45+
Dealership("Cedar Falls, Iowa").second(),
46+
Salesperson("Car McCarface").third()
47+
)
48+
)
49+
)
50+
}
4051
```
4152

4253
Let's say we have an api. Our api operates under the following conditions:
@@ -45,73 +56,122 @@ Let's say we have an api. Our api operates under the following conditions:
4556

4657
Because we have some common errors that every endpoint can return to us, we can define a sealed class of these and call them `CommonServerError` because they make sense to be sealed together for reusability. Likewise, we can logically group our specific errors into `RegistrationError` and we have `Registration` as our success type.
4758

48-
With Coproducts, we're able to define a result for this api call as `Coproduct3<CommonServerError, RegistrationError, Registration>`. We've been able to compose these results without having to write our own sealed class containing all the common errors for each endpoint.
59+
The most obvious approach would be to use Kotlin's `sealed class` to create a return type for this api call:
4960

50-
#### Extensions
61+
```kotlin:ank
62+
sealed class ApiResult {
63+
data class CommonServerError(val value: CommonServerError): ApiResult()
64+
data class RegistrationError(val value: RegistrationError): ApiResult()
65+
data class Registration(val value: Registration): ApiResult()
66+
}
67+
```
5168

52-
##### coproductOf
69+
Immediately we can observe there's boilerplate to this approach. We need to make a data class that just holds a single value to combine these results to a common type. Any time we need to add to ApiResult we need to go through and add another wrapping class to conform it to this type.
5370

54-
So now that we've got our api response modeled, we need to be able to create an instance of `Coproduct3`.
71+
Upon using it we can also observe that there's unwrapping that needs to take place to actually use the values:
5572

5673
```kotlin:ank
74+
fun handleResult(apiResult: ApiResult): String {
75+
return when (apiResult) {
76+
is ApiResult.CommonServerError -> "Common: ${apiResult.value}"
77+
is ApiResult.RegistrationError -> "RegistrationError: ${apiResult.value}"
78+
is ApiResult.Registration -> "Registration: ${apiResult.value}"
79+
}
80+
}
81+
```
82+
83+
With Coproducts, we're able to define a result for this api call as `typealias ApiResult = Coproduct3<CommonServerError, RegistrationError, Registration>`. We've been able to compose these results without having to write our own sealed class containing all the common errors for each endpoint. This lets us flatten a layer of boilerplate by abstracting the sealed hierarchy and lets us freely compose types from different domain types.
84+
85+
#### Constructors
86+
87+
So now that we've got our api response modeled, we need to be able to create an instance of `Coproduct3`.
88+
89+
```kotlin:ank:playground
5790
import arrow.generic.*
58-
import arrow.generic.coproduct3.coproductOf
91+
import arrow.generic.coproduct3.First
92+
5993
60-
val apiResult = coproductOf<CommonServerError, RegistrationError, Registration>(ServerError) //Returns Coproduct3<CommonServerError, RegistrationError, Registration>
94+
fun main() {
95+
println(
96+
//sampleStart
97+
First<CommonServerError, RegistrationError, Registration>(ServerError)
98+
//sampleEnd
99+
)
100+
}
61101
```
62102

63-
There are `coproductOf` constructor functions for each Coproduct regardless of the arity (number of types). All we have to do is pass in our value and have the correct type parameters on it, the value must be a type declared on the function call.
103+
Coproducts are backed by a sealed class hierarchy and we can use the data classes to create Coproducts. The class names resemble the index of the generic, for example, Coproduct3<A, B, C> has First, Second and Third. First references the `A`, Second references the `B` and so forth.
64104

65105
If we pass in a value that doesn't correspond to any types on the Coproduct, it won't compile:
66106

67-
```kotlin:ank
107+
```kotlin:ank:fail
68108
import arrow.generic.*
69-
import arrow.generic.coproduct3.coproductOf
109+
import arrow.generic.coproduct3.First
70110
71-
//val apiResult = coproductOf<String, RegistrationError, Registration>(ServerError)
72-
//error: type mismatch: inferred type is ServerError but String was expected
111+
fun main() {
112+
println(
113+
//sampleStart
114+
First<String, RegistrationError, Registration>(ServerError)
115+
//sampleEnd
116+
)
117+
}
73118
```
74119

75-
##### cop
120+
#### Extensions
121+
122+
##### constructors
76123

77124
You might be saying "That's great and all but passing in values as parameters is so Java, I want something more Kotlin!". Well look no further, just like [Either]({{ '/docs/arrow/core/either' | relative_url }})'s `left()` and `right()` extension methods, Coproducts can be created with an extension method on any type:
78125

79-
```kotlin:ank
126+
```kotlin:ank:playground
80127
import arrow.generic.*
81-
import arrow.generic.coproduct3.cop
128+
import arrow.generic.coproduct3.*
82129
83-
val apiResult = ServerError.cop<CommonServerError, RegistrationError, Registration>() //Returns Coproduct3<CommonServerError, RegistrationError, Registration>
130+
fun main() {
131+
//sampleStart
132+
println(ServerError.first<CommonServerError, RegistrationError, Registration>())
133+
println(CarAlreadyRegistered.second<CommonServerError, RegistrationError, Registration>())
134+
//sampleEnd
135+
}
84136
```
85137

86-
All we have to do is provide the type parameters and we can make a Coproduct using the `cop` extension method. Just like `coproductOf`, if the type of the value isn't in the type parameters of the method call, it won't compile:
138+
All we have to do is provide the type parameters and we can make a Coproduct using the extension methods. Just like the data classes, if the type of the value isn't in the type parameters of the method call, or it's not in the correct type parameter index, it won't compile:
87139

88-
```kotlin:ank
140+
```kotlin:ank:fail
89141
import arrow.generic.*
90-
import arrow.generic.coproduct3.cop
142+
import arrow.generic.coproduct3.first
91143
92-
//val apiResult = ServerError.cop<String, RegistrationError, Registration>()
93-
//error: type mismatch: inferred type is ServerError but String was expected
144+
fun main() {
145+
println(
146+
//sampleStart
147+
"String".first<CommonServerError, RegistrationError, Registration>()
148+
//sampleEnd
149+
)
150+
}
94151
```
95152

96153
##### fold
97154

98155
Obviously, we're not just modeling errors for fun, we're going to handle them! All Coproducts have `fold` which allows us to condense the Coproduct down to a single type. For example, we could handle errors as such in a UI:
99156

100-
```kotlin:ank
157+
```kotlin:ank:playground
101158
import arrow.generic.*
102-
import arrow.generic.coproduct3.Coproduct3
103-
import arrow.generic.coproduct3.fold
159+
import arrow.generic.coproduct3.*
104160
105161
fun handleCommonError(commonError: CommonServerError) {
162+
println("Encountered a common error $commonError")
106163
}
107164
108165
fun showCarAlreadyRegistered() {
166+
println("Car is already registered")
109167
}
110168
111169
fun callPolice() {
170+
println("That car is stolen!!!!1!")
112171
}
113172
114173
fun showCarSuccessfullyRegistered(car: Car) {
174+
println("Successfully Registered!")
115175
}
116176
117177
fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>) = apiResult.fold(
@@ -128,14 +188,17 @@ fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError,
128188
showCarSuccessfullyRegistered(registeredCar)
129189
}
130190
)
191+
192+
fun main() {
193+
renderApiResult(Registration(Car(Speed(100))).third())
194+
}
131195
```
132196

133197
This example returns `Unit` because all of these are side effects, let's say our application was built for a command line and we just have to show a `String` for the result of the call (if only it was always that easy):
134198

135-
```kotlin:ank
199+
```kotlin:ank:playground
136200
import arrow.generic.*
137-
import arrow.generic.coproduct3.Coproduct3
138-
import arrow.generic.coproduct3.fold
201+
import arrow.generic.coproduct3.*
139202
140203
fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): String = apiResult.fold(
141204
{ commonError ->
@@ -155,6 +218,10 @@ fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError,
155218
"Successfully Registered: $registration.car"
156219
}
157220
)
221+
222+
fun main() {
223+
println(renderApiResult(Registration(Car(Speed(100))).third()))
224+
}
158225
```
159226

160227
Here we're able to return the result of the `fold` and since it's exhaustively evaluated, we're forced to handle all cases! Neat! Let's say we also want to store the `Registration` object into our database when we successfully register a car. We don't really want to have to `fold` over every single case just to handle something for the `Registration`, this is where `select<T>` comes to the rescue!
@@ -163,10 +230,11 @@ Here we're able to return the result of the `fold` and since it's exhaustively e
163230

164231
We're able to take a Coproduct and `select` the type we care about from it. `select` returns an `Option`, if the value of the Coproduct was for the type you're trying to `select`, you'll get `Some`, if it was not the type used with `select`, you'll get `None`.
165232

166-
```kotlin:ank
233+
```kotlin:ank:playground
167234
import arrow.generic.*
168235
import arrow.generic.coproduct3.Coproduct3
169236
import arrow.generic.coproduct3.select
237+
import arrow.generic.coproduct3.first
170238
171239
fun handleApiResult(
172240
database: Database,
@@ -178,16 +246,28 @@ fun handleApiResult(
178246
{ database.insertRegistration(it) }
179247
)
180248
}
249+
250+
fun main() {
251+
println(
252+
ServerError.first<CommonServerError, RegistrationError, Registration>()
253+
.select<RegistrationError>()
254+
)
255+
}
181256
```
182257

183258
`select` can only be called with a type that exists on the Coproduct, if the type doesn't exist, it won't compile:
184-
```kotlin:ank
259+
260+
```kotlin:ank:fail
185261
import arrow.generic.*
186262
import arrow.generic.coproduct3.Coproduct3
187263
import arrow.generic.coproduct3.select
188264
189-
fun handleApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): Unit {
190-
// apiResult.select<String>()
191-
//error: type mismatch: inferred type is Coproduct3<CommonServerError, RegistrationError, Registration> but Coproduct3<String, *, *> was expected
265+
fun main() {
266+
println(
267+
//sampleStart
268+
ServerError.first<CommonServerError, RegistrationError, Registration>()
269+
.select<String>()
270+
//sampleEnd
271+
)
192272
}
193273
```

modules/meta/arrow-meta/src/main/java/arrow/common/utils/ProcessorUtils.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ private val ProtoBuf.ConstructorOrBuilder.isSecondary: Boolean get() = Flags.IS_
104104

105105
fun String.removeBackticks() = replace("`", "")
106106

107+
fun String.toCamelCase(): String =
108+
when {
109+
length <= 1 -> toLowerCase()
110+
111+
else -> first().toLowerCase() + substring(1)
112+
}
113+
107114
fun knownError(message: String, element: Element? = null): Nothing =
108115
throw KnownException(message, element)
109116

0 commit comments

Comments
 (0)