flatMap
とfor
を作る
flatMap
で更にmap
系の訓練をするEither
を実用する際は複数のEither
を組み合わせて初めて真価を発揮することを知るfor
の存在を知る
フォームのバリデーションをEither
を使って実装する
以下のクラスが存在するとする
@AllArgsConstructor
@EqualsAndHashCode
public class FirstName {
private final String value;
}
@AllArgsConstructor
@EqualsAndHashCode
public class LastName {
private final String value;
}
@AllArgsConstructor
@EqualsAndHashCode
public class FullName {
private final FirstName first;
private final LastName last;
}
public enum Grade {
S, A
}
@AllArgsConstructor
@EqualsAndHashCode
public class User {
private final FirstName first;
private final LastName last;
private final Grade grade;
}
文字列をValueObject
もしくはエラーメッセージ(String)
にする以下のForm
クラスを実装する
(条件分岐が本題ではないので、null
は考慮しなくて良い)
public class FirstNameForm {
public static ??? bind(String s) {
// 空文字を不正とする
}
}
public class LastNameForm {
public static ??? bind(String s) {
// 空文字を不正とする
}
}
public class GradeForm {
public static ??? bind(String s) {
// 'S' と 'A' 以外を不正とする
}
}
Either
にflatMap
を実装せよ
flatMap
は普通にmap
するとネストしてまう場合に用いるメソッド- ここまでで理解した通り、
Either<A, B>
に対してFunction<B, C>
をmap
するとEither<A, C>
になる - しかし、
Either<A, B>
に対してFunction<B, Either<A, C>>
をmap
するとEither<A, Either<A, C>
になってしまう - ここで、
Either<A, B>
にFunction<B, Either<A, C>>
をflatMap
するとEither<A, C>
とする事が出来る
- ここまでで理解した通り、
flatMap
の仕様として、2つのEither
型であるe1
とe2
において以下を満たすことe1
の左の型とe2
の左の型が同じであることe1
が右でありe2
も右である場合のみ、e1
の右値とe2
の右値を使って演算をし、右に詰めて返すe1
が左の場合はe2
によらずe1
の左値を返すe2
が左の場合はe1
によらずe2
の左値を返す
以下のテストを通すコードを実装せよ
def "flatMap_2"() {
when:
def first = new FirstNameForm().bind(f)
def last = new LastNameForm().bind(l)
then:
first.flatMap {
fv ->
last.map {
lv -> new FullName(fv, lv)
}
} == exp
where:
f | l || exp
'John' | 'Doe' || Either.right(new FullName(new FirstName('John'), new LastName('Doe')))
'' | 'Doe' || Either.left('FirstName must not be empty')
'John' | '' || Either.left('LastName must not be empty')
'' | '' || Either.left('FirstName must not be empty')
}
また、以下のテストも通る事を確認せよ
(上記のflatMap_2
が通ればEither
の改修は必要なくflatMap_3
も通るはず)
def "flatMap_3"() {
when:
def first = new FirstNameForm().bind(f)
def last = new LastNameForm().bind(l)
def grade = new GradeForm().bind(g)
then:
first.flatMap {
fv ->
last.flatMap {
lv ->
grade.map {
gv -> new User(fv, lv, gv)
}
}
} == exp
where:
f | l | g || exp
'John' | 'Doe' | 'S' || Either.right(new User(new FirstName('John'), new LastName('Doe'), Grade.S))
'' | 'Doe' | '' || Either.left('FirstName must not be empty')
'John' | '' | '' || Either.left('LastName must not be empty')
'' | '' | '' || Either.left('FirstName must not be empty')
'John' | 'Doe' | '' || Either.left("Grade must be 'S' or 'A'")
}
上記2つのテストは次で出るforExpression
のヒントなので、頑張って理解してください
(いきなりflatMap_3
読めと言われるのは相当難しいと思うけど頑張って!)
また、以下の部分のlast.map
をlast.flatMap
に変えてlv ->
の部分を少し書き換えた方法でもテストが通ることを書き換えて確認せよ
first.flatMap {
fv ->
last.map {
lv -> new FullName(fv, lv)
}
} == exp
flatMap_3
でも同様にgrade.map
をgrade.flatMap
に書き換えて動作確認せよ
first.flatMap {
fv ->
last.flatMap {
lv ->
grade.map {
gv -> new User(fv, lv, gv)
}
}
} == exp
Either
にstatic
でforExpression
を実装せよ
Scala
ではfor式
、Haskell
ではdo
と言ったりする- 一般には
flatMap
を用いて実現される for式
はEither
の機能では無く、イメージとしてはList
やOptional
等の<T>
をつめて使う箱に対する汎用的な機能であるScala
ではとあるインターフェースを実装しているクラスは全てfor式
に渡せるが、今回はEither
なら渡せるfor式
で良い
- 任意数の
Either
と、それらEither
の全ての右値を引数に取り任意の型で返す関数を渡すと、Either
が全て右だった場合のみEither
を全て剥がし関数を適用しEither
に包んで返す処理である- 任意数とは言ったが実際にはオーバーロードして異なるメソッドとして実装する
2引数用のforExpression
と3引数用のforExpression
を実装せよ- 内部で
flatMap
を用いて実現すること
- 内部で
- 左を含む
Either
をforExpression
に渡した場合は、最初に検知した左値を返す
以下のテストを通すコードを実装せよ
def "for_2"() {
when:
def first = new FirstNameForm().bind(f)
def last = new LastNameForm().bind(l)
then:
Either.forExpression(
first,
last,
{ _f, _l -> new FullName(_f, _l) }
) == exp
where:
f | l || exp
'John' | 'Doe' || Either.right(new FullName(new FirstName('John'), new LastName('Doe')))
'' | 'Doe' || Either.left('FirstName must not be empty')
'John' | '' || Either.left('LastName must not be empty')
'' | '' || Either.left('FirstName must not be empty')
}
また、以下のテストも通る事を確認せよ
(上記のfor_2
が通ればEither
の改修は必要なくfor_3
も通るはず)
def "for_3"() {
when:
def first = new FirstNameForm().bind(f)
def last = new LastNameForm().bind(l)
def grade = new GradeForm().bind(g)
then:
Either.forExpression(
first,
last,
grade,
{ _f, _l, _g -> new User(_f, _l, _g) }
) == exp
where:
f | l | g || exp
'John' | 'Doe' | 'S' || Either.right(new User(new FirstName('John'), new LastName('Doe'), Grade.S))
'' | 'Doe' | '' || Either.left('FirstName must not be empty')
'John' | '' | '' || Either.left('LastName must not be empty')
'' | '' | '' || Either.left('FirstName must not be empty')
'John' | 'Doe' | '' || Either.left("Grade must be 'S' or 'A'")
}
任意のForm
とforExpression
を利用者側に公開するのはForm
として少し不親切なので、Forms
クラスを作り複合Form
にリファクタリングする
- 最後の仕上げと言った感じ
Either
もforExpression
も改修不要
以下のテストが通る様にFullNameForms
とUserForms
クラスを作成せよ
def "for_form_2"() {
expect:
FullNameForms.bind(f, l) == exp
where:
f | l || exp
'John' | 'Doe' || Either.right(new FullName(new FirstName('John'), new LastName('Doe')))
'' | 'Doe' || Either.left('FirstName must not be empty')
'John' | '' || Either.left('LastName must not be empty')
'' | '' || Either.left('FirstName must not be empty')
}
def "for_form_3"() {
expect:
UserForms.bind(f, l, g) == exp
where:
f | l | g || exp
'John' | 'Doe' | 'S' || Either.right(new User(new FirstName('John'), new LastName('Doe'), Grade.S))
'' | 'Doe' | '' || Either.left('FirstName must not be empty')
'John' | '' | '' || Either.left('LastName must not be empty')
'' | '' | '' || Either.left('FirstName must not be empty')
'John' | 'Doe' | '' || Either.left("Grade must be 'S' or 'A'")
}
ポイント
- 関数を渡す処理の記述が
Groovy
からJava
に変わったことで、メソッド参照が出来る様になっていることに気づけると良い Form
が組み合わさってForms
が実現されるのを良く理解して欲しい- 個々の
Form
のバリデーションルールは単体テストが済んでいて、Forms
は組み合わせることだけが責務なので安心して組み合わせられる
- 個々の
Forms
を更に上位のForms
に組み合わせることが簡単に実現できることを実感して欲しいSignUpForms
UserForms
NameForm
,GenderForm
,BirthDateForm
,,,
AddressForms
ZipCodeForm
,CityForm
,,,
flatMap
であることによって、Forms(Forms(Form, Form))
みたいな入れ子構成が最終的にフラットなEither
になることをよく理解して欲しいmap
だとEither<Either<Either<String, SignUpForms>>>
みたいになってしまう
- 一番大事なこと
flatMap
を用いると自分で剥がしたり詰めたりする処理を書かなくて良くなるので、get
してしまって実行例外と言った低品質なコードを書かなくて良くなるfor式
があるとflatMap
という難しそうな処理では無く、あたかも剥がしているかの様な簡単な記述でそれを読み書きすることが出来るflatMap
もforExpression
も、引数から戻りまで左の型はずっと変わらないし、Either
を剥がして返却することもないのでEither<L,
の部分は不変であることが保たれるOptional
のflatMap
やforExpression
ではOptional
であることが保たれるEither
やOptional
を文脈と言ったりする
Either
の亜種であるValidation
を検討中
Validation<Failure, Success>
はEither<Left, Right>
と異なり左側に全てのエラーをためることが出来る
例えば可否チェックで以下の項目があるとき、ある箇所でこけたらそれ以降はチェックする必要がないので
Either
を使って最初のエラーだけを返すということがやりかたの1つとして挙げられる
- 会員が存在しない
- 契約が存在しない
- 回線が存在しない
- 機器が存在しない
しかし例えばまさにバリデーションの様に、'' | '' | '' || Either.left('FirstName must not be empty')
の様に最初のエラーだけだと不親切な場合がある
そういう際はValidation
を使うと3箇所全てのバリデーションエラーメッセージを返却することが出来る様になる
(テストイメージ)
'' | '' | '' || Validation.failure(['FirstName must not be empty', 'LastName must not be empty', "Grade must be 'S' or 'A'"])
が、Java
の限界を感じ、すこしださいやりかたでないと実現出来なそうなので、どのだささで実現するか模索中