Skip to content

Instantly share code, notes, and snippets.

@suzuki-hoge
Last active July 15, 2017 03:14
Show Gist options
  • Save suzuki-hoge/7f3d42310e3c4bfc51fba6b9ec06fb4f to your computer and use it in GitHub Desktop.
Save suzuki-hoge/7f3d42310e3c4bfc51fba6b9ec06fb4f to your computer and use it in GitHub Desktop.

もっともっと Either

flatMapforを作る

意図

  • flatMapで更にmap系の訓練をする
  • Eitherを実用する際は複数のEitherを組み合わせて初めて真価を発揮することを知る
  • forの存在を知る

お題

フォームのバリデーションをEitherを使って実装する

ValueObject

以下のクラスが存在するとする

@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;
}

Form

文字列を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

EitherflatMapを実装せよ

解説とヒント

  • 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型であるe1e2において以下を満たすこと
    • 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.maplast.flatMapに変えてlv ->の部分を少し書き換えた方法でもテストが通ることを書き換えて確認せよ

first.flatMap {
    fv ->
        last.map {
            lv -> new FullName(fv, lv)
        }
} == exp

flatMap_3でも同様にgrade.mapgrade.flatMapに書き換えて動作確認せよ

    first.flatMap {
        fv ->
            last.flatMap {
                lv ->
                    grade.map {
                        gv -> new User(fv, lv, gv)
                    }
            }
    } == exp

Either の拡張 - forExpression

EitherstaticforExpressionを実装せよ

解説とヒント

  • Scalaではfor式Haskellではdoと言ったりする
  • 一般にはflatMapを用いて実現される
  • for式Eitherの機能では無く、イメージとしてはListOptional等の<T>をつめて使う箱に対する汎用的な機能である
    • Scalaではとあるインターフェースを実装しているクラスは全てfor式に渡せるが、今回はEitherなら渡せるfor式で良い
  • 任意数のEitherと、それらEitherの全ての右値を引数に取り任意の型で返す関数を渡すと、Eitherが全て右だった場合のみEitherを全て剥がし関数を適用しEitherに包んで返す処理である
    • 任意数とは言ったが実際にはオーバーロードして異なるメソッドとして実装する
    • 2引数用のforExpression3引数用のforExpressionを実装せよ
      • 内部でflatMapを用いて実現すること
  • 左を含むEitherforExpressionに渡した場合は、最初に検知した左値を返す

以下のテストを通すコードを実装せよ

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'")
}

リファクタリング - Forms

任意のFormforExpressionを利用者側に公開するのはFormとして少し不親切なので、Formsクラスを作り複合Formにリファクタリングする

解説とヒント

  • 最後の仕上げと言った感じ
  • EitherforExpressionも改修不要

以下のテストが通る様にFullNameFormsUserFormsクラスを作成せよ

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という難しそうな処理では無く、あたかも剥がしているかの様な簡単な記述でそれを読み書きすることが出来る
    • flatMapforExpressionも、引数から戻りまで左の型はずっと変わらないし、Eitherを剥がして返却することもないのでEither<L, の部分は不変であることが保たれる
      • OptionalflatMapforExpressionではOptionalであることが保たれる
      • EitherOptionalを文脈と言ったりする

次回予告

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の限界を感じ、すこしださいやりかたでないと実現出来なそうなので、どのだささで実現するか模索中

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