Validatorに (validateToEitherと同様な)validateToApみたいなのを作らずApplicativeValidatorを別に用意したのは何故なんでしょう?validateToValidationみたいな謎な名前になってしまうから?
- ApplicativeBuilder的な
ComposingNがあるのでFunctionNにcurriedを持たせなくても良さそうですね(実装は泥臭くなりますがオブジェクト生成数を抑制できます)- 逆にカリー化できる関数があると
ComposingNのようなものを不要にできたりもします。 - とは言えYAVIのユースケースだと
Validatorからメソッドチェーンで使うのが主だと思うので、ComposingNの方が有用そうですね。 - 完全に余談ですが、カリー化できる
FunctionNを別途定義するより、curriedを static メソッドにするとcurried(Foo::bar)みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions的な1クラスのoverloadですませられますし)
- 逆にカリー化できる関数があると
Validation<E, >のapplyがValidation<List<E>, >を返すのがびっくりしますね。Applicative の挙動を期待するなら型としてはValidation<E, >になるので。- YAVI の利用範囲から考えると、エラーの集約方法を汎用化する必要もなさそうなので、error は最初から
Listに決め打ちしちゃってもいいかもしれません。以下のようなイメージです。 -
public interface Validation<E, T> extends Serializable { boolean isValid(); T value(); List<E> error(); ...
- 名前があれであれば
ValidationNelとかValidationLとか? - 元のコードで
Validation<ConstraintViolations, >だったものもValidationL<ConstraintViolation, >にするイメージですね。
- YAVI の利用範囲から考えると、エラーの集約方法を汎用化する必要もなさそうなので、error は最初から
- せっかく Applicative な結果ができたので
sequence/traverseが欲しくなりますね。-
Validator<Email> emailValidator = ValidatorBuilder.of(Email.class) .constraint(...) .build() .prefixed("email"); List<Email> mails = List.of(...); ValidationL<ConstraintViolation, List<Email>> validated = ValidationL.traverse(mails, emailValidator.applicative()::validate);
- List に特化するよりは Collector化してStreamにできる全コンテナに対応してもいいかもです。参考 https://gist.github.com/gakuzzzz/0c779d5335f4b2bff596#traverse-%E3%81%A8-sequence
-
- 本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?
ApplicativeValidator.validate()の返り値を次のような特化型Validationにして、ConstraintViolationsを返すようにしてみます。
package am.ik.yavi.core;
import am.ik.yavi.fn.Validation;
/**
* A specialized {@code Validation} class that regards {@code ConstraintViolation} as failure type
* @param <T> value type in the case of success
* @since 0.6.0
*/
public class Validated<T> implements Validation<ConstraintViolation, T> {
private final Validation<ConstraintViolation, T> delegate;
public static <T> Validated<T> of(Validation<ConstraintViolation, T> delegate) {
return new Validated<>(delegate);
}
Validated(Validation<ConstraintViolation, T> delegate) {
this.delegate = delegate;
}
@Override
public boolean isValid() {
return delegate.isValid();
}
@Override
public T value() {
return delegate.value();
}
@Override
public ConstraintViolations error() {
return new ConstraintViolations(delegate.error());
}
}ここで
本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?
これの対応が必要になりそう。
いったん、List error()に変更しました。
making/yavi@35f8e9b
素敵!!
ApplicativeValidator.validate()の返り値を次のような特化型Validationにして、ConstraintViolationsを返すようにしてみます。
あーなるほど、アリですね!
この範囲であれば上限境界や下限境界そこまで気にしなくて大丈夫そうな気がします。
その辺の境界を入れたほうが良さそうなやつは Validation#map とか Validations.apply とか Validations.compose とかその辺とかなので。
この範囲であれば上限境界や下限境界そこまで気にしなくて大丈夫そうな気がします。
これ入れないとvalidateしたあとcomposeするとValidationに戻っちゃいました。
その辺の境界を入れたほうが良さそうなやつは Validation#map とか Validations.apply とか Validations.compose とかその辺とかなので。
あ、その辺のことでした!
あ、ですね! compose とかは確かに要りますね!
ApplicativeValidator.validate()の返り値を次のような特化型Validationにして、ConstraintViolationsを返すようにしてみます。package am.ik.yavi.core; import am.ik.yavi.fn.Validation; /** * A specialized {@code Validation} class that regards {@code ConstraintViolation} as failure type * @param <T> value type in the case of success * @since 0.6.0 */ public class Validated<T> implements Validation<ConstraintViolation, T> { private final Validation<ConstraintViolation, T> delegate; public static <T> Validated<T> of(Validation<ConstraintViolation, T> delegate) { return new Validated<>(delegate); } Validated(Validation<ConstraintViolation, T> delegate) { this.delegate = delegate; } @Override public boolean isValid() { return delegate.isValid(); } @Override public T value() { return delegate.value(); } @Override public ConstraintViolations error() { return new ConstraintViolations(delegate.error()); } }
自分の知識だとfoldやmapErrorをConstraintViolationsに特化できなかったので、いったんValidation<ContraintViolation, T>に戻します...
こういうオーバーライドがしたかったが、できなかった。
@Override
public <U> U fold(Function<ConstraintViolations, U> errorsMapper, Function<T, U> mapper) {
// ...
}
@Override
public <E2> Validation<E2, T> mapErrors(Function<ConstraintViolations, List<E2>> errorsMapper) {
// ...
}ValidationL<ConstraintViolation, List> validated = ValidationL.traverse(mails, emailValidator.applicative()::validate);
traverseでConstraintViolationを集約した場合、ConstraintViolationにmailsのindexをfieldNameに含めたくなりますね。indexを受けるlamdaも渡さないと実用的ではない気がする...
完全に余談ですが、カリー化できる FunctionN を別途定義するより、curried を static メソッドにすると curried(Foo::bar) みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions 的な1クラスのoverloadですませられますし)
試してみます。
making/yavi@8f85fcd
staticメソッドにしました。
Validationクラスに関しては上限・下限を設定。
making/yavi@7d07337
いったんマージしました。アドバイスありがとうございました。
もう少しブラッシュアップしてリリースしたいと思います。
https://github.com/functionaljava/functionaljava/blob/series/5.x/core/src/main/java/fj/data/Validation.java
を見ながらcomposeをaccumulateにしたほうがいいのかなとか思っています。
おっと! Scala3リリースとかでバタバタしてて気付いてませんでした! 週末拝見させて頂きます!
また Validations.compose や Validator#composable ですが、 compose という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap による合成も合成には違いないので。
このコメントを受けて Validation.compose メソッド及び ComposingN も別の名前にした方がいいかなと思っているのですが、
使用例から accumulate と combine があるのですが、どれが自然でしょうか?
and でもいいか
vavrに寄せてcombineに変更してみます。
accumulate, and, combine だと combine が個人的にはいいかなと思います。 vavr に寄せるの賛成 👍
いったん、
List<E> error()に変更しました。making/yavi@35f8e9b
今まで
List<List<String>>とかList<CostraintViolations>みたいなネストなListが出て気持ち悪いなぁと思っていたところがすっきりしました。この変更がなければtraverseも変な返り値になっていました。ありがとうございます!