Skip to content

Update Coproducts #1291

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

Merged
merged 20 commits into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41281ae
Make data classes public, deprecate old extension constructor and add…
abergfeld Feb 6, 2019
f14f102
Update docs
abergfeld Feb 6, 2019
f8c5390
Merge branch 'master' into update-coproducts
abergfeld Feb 7, 2019
06cab30
Use camelcase class names for function names
abergfeld Feb 7, 2019
43a8ec2
Deprecate coproductOf as well
abergfeld Feb 7, 2019
60455d2
First pass at runnable docs
abergfeld Feb 7, 2019
e827f0b
First pass at kdocs
abergfeld Feb 7, 2019
7776549
Merge remote-tracking branch 'origin/update-coproducts' into update-c…
abergfeld Feb 7, 2019
e42fb0b
Merge branch 'master' into update-coproducts
JorgeCastilloPrz Feb 7, 2019
119ef92
Add test for type inference with ADT
abergfeld Feb 8, 2019
a7102b8
Add example of how Coproducts can flatten hierarchies
abergfeld Feb 8, 2019
4a804a6
Move Constructors to top level section since it's no longer an extens…
abergfeld Feb 8, 2019
23c2d72
Update to use @param and @return for kdocs so the docs get generated …
abergfeld Feb 8, 2019
4a0de81
Merge remote-tracking branch 'origin/update-coproducts' into update-c…
abergfeld Feb 8, 2019
8d2b556
Merge branch 'master' into update-coproducts
raulraja Feb 10, 2019
4b62735
Add String Util method and update docs
abergfeld Feb 12, 2019
b717c2e
Merge remote-tracking branch 'origin/update-coproducts' into update-c…
abergfeld Feb 12, 2019
14940aa
Merge branch 'master' into update-coproducts
abergfeld Feb 12, 2019
4fae979
Merge branch 'master' into update-coproducts
JorgeCastilloPrz Feb 12, 2019
e980dd0
Merge branch 'master' into update-coproducts
raulraja Feb 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
package arrow.generic

import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.some
import arrow.core.*
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.generic.coproduct3.*
import arrow.test.UnitSpec
import io.kotlintest.shouldBe
import io.kotlintest.runner.junit4.KotlinTestRunner
import io.kotlintest.shouldBe
import org.junit.runner.RunWith

@RunWith(KotlinTestRunner::class)
Expand All @@ -24,8 +19,13 @@ class CoproductTest : UnitSpec() {
init {

"Coproducts should be generated up to 22" {
class Proof2(f: Coproduct2<Unit, Unit>) { val x = f }
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 }
class Proof2(f: Coproduct2<Unit, Unit>) {
val x = f
}

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
}
}

"select should return None if value isn't correct type" {
Expand Down Expand Up @@ -82,5 +82,49 @@ class CoproductTest : UnitSpec() {
{ "Third" }
) shouldBe "Third"
}

"cop replacement functions are equivalent" {
val firstValue = "String"
val secondValue = 100L
val thirdValue = none<String>()

firstValue.cop<String, Long, Option<String>>() shouldBe firstValue.first<String, Long, Option<String>>()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't we remove cop ext funs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I think the intent is to deprecate them and remove them in a later version? #1284 (comment)

I'm not sure what the next Arrow version will be I guess, I may have misunderstood

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is correct @abergfeld they will be deprecated in 0.9.0 coming out now and will be removed sometime before 1.0 final.

secondValue.cop<String, Long, Option<String>>() shouldBe secondValue.second<String, Long, Option<String>>()
thirdValue.cop<String, Long, Option<String>>() shouldBe thirdValue.third<String, Long, Option<String>>()
}

"types can be inferred with the extension constructors" {
fun typeInferenceTest(input: String?): Coproduct3<String, Int, Option<String>> {
return when {
input == null -> none<String>().third()
input.length > 100 -> input.length.second()
else -> input.first()
}
}

typeInferenceTest(null) shouldBe Third<String, Int, Option<String>>(none())
}

"types can be inferred with the ADT constructors" {
fun typeInferenceTest(input: String?): Coproduct3<String, Int, Option<String>> {
return when {
input == null -> Third(none())
input.length > 100 -> Second(input.length)
else -> First(input)
}
}

typeInferenceTest(null) shouldBe none<String>().third<String, Int, Option<String>>()
}

"Coproduct data classes should be available" {
val firstValue = "String"
val secondValue = 100L
val thirdValue = none<String>()

First<String, Long, Option<String>>(firstValue) shouldBe firstValue.first<String, Long, Option<String>>()
Second<String, Long, Option<String>>(secondValue) shouldBe secondValue.second<String, Long, Option<String>>()
Third<String, Long, Option<String>>(thirdValue) shouldBe thirdValue.third<String, Long, Option<String>>()
}
}
}
148 changes: 114 additions & 34 deletions modules/docs/arrow-docs/docs/docs/generic/coproduct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ compile 'io.arrow-kt:arrow-generic:$arrow_version'

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.

```kotlin:ank
```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.Coproduct3
import arrow.generic.coproduct3.fold
import arrow.generic.coproduct3.*

fun toDisplayValues(items: List<Coproduct3<Car, Dealership, Salesperson>>): List<String> {
return items.map {
Expand All @@ -37,6 +36,18 @@ fun toDisplayValues(items: List<Coproduct3<Car, Dealership, Salesperson>>): List
)
}
}

fun main() {
println(
toDisplayValues(
listOf<Coproduct3<Car, Dealership, Salesperson>>(
Car(Speed(100)).first(),
Dealership("Cedar Falls, Iowa").second(),
Salesperson("Car McCarface").third()
)
)
)
}
```

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

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.

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.
The most obvious approach would be to use Kotlin's `sealed class` to create a return type for this api call:

#### Extensions
```kotlin:ank
sealed class ApiResult {
data class CommonServerError(val value: CommonServerError): ApiResult()
data class RegistrationError(val value: RegistrationError): ApiResult()
data class Registration(val value: Registration): ApiResult()
}
```

##### coproductOf
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.

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

```kotlin:ank
fun handleResult(apiResult: ApiResult): String {
return when (apiResult) {
is ApiResult.CommonServerError -> "Common: ${apiResult.value}"
is ApiResult.RegistrationError -> "RegistrationError: ${apiResult.value}"
is ApiResult.Registration -> "Registration: ${apiResult.value}"
}
}
```

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.

#### Constructors

So now that we've got our api response modeled, we need to be able to create an instance of `Coproduct3`.

```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.coproductOf
import arrow.generic.coproduct3.First


val apiResult = coproductOf<CommonServerError, RegistrationError, Registration>(ServerError) //Returns Coproduct3<CommonServerError, RegistrationError, Registration>
fun main() {
println(
//sampleStart
First<CommonServerError, RegistrationError, Registration>(ServerError)
//sampleEnd
)
}
```

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.
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.

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

```kotlin:ank
```kotlin:ank:fail
import arrow.generic.*
import arrow.generic.coproduct3.coproductOf
import arrow.generic.coproduct3.First

//val apiResult = coproductOf<String, RegistrationError, Registration>(ServerError)
//error: type mismatch: inferred type is ServerError but String was expected
fun main() {
println(
//sampleStart
First<String, RegistrationError, Registration>(ServerError)
//sampleEnd
)
}
```

##### cop
#### Extensions

##### constructors

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:

```kotlin:ank
```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.cop
import arrow.generic.coproduct3.*

val apiResult = ServerError.cop<CommonServerError, RegistrationError, Registration>() //Returns Coproduct3<CommonServerError, RegistrationError, Registration>
fun main() {
//sampleStart
println(ServerError.first<CommonServerError, RegistrationError, Registration>())
println(CarAlreadyRegistered.second<CommonServerError, RegistrationError, Registration>())
//sampleEnd
}
```

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:
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:

```kotlin:ank
```kotlin:ank:fail
import arrow.generic.*
import arrow.generic.coproduct3.cop
import arrow.generic.coproduct3.first

//val apiResult = ServerError.cop<String, RegistrationError, Registration>()
//error: type mismatch: inferred type is ServerError but String was expected
fun main() {
println(
//sampleStart
"String".first<CommonServerError, RegistrationError, Registration>()
//sampleEnd
)
}
```

##### fold

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:

```kotlin:ank
```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.Coproduct3
import arrow.generic.coproduct3.fold
import arrow.generic.coproduct3.*

fun handleCommonError(commonError: CommonServerError) {
println("Encountered a common error $commonError")
}

fun showCarAlreadyRegistered() {
println("Car is already registered")
}

fun callPolice() {
println("That car is stolen!!!!1!")
}

fun showCarSuccessfullyRegistered(car: Car) {
println("Successfully Registered!")
}

fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>) = apiResult.fold(
Expand All @@ -128,14 +188,17 @@ fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError,
showCarSuccessfullyRegistered(registeredCar)
}
)

fun main() {
renderApiResult(Registration(Car(Speed(100))).third())
}
```

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):

```kotlin:ank
```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.Coproduct3
import arrow.generic.coproduct3.fold
import arrow.generic.coproduct3.*

fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): String = apiResult.fold(
{ commonError ->
Expand All @@ -155,6 +218,10 @@ fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError,
"Successfully Registered: $registration.car"
}
)

fun main() {
println(renderApiResult(Registration(Car(Speed(100))).third()))
}
```

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!
Expand All @@ -163,10 +230,11 @@ Here we're able to return the result of the `fold` and since it's exhaustively e

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`.

```kotlin:ank
```kotlin:ank:playground
import arrow.generic.*
import arrow.generic.coproduct3.Coproduct3
import arrow.generic.coproduct3.select
import arrow.generic.coproduct3.first

fun handleApiResult(
database: Database,
Expand All @@ -178,16 +246,28 @@ fun handleApiResult(
{ database.insertRegistration(it) }
)
}

fun main() {
println(
ServerError.first<CommonServerError, RegistrationError, Registration>()
.select<RegistrationError>()
)
}
```

`select` can only be called with a type that exists on the Coproduct, if the type doesn't exist, it won't compile:
```kotlin:ank

```kotlin:ank:fail
import arrow.generic.*
import arrow.generic.coproduct3.Coproduct3
import arrow.generic.coproduct3.select

fun handleApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): Unit {
// apiResult.select<String>()
//error: type mismatch: inferred type is Coproduct3<CommonServerError, RegistrationError, Registration> but Coproduct3<String, *, *> was expected
fun main() {
println(
//sampleStart
ServerError.first<CommonServerError, RegistrationError, Registration>()
.select<String>()
//sampleEnd
)
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ private val ProtoBuf.ConstructorOrBuilder.isSecondary: Boolean get() = Flags.IS_

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

fun String.toCamelCase(): String =
when {
length <= 1 -> toLowerCase()

else -> first().toLowerCase() + substring(1)
}

fun knownError(message: String, element: Element? = null): Nothing =
throw KnownException(message, element)

Expand Down
Loading