-
Notifications
You must be signed in to change notification settings - Fork 451
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
Add arrow.generic.Coproduct
docs
#998
Changes from 5 commits
d371e35
2e42863
cf53d9c
c9b4067
652cf61
1b7c749
7384f3c
9e40b5b
e70b421
ed00f01
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,167 @@ | ||
--- | ||
layout: docs | ||
title: Generic | ||
permalink: /docs/generic/coproduct/ | ||
--- | ||
|
||
## Arrow Generic | ||
|
||
{:.beginner} | ||
beginner | ||
|
||
`arrow-generic` provides meta programming facilities over Product types like data classes, tuples, and heterogeneous lists; and Coproduct types like sealed classes. | ||
|
||
### Install | ||
|
||
```groovy | ||
compile 'io.arrow-kt:arrow-generic:$arrow_version' | ||
``` | ||
|
||
### Features | ||
|
||
#### Coproduct | ||
|
||
Coproducts represent a container in which only one of the specified set of types exist. It's very similar in `Either` in that respect. `Either` 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, 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. | ||
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 add links to Either docs everytime you mention it. 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. "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 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. "...Car Dealer app that can show |
||
|
||
Let's say we have an api. Our api operates under the following conditions: | ||
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. All this scenarios would work better using very simple code snippets. |
||
- Every endpoint could tell the client, `ServerError`, `UserUnauthorized` or `OverRequestLimit`. | ||
- For specific endpoints, we may have specific results to the context of what they do, for example, an endpoint for registering a `Car` for some service. This endpoint could return to us `CarAlreadyRegistered`, `StolenCar` or `SuccessfullyRegistered`. For fun, let's say the `SuccessfullyRegistered` response also contains a `Registration` object with some data. | ||
|
||
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. | ||
|
||
#### Extensions | ||
|
||
##### coproductOf | ||
|
||
So now that we've got our api response modeled, we need to be able to create an instance of `Coproduct3`. | ||
|
||
```kotlin:ank | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.coproductOf | ||
|
||
val apiResult = coproductOf<CommonServerError, RegistrationError, Registration>(ServerError) //Returns Coproduct3<CommonServerError, RegistrationError, Registration> | ||
``` | ||
|
||
There are `coproductOf` constructor functions for each Coproduct. 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. | ||
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. First line, I'd better say "...for each Coproduct regardless of the arity (number of types)." |
||
|
||
If we pass in a value that doesn't correspond to any types on the Coproduct, it won't compile: | ||
```kolint:ank | ||
//val apiResult = coproductOf<String, RegistrationError, Registration>(ServerError) | ||
``` | ||
|
||
##### cop | ||
|
||
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`'s `left()` and `right()` extension methods, Coproducts can be created with an extension method on any type: | ||
|
||
```kotlin:ank | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.cop | ||
|
||
val apiResult = ServerError.cop<CommonServerError, RegistrationError, Registration>() //Returns Coproduct3<CommonServerError, RegistrationError, Registration> | ||
``` | ||
|
||
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: | ||
|
||
```kotlin:ank | ||
//val apiResult = ServerError.cop<String, RegistrationError, Registration>() | ||
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 write the exception thrown here? |
||
``` | ||
|
||
##### 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: | ||
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. Maybe we can add some note here about exhaustive evaluation? |
||
|
||
```kotlin:ank | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.Coproduct3 | ||
import arrow.generic.coproduct3.fold | ||
|
||
fun handleCommonError(commonError: CommonServerError) { | ||
} | ||
|
||
fun showCarAlreadyRegistered() { | ||
} | ||
|
||
fun callPolice() { | ||
} | ||
|
||
fun showCarSuccessfullyRegistered(car: Car) { | ||
} | ||
|
||
fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>) = apiResult.fold( | ||
{ commonError -> handleCommonError(commonError) }, | ||
{ registrationError -> | ||
when (registrationError) { | ||
CarAlreadyRegistered -> showCarAlreadyRegistered() | ||
StolenCar -> callPolice() | ||
} | ||
}, | ||
{ registration -> | ||
val registeredCar = registration.car | ||
|
||
showCarSuccessfullyRegistered(registeredCar) | ||
} | ||
) | ||
``` | ||
|
||
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 | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.Coproduct3 | ||
import arrow.generic.coproduct3.fold | ||
|
||
fun renderApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): String = apiResult.fold( | ||
{ commonError -> | ||
when (commonError) { | ||
ServerError -> "Server error, try again later." | ||
UserUnauthorized -> "Unauthorized!" | ||
OverRequestLimit -> "Too many api requests, try again later." | ||
} | ||
}, | ||
{ registrationError -> | ||
when (registrationError) { | ||
CarAlreadyRegistered -> "Car already registered." | ||
StolenCar -> "Car reported stolen!" | ||
} | ||
}, | ||
{ registration -> | ||
"Successfully Registered: $registration.car" | ||
} | ||
) | ||
``` | ||
|
||
Here we're able to return the result of the `fold` and we're forced to handle all cases! Neat! | ||
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. Ah! There it is. Maybe just explicitly mention exhaustive evaluation here. You're saying it but still mentioning it by its real name should help. |
||
|
||
##### select | ||
|
||
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! 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`. | ||
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. Maybe we could move all the first part until "...select comes to the rescue!" to the previous section. Then you present select as a new section, which would start as "We're able to take a...". That helps to start introducing the concept by the need for it before even giving it a name. It's just a more gradual way to put it. |
||
|
||
```kotlin:ank | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.Coproduct3 | ||
import arrow.generic.coproduct3.select | ||
|
||
fun handleApiResult( | ||
database: Database, | ||
apiResult: Coproduct3<CommonServerError, RegistrationError, Registration> | ||
): Unit { | ||
apiResult.select<Registration>() | ||
.fold( | ||
{}, //Wasn't Registration, nothing to do here | ||
{ database.insertRegistration(it) } | ||
) | ||
} | ||
``` | ||
|
||
`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 | ||
import arrow.generic.* | ||
import arrow.generic.coproduct3.Coproduct3 | ||
import arrow.generic.coproduct3.select | ||
|
||
fun handleApiResult(apiResult: Coproduct3<CommonServerError, RegistrationError, Registration>): Unit { | ||
//apiResult.select<String>() Doesn't compile | ||
} | ||
``` |
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.
What about "
arrow-generic
provides meta programming facilities over Product types (data classes, tuples, heterogeneous lists...). It also provides the Coproduct type, similar to sealed classes.(Even if by implementation we're providing N Coproduct types, semantically all of them are the same thing, the Coproduct, I don't think there's need here to state it's a bunch of them, at least not yet on this paragraph)