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
-
- 本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?
-
-
Save gakuzzzz/6acd31a9c7d1756ae6c2745442720a79 to your computer and use it in GitHub Desktop.
ApplicativeBuilder的な ComposingN があるので FunctionN に curried を持たせなくても良さそうですね(実装は泥臭くなりますがオブジェクト生成数を抑制できます)
ふむふむ。オブジェクト生成数は確かに気になっていました。まずはシンプルに実装してみました。
完全に余談ですが、カリー化できる FunctionN を別途定義するより、curried を static メソッドにすると curried(Foo::bar) みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions 的な1クラスのoverloadですませられますし)
試してみます。
Validation<E, > の apply が Validation<List, > を返すのがびっくりしますね。Applicative の挙動を期待するなら型としては Validation<E, > になるので。
実装としてはvavrを参考にしたので、そういうものかと思っていました。
https://github.com/vavr-io/vavr/blob/master/src/main/java/io/vavr/control/Validation.java#L732-L753
apply
がValidation<E, T>
の場合、どこでListにするのでしょうか?
せっかく Applicative な結果ができたので sequence / traverse が欲しくなりますね。
用途がいまいちわからなかったので実装しませんでしたが、良さそうなので実装したいと思います。
List に特化するよりは Collector化してStreamにできる全コンテナに対応してもいいかもです。参考 https://gist.github.com/gakuzzzz/0c779d5335f4b2bff596#traverse-%E3%81%A8-sequence
試してみます。
本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?
このPRに関してはvavrのコードは確かにそのようになっていたのですが、まずは概念を理解するためにもシンプルに実装したかったので全部取っ払いましたが、全般的に見直す余地はあると思っています。
というかジェネリクスよくわかっていないのでPRが欲しい...
ところでap
はapply
の略で合っていますか?なぜ略すのでしょうか?
@making ご回答ありがとうございます!
yesです。validateToApplicative, validatoToAp、validateToValidationなど検討しましたが、どれもしっくりこなかったです。
元々validateToEitherも気に入っていなくて、validateの返り値をBreaking Changeとして変えたいと思っているくらいでした。やっぱりvalidateメソッド名がよくて、違和感がなければEitherの方もvalidateToEitherをdeplicatedにしてvalidator.either().validate(...)にしようかなとも思っています。
なるほどなるほど。確かにそれならEitherの方もvalidator.either().validate(...)
に統一するほうがしっくり来そうですね。
実装としてはvavrを参考にしたので、そういうものかと思っていました。
https://github.com/vavr-io/vavr/blob/master/src/main/java/io/vavr/control/Validation.java#L732-L753
applyがValidation<E, T>の場合、どこでListにするのでしょうか?
おーなるほど。これは vavr が思い切ってる感じですねー。
これだと Applicative則を満たせなくなってしまうんですよね。
Haskell や Scala でよく見る例では、 E が勝手にどこかで List になったりはしない感じです。
では E の集約をどうやって実現しているかというと、 E に対して加算が可能(Semigroup)という制約を課している感じです。
なので E 足す E という演算が可能になって、新たな E が算出されるという感じですね。
List 足す List の場合は単純に List を繋げる演算として定義されているので、 Validation<List<E>, >
だと List が連結される、という作りになっています。
Java の場合はこういった Semigroup の制約を掛けるという事が難しいので vavr は割り切った感じなんですかねー?
個人的には YAVI のユースケースでは、エラーをListとして集めるケースが全部だと思うので、汎用的な Validation を作るよりは上記で提示したようなFailureをListに限定した ValidationL<E, >
にしちゃう方が使いやすいのかな、と思います。
あるいはもうfailureの型も抽象化せずに、 ConstraintViolations
に固定してしまうというのも手かもしれません。これであれば Either<ConstraintViolations, >
との変換も楽になりますしね。代わりに mapError
ができなくなりますが。
このPRに関してはvavrのコードは確かにそのようになっていたのですが、まずは概念を理解するためにもシンプルに実装したかったので全部取っ払いましたが、全般的に見直す余地はあると思っています。
というかジェネリクスよくわかっていないのでPRが欲しい...
なるほどなるほど、たしかに概念を理解するにはノイズになりすぎますよね……。わかります。
PRは余力があれば週末とかに頑張ってみます……
ところで
ap
はapply
の略で合っていますか?なぜ略すのでしょうか?
合ってます! がなぜそう略すのかは僕も正確な理由を把握できてないですね。 Scala の場合は apply
が特別扱いされるので区別したかったみたいな側面はあるかもしれませんが……。
Haskell の場合元々 <*>
という記号ですしね。演算子的に扱うために明示的な名前よりも記号的な ap の方が他の識別子が目立つからみたいな推測をしていますが、本当の所はわかりません。
また Validations.compose
や Validator#composable
ですが、 compose
という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap
による合成も合成には違いないので。
こういった横方向の合成には積の意味で product
とかが使われる事がよくあります。https://github.com/typelevel/cats/blob/273257054dff5675aa763cfa09481e1f86709b45/core/src/main/scala/cats/data/Validated.scala#L1047
なのでこの辺の知識がある人にとっては Validator#productive
とかの方が合成方向が明示的になってわかりやすいかもしれません。ただ product
で製品をイメージしてしまう層にとっては逆にわかりづらくなってしまうかもですね。
また Validations.compose や Validator#composable ですが、 compose という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap による合成も合成には違いないので。
なるほど。validator.applicative().validate(...)
とvalidator.composable().validate(...)
だとどちらが自然ですかね?
あるいはvalidator.validation().validate(...)
。わかりづらいか...
その2択であれば個人的には applicative()
の方がわかりやすい気がしますが、あくまで Applicative や Monad に慣れてる側の人間の感覚なので悩ましいですね。
validator.either().validate(...)
との対比も考えると collective()
とか errorCollective()
みたいな方が明示的だったりしますかね?
では E の集約をどうやって実現しているかというと、 E に対して加算が可能(Semigroup)という制約を課している感じです。
個人的には YAVI のユースケースでは、エラーをListとして集めるケースが全部だと思うので、汎用的な Validation を作るよりは上記で提示したようなFailureをListに限定した ValidationL<E, > にしちゃう方が使いやすいのかな、と思います。
なるほど。確かにSemigroup
を導入するのは大袈裟すぎるのでList前提が良さそうですね。
その2択であれば個人的には applicative() の方がわかりやすい気がしますが、あくまで Applicative や Monad に慣れてる側の人間の感覚なので悩ましいですね。
いったんapplicative
で行こうと思います。ありがとうございます。
👍
なるほど。確かにSemigroupを導入するのは大袈裟すぎるのでList前提が良さそうですね。
ああ、mapError
使えないのか。悩ましい。
List<ConstraintViolation>
をConstraintViolations
に変換できると嬉しいですが、toEither
を介すしかないかな...
ああ確かに List に固定化した場合も mapError
が微妙な感じになりますね……。
ConstraintViolations に固定化してしまうというのは有りかもしれません。
いったん、List<E> error()
に変更しました。
making/yavi@35f8e9b
今までList<List<String>>
とかList<CostraintViolations>
みたいなネストなListが出て気持ち悪いなぁと思っていたところがすっきりしました。この変更がなければtraverse
も変な返り値になっていました。
ありがとうございます!
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 に寄せるの賛成 👍
yesです。
validateToApplicative
,validatoToAp
、validateToValidation
など検討しましたが、どれもしっくりこなかったです。元々
validateToEither
も気に入っていなくて、validate
の返り値をBreaking Changeとして変えたいと思っているくらいでした。やっぱり
validate
メソッド名がよくて、違和感がなければEitherの方もvalidateToEither
をdeplicatedにしてvalidator.either().validate(...)
にしようかなとも思っています。