Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active May 20, 2021 10:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gakuzzzz/6acd31a9c7d1756ae6c2745442720a79 to your computer and use it in GitHub Desktop.
Save gakuzzzz/6acd31a9c7d1756ae6c2745442720a79 to your computer and use it in GitHub Desktop.
YAVI に Applicative な Validation 入れるよの話

making/yavi#121

  • Validator に (validateToEither と同様な) validateToAp みたいなのを作らず ApplicativeValidator を別に用意したのは何故なんでしょう?
    • validateToValidation みたいな謎な名前になってしまうから?
  • ApplicativeBuilder的な ComposingN があるので FunctionNcurried を持たせなくても良さそうですね(実装は泥臭くなりますがオブジェクト生成数を抑制できます)
    • 逆にカリー化できる関数があると ComposingN のようなものを不要にできたりもします。
    • とは言えYAVIのユースケースだと Validator からメソッドチェーンで使うのが主だと思うので、ComposingN の方が有用そうですね。
    • 完全に余談ですが、カリー化できる FunctionN を別途定義するより、curried を static メソッドにすると curried(Foo::bar) みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions 的な1クラスのoverloadですませられますし)
  • Validation<E, >applyValidation<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, > にするイメージですね。
  • せっかく 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
  • 本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?
@making
Copy link

making commented May 13, 2021

Validator に (validateToEither と同様な) validateToAp みたいなのを作らず ApplicativeValidator を別に用意したのは何故なんでしょう?
validateToValidation みたいな謎な名前になってしまうから?

yesです。validateToApplicative, validatoToApvalidateToValidationなど検討しましたが、どれもしっくりこなかったです。
元々validateToEitherも気に入っていなくて、validateの返り値をBreaking Changeとして変えたいと思っているくらいでした。

やっぱりvalidateメソッド名がよくて、違和感がなければEitherの方もvalidateToEitherをdeplicatedにしてvalidator.either().validate(...)にしようかなとも思っています。

@making
Copy link

making commented May 13, 2021

ApplicativeBuilder的な ComposingN があるので FunctionN に curried を持たせなくても良さそうですね(実装は泥臭くなりますがオブジェクト生成数を抑制できます)

ふむふむ。オブジェクト生成数は確かに気になっていました。まずはシンプルに実装してみました。

完全に余談ですが、カリー化できる FunctionN を別途定義するより、curried を static メソッドにすると curried(Foo::bar) みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions 的な1クラスのoverloadですませられますし)

試してみます。

@making
Copy link

making commented May 13, 2021

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

applyValidation<E, T>の場合、どこでListにするのでしょうか?

@making
Copy link

making commented May 13, 2021

せっかく Applicative な結果ができたので sequence / traverse が欲しくなりますね。

用途がいまいちわからなかったので実装しませんでしたが、良さそうなので実装したいと思います。

List に特化するよりは Collector化してStreamにできる全コンテナに対応してもいいかもです。参考 https://gist.github.com/gakuzzzz/0c779d5335f4b2bff596#traverse-%E3%81%A8-sequence

試してみます。

@making
Copy link

making commented May 13, 2021

本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?

このPRに関してはvavrのコードは確かにそのようになっていたのですが、まずは概念を理解するためにもシンプルに実装したかったので全部取っ払いましたが、全般的に見直す余地はあると思っています。
というかジェネリクスよくわかっていないのでPRが欲しい...

@making
Copy link

making commented May 13, 2021

ところでapapplyの略で合っていますか?なぜ略すのでしょうか?

@gakuzzzz
Copy link
Author

gakuzzzz commented May 13, 2021

@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は余力があれば週末とかに頑張ってみます……

ところで apapply の略で合っていますか?なぜ略すのでしょうか?

合ってます! がなぜそう略すのかは僕も正確な理由を把握できてないですね。 Scala の場合は apply が特別扱いされるので区別したかったみたいな側面はあるかもしれませんが……。
Haskell の場合元々 <*> という記号ですしね。演算子的に扱うために明示的な名前よりも記号的な ap の方が他の識別子が目立つからみたいな推測をしていますが、本当の所はわかりません。

@gakuzzzz
Copy link
Author

また Validations.composeValidator#composable ですが、 compose という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap による合成も合成には違いないので。

こういった横方向の合成には積の意味で product とかが使われる事がよくあります。https://github.com/typelevel/cats/blob/273257054dff5675aa763cfa09481e1f86709b45/core/src/main/scala/cats/data/Validated.scala#L1047

なのでこの辺の知識がある人にとっては Validator#productive とかの方が合成方向が明示的になってわかりやすいかもしれません。ただ product で製品をイメージしてしまう層にとっては逆にわかりづらくなってしまうかもですね。

@making
Copy link

making commented May 13, 2021

また Validations.compose や Validator#composable ですが、 compose という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap による合成も合成には違いないので。

なるほど。validator.applicative().validate(...)validator.composable().validate(...)だとどちらが自然ですかね?

あるいはvalidator.validation().validate(...)。わかりづらいか...

@gakuzzzz
Copy link
Author

その2択であれば個人的には applicative() の方がわかりやすい気がしますが、あくまで Applicative や Monad に慣れてる側の人間の感覚なので悩ましいですね。

validator.either().validate(...) との対比も考えると collective() とか errorCollective() みたいな方が明示的だったりしますかね?

@making
Copy link

making commented May 13, 2021

では E の集約をどうやって実現しているかというと、 E に対して加算が可能(Semigroup)という制約を課している感じです。

個人的には YAVI のユースケースでは、エラーをListとして集めるケースが全部だと思うので、汎用的な Validation を作るよりは上記で提示したようなFailureをListに限定した ValidationL<E, > にしちゃう方が使いやすいのかな、と思います。

なるほど。確かにSemigroupを導入するのは大袈裟すぎるのでList前提が良さそうですね。

@making
Copy link

making commented May 13, 2021

その2択であれば個人的には applicative() の方がわかりやすい気がしますが、あくまで Applicative や Monad に慣れてる側の人間の感覚なので悩ましいですね。

いったんapplicativeで行こうと思います。ありがとうございます。

@gakuzzzz
Copy link
Author

👍

@making
Copy link

making commented May 13, 2021

なるほど。確かにSemigroupを導入するのは大袈裟すぎるのでList前提が良さそうですね。

ああ、mapError使えないのか。悩ましい。

List<ConstraintViolation>ConstraintViolationsに変換できると嬉しいですが、toEitherを介すしかないかな...

@gakuzzzz
Copy link
Author

ああ確かに List に固定化した場合も mapError が微妙な感じになりますね……。

ConstraintViolations に固定化してしまうというのは有りかもしれません。

@making
Copy link

making commented May 13, 2021

いったん、List<E> error()に変更しました。
making/yavi@35f8e9b

今までList<List<String>>とかList<CostraintViolations>みたいなネストなListが出て気持ち悪いなぁと思っていたところがすっきりしました。この変更がなければtraverseも変な返り値になっていました。
ありがとうございます!

@making
Copy link

making commented May 13, 2021

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());
	}
}

ここで

本筋とは関係ないですが様々なところで上限境界や下限境界を使ってないのは意図的なんでしょうか?

これの対応が必要になりそう。

@gakuzzzz
Copy link
Author

gakuzzzz commented May 13, 2021

いったん、List error()に変更しました。
making/yavi@35f8e9b

素敵!!

ApplicativeValidator.validate() の返り値を次のような特化型Validationにして、ConstraintViolationsを返すようにしてみます。

あーなるほど、アリですね!
この範囲であれば上限境界や下限境界そこまで気にしなくて大丈夫そうな気がします。
その辺の境界を入れたほうが良さそうなやつは Validation#map とか Validations.apply とか Validations.compose とかその辺とかなので。

@making
Copy link

making commented May 13, 2021

この範囲であれば上限境界や下限境界そこまで気にしなくて大丈夫そうな気がします。

これ入れないとvalidateしたあとcomposeするとValidationに戻っちゃいました。

その辺の境界を入れたほうが良さそうなやつは Validation#map とか Validations.apply とか Validations.compose とかその辺とかなので。

あ、その辺のことでした!

@gakuzzzz
Copy link
Author

あ、ですね! compose とかは確かに要りますね!

@making
Copy link

making commented May 13, 2021

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());
	}
}

自分の知識だとfoldmapErrorConstraintViolationsに特化できなかったので、いったん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) {
		// ...
	}

@making
Copy link

making commented May 13, 2021

ValidationL<ConstraintViolation, List> validated = ValidationL.traverse(mails, emailValidator.applicative()::validate);

traverseConstraintViolationを集約した場合、ConstraintViolationmailsのindexをfieldNameに含めたくなりますね。indexを受けるlamdaも渡さないと実用的ではない気がする...

@making
Copy link

making commented May 13, 2021

完全に余談ですが、カリー化できる FunctionN を別途定義するより、curried を static メソッドにすると curried(Foo::bar) みたいな使い方が可能になるので個人的にはそちらの方がお勧めです。(Functions 的な1クラスのoverloadですませられますし)

試してみます。

making/yavi@8f85fcd
staticメソッドにしました。

@making
Copy link

making commented May 14, 2021

Validationクラスに関しては上限・下限を設定。
making/yavi@7d07337

@making
Copy link

making commented May 14, 2021

いったんマージしました。アドバイスありがとうございました。
もう少しブラッシュアップしてリリースしたいと思います。

https://github.com/functionaljava/functionaljava/blob/series/5.x/core/src/main/java/fj/data/Validation.java
を見ながらcomposeaccumulateにしたほうがいいのかなとか思っています。

@gakuzzzz
Copy link
Author

おっと! Scala3リリースとかでバタバタしてて気付いてませんでした! 週末拝見させて頂きます!

@making
Copy link

making commented May 18, 2021

@gakuzzzz

また Validations.compose や Validator#composable ですが、 compose という単語で全然間違いではないのですが、合成の方向ってネスト方向にもあったりするんですよね。 また flatMap による合成も合成には違いないので。

このコメントを受けて Validation.compose メソッド及び ComposingN も別の名前にした方がいいかなと思っているのですが、
使用例から accumulatecombine があるのですが、どれが自然でしょうか?

@making
Copy link

making commented May 18, 2021

and でもいいか

@making
Copy link

making commented May 19, 2021

vavrに寄せてcombineに変更してみます。

@gakuzzzz
Copy link
Author

accumulate, and, combine だと combine が個人的にはいいかなと思います。 vavr に寄せるの賛成 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment