-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Validated beginners doc #1903
Validated beginners doc #1903
Conversation
This commit is the first approach to address typelevel#1678, developing the form-validation example.
This commit contains a simple form-validation example and a couple of approaches making use of `Validated`. It aims to solve typelevel#1678
Codecov Report
@@ Coverage Diff @@
## master #1903 +/- ##
==========================================
+ Coverage 95.2% 95.54% +0.33%
==========================================
Files 248 248
Lines 4379 4420 +41
Branches 118 124 +6
==========================================
+ Hits 4169 4223 +54
+ Misses 210 197 -13
Continue to review full report at Codecov.
|
Hi @AlejandroME, this looks really good and thank you for your contribution! |
@LukaJCB Thank you for your feedback! :) |
- The code examples for form validation are now using `tut` with its respective output in the generated .html. - Fixes some structural issues (`Either` return type in the `Validated` example, use of `mapN` instead of `|@|`). - Adds a deprecation notice about cartesian builder and changes this for using `.mapN`.
@LukaJCB It's done! :) I have a couple of points to state:
Thank you! |
private def validateAge(age: Int): ValidationResult[Int] = | ||
if (age >= 18 && age <= 75) age.validNel else AgeIsInvalid.invalidNel | ||
|
||
def validateForm(username: String, password: String, firstName: String, lastName: String, age: Int): ValidationResult[RegistrationData] = { |
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.
Can you perhaps put all the validateN
functions into another snippet that's checked by tut, and then only have the validateForm
go without tut
? I don't think you need the surrounding trait btw :)
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.
Meant to put this on the snippet above 😅
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.
Got it! Working on it and also on the support of <2.11.x Either
for-comprehensions (Travis complained about flatMap
on this versions).
Thank you for your patience with this! :)
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.
Just import cats.syntax.either._
to get that working :)
Thank you, for you help with the docs!
- The `Validated` approach with the for-comprehension has been splitted in two: the instructions that compile and the for-comprehension that doesn't compile. For the first ones, I've used `tut:silent` and for the second one I've used `tut:book:fail`. With this, all the code of this proposal is checked by tut correctly. - Adds an import for Either to bring .flatMap in Scala versions prior to 2.12.x.
@LukaJCB I've fixed the checking with I'm waiting for the Travis build outcome and further suggestions :) |
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.
LGTM to me overall, with a couple of minor issues :)
|
||
```scala | ||
sealed abstract class Validated[+E, +A] extends Product with Serializable { | ||
// Implementation elided |
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.
Tiny nitpick, but can we reduce the indentation here?
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.
Is there a standard used by cats? Spaces, tabs? I'm asking this because all the code snippets here are using tabs, so, if there's a defined standard I can fix the indentation everywhere :)
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.
two spaces is standard :)
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.
Got it! I'll fix it in all the code snippets.
type ValidationResult[A] = Validated[NonEmptyList[DomainValidation], A] | ||
|
||
private def validateUserName(userName: String): ValidationResult[String] = | ||
if (userName.matches("^[a-zA-Z0-9]+$")) userName.validNel else UsernameHasSpecialCharacters.invalidNel |
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.
Maybe we could give a very quick heads-up on the use of .validNel
and .invalidNel
? :)
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.
Nevermind, I see you're doing it afterwards 😄
|
||
### Meeting applicative | ||
|
||
We have to look into another direction: a for-comprehension plays well in a fail-fast scenario, but the structure in our previous example was designed to catch one error at a time, so, our next step is to tweak the implementation a bit. |
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.
Maybe we should mention somewhere here, that in order to get the Applicative
instance for Validated
the left side needs to have a Semigroup
instance :)
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.
Working on it! :)
To do this, you'll need to use either `.toValidated` or `.toValidatedNel`. Let's see an example: | ||
|
||
```tut:book | ||
FormValidator.validateForm( |
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'm not sure how good this example really is, the fail-fast behaviour of Either
is already done at this point, so converting to Validated does not somehow enable the Error accumulation we might want, as you probably know. :)
Maybe we could include an example that doesn't drop the error accumulation?
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 was thinking the same. I've done this little example for making use of .invalid
/.invalidNel
but nothing, apart from this snippet came to my mind. I'll think about this and I'll reach you out again :)
- Fixes indentation in all the snippets (two spaces). - Added a new section ("short detour") in where I explain the `Semigroup` role in the accumulation, with a little portion of `Validated` code. - Adds "Back and forth" section explaining how to convert between `Validated` and `Either`. I can't figure it out how to convert from `Either` to `Validated` 'failing slowly'.
@LukaJCB Some notes here:
|
About your 3rd point, why don't you just convert some of your |
|
||
Typically, you'll see that `Validated` will be accompanied by a `NonEmptyList` when it comes to accumulation. The thing here is that you can define your own accumulative data structure and you're not limited to the aforementioned construction. | ||
|
||
For doing this, you have to provide a `Semigroup` instance. `NonEmptyList`, by definition has its own `Semigroup`. For those who don't know what a `Semigroup` is, let's see a simple example. |
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.
This is really great, thank you! However, you didn't really include an example?
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 was more about how ap
works for Invalid
, but I can also provide a simple example of Semigroup
. What do you think about giving the example first and then point to the actual behavior, as it is written now?
|
||
#### Accumulative Structures | ||
|
||
According to [Wikipedia](https://en.wikipedia.org/wiki/Semigroup): |
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.
Instead of linking to wikipedia, I think the link to cats documentation should be enough :)
Maybe we could just say " For those who don't know what a Semigroup
is, you can find out more here."
What do you think? :)
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.
Sounds good! I'll fix it.
@LukaJCB Yes! Sorry, I didn't figure it out! I'll work on this :) |
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.
Thanks already @AlejandroME !
I left a few comments.
|
||
sealed trait FormValidator{ | ||
private def validateUserName(userName: String): Either[DomainValidation, String] = | ||
if (userName.matches("^[a-zA-Z0-9]+$")) Right(userName) else Left(UsernameHasSpecialCharacters) |
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.
You could use Either.cond
instead of if (...) Right(...) else Left(...)
.
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.
This is new for me! I'll change it in all the conditions :)
|
||
def validateUserName(userName: String): Validated[DomainValidation, String] = { | ||
if (userName.matches("^[a-zA-Z0-9]+$")) Valid(userName) else Invalid(UsernameHasSpecialCharacters) | ||
} |
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.
Maybe reuse the Either
methods here and call toValidated
?
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.
Good suggestion. I'll take a look at it!
```tut:silent | ||
import cats.data._ | ||
import cats.data.Validated._ | ||
import cats.implicits._ |
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.
You don't need to import these again.
|
||
sealed trait FormValidatorNel { | ||
|
||
type ValidationResult[A] = Validated[NonEmptyList[DomainValidation], A] |
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.
Since you are using validNel
and invalidNel
below, I think you can use ValidatedNel
here as well.
} | ||
``` | ||
|
||
We've omitted the complete implementation because our focus here is the case in where you need to append (that's the function of this method) two failures. Note the `implicit EE: Semigroup[EE]` parameter and the usage of its `.combine` operation. In the case of `NonEmptyList`, we're talking about a `List`, with certain properties that allow us to _combine_ (append) more than one element to it. That's because, apart from the fact that it is a `List`, it also has an instance of `Semigroup`, telling it how to operate with the accumulation. |
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 find this section a bit confusing.
Maybe something like :
We've omitted the complete implementation and only show the part where two failures are combined using the
combine
method of theSemigroup
instance ofEE
.
I don't think we need the part about NonEmptyList
. Maybe you can add an example of NonEmptyList
's Semigroup
to the Accumulative Structures section ?
Something like :
NonEmptyList.one("error 1") |+| NonEmptyList("error 2", "error 3")
"error 1".invalidNel[Int] |+| "error 2".invalidNel
("error 1".invalidNel[Int], "error 2".invalidNel[Int]).mapN(_ + _)
### Going back and forth | ||
|
||
cats offer you a nice set of combinators to transform your `Validated` based approach to an `Either` one and vice-versa. | ||
Please note that, if you're using an `Either`-based approach as seen in our first example and you choose to convert it to a `Validated` one, you're constrained to the fail-fast nature of `Either`, but you're gaining a broader set of features with `Validated`. |
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 do you mean with
you're gaining a broader set of features with
Validated
.
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.
This is the section in where I have doubts about it. I was trying to express that, coming from Either
to Validated
, the person will gain more combinators (Features), even if you transfer the Either
behavior (fail-fast) to Validated
.
With your previous comments, I have an idea for simplifying this section (or even delete it). Let me work on it and I'll reach you out again :)
Our data will be represented this way: | ||
|
||
```tut:silent | ||
case class RegistrationData(username: String, password: String, firstName: String, lastName: String, age: Int) |
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.
Maybe add final
here. Probably good to show best practices in the documentation?
- Added `final` modifier to `RegistrationData` case class. - Changed if-else in `Either` example to `Either.cond`. - Reused `Either` example validations in `Validated` (non-compiling) approach with `.toValidated`. - Deleted redundant imports for tut. - Changed `Validated` of a `NonEmptyList` for `ValidatedNel` type alias. - Deleted 'disclaimer' about converting from `Validated` to `Either`, losing the fail-slow functionality. - Added `Semigroup` example based on the Peter's one but in the context of the example provided here. - Fixed some typos and grammar errors. - Pointed out to the cats `Semigroup` documentation instead of Wikipedia.
Hello, @LukaJCB and @peterneyens! I've worked on your suggestions. Some notes:
I'll stay tuned for your comments about this. Thanks! |
Looks like all comments are addressed. LGTM thanks so much @AlejandroME !! |
Thank you so much for all the feedback received! I'm very happy to contribute to the project! 👍 |
This is an approach to address #1678.
Being my first PR, I pushed to my fork the first draft of this a week ago and @zainab-ali gladly helped me with a revision.
This PR contains fixes to the suggestions made in the revision, grammar fixes and the addition of
toValidated
/toValidatedNel
combinators in the example.I'm aware of the discussion made in the issue about the detail level of this doc. I tried to document this as clearly as possible without delving into too much detail.