Javaによる関数型プログラミング、その3



Javaによる関数型プログラミング、その2の続きである。このシリーズは/tag/関数型プログラミングにある。

さて、関数型プログラミングというと、必ず出てくるのが「モナド」という概念なのだが、正直言ってこれがわからない。何なのか、何が嬉しいのか、どこを見てもさっぱりわからないのだ。何の役に立つのだろうか?

ちなみに数ある解説の中で最もわかりやすいと思われるのが、関数型つまみ食い: モナドが難しいと思われている理由という記事である。もっとも、これでもまだ私の頭では理解できないのだが。。。

第一印象:モナドとはパイプを構成する型である

あちこちを見てみると、やたりに難しいことが書いてあるのだが、初心者としては「パイプ」だと思えば良いだろう。いくつかの「計算」があり、それをつなげていき最終的に所望の結果を得る「あれ」である。

よく使われる例として以下がある。

※Optionalがわからない方は、他で調べてほしい。

  Optional<ZipCode> getZipCode(Optional<Person> person) {
    Optional<ZipCode>zipCode = person.map(p->p.getAddress()).map(a->a.getZipCode());
    return zipCode;
  }

見れば明らかだが、PersonからAddressを取得する計算、AddressからZipCodeを取得する計算をつなげているわけだ。まるでパイプのように、それぞれが入力から出力を作りだしている。

当然だが、これらの各計算では副作用があってはならない。Javaではどうとでも書くことができてしまうが、それをやると「関数型」にはならないというわけである。

もちろん、副作用が無いので、上記のコードはスレッドセーフである。つまり、複数のスレッドが同時にこのコードを使用することができる。

この時に使用している型Optionalというものがモナドというものらしい。モナドを使わず、単純にZipCodeを得るなら、以下とすればよいのである。

  ZipCode getZipCode(Person person) {
    ZipCode zipCode = person.getAddress().getZipCode();
    return zipCode;
  }

なぜOptionalモナドを使うのか?

単純に「person.getAddress().getZipCode()」とすれば良いものを、なぜわざわざOptionalというモナドを使うのだろうか?

先の「関数型つまみ食い: モナドが難しいと思われている理由」という記事によれば、モナドとは関数を純粋に保ったまま「おまけ」をつける機能なのだそうだ。つまり、「入出力に紛れ込む副作用」を本来の処理から隠蔽して冗長さを軽減するために導入されたのが、いわゆる「モナド」だそうである。

さて、Optionalでの「おまけ」とは何かと言えば、null値である。このコードでは、以下のような例外的な状況があるわけだ。

  • そもそも受け取ったPersonがnull値。
  • getAddress()で取得した値がnull値。
  • getZipCode()で取得した値がnull値。

等の例外的状況がありうる。しかし、単純に「person.getAddress().getZipCode()」としてしまったら、この状況に対応できない。通常のJavaコードであれば、以下のいずれかの対策をするだろう。

  ZipCode getZipCode(Person person) {
    if (person == null) return null;
    Address address = person.getAddress();
    if (address == null) return null;
    return address.getZipCode(); // さらに上位でnullかどうか判断する
  }

あるいは、

  // 上位でNullPointerExceptionをキャッチする
  ZipCode getZipCode(Person person) {
    ZipCode zipCode = person.getAddress().getZipCode();
    return zipCode;
  }

何にしても、「nullかどうかを注意深くチェックしながら進む」か「NullPointerExceptionをキャッチする」という方策になる。

しかし、Optionalモナドの場合には、以下で済んでしまうのである。

person.map(p->p.getAddress()).map(a->a.getZipCode()).ifPresent(z->何かする);

if文もいらなければ、例外処理もいらない。

もしこうだったら?

先の例では、実は以下のようなクラスを仮定していた。

  public static class Person {
    public Address getAddress() { ....; }  
  }
  public static class Address {
    public ZipCode getZipCode() { ....; }
  }
  public static class ZipCode {    
  }

しかし、getAddress()やgetZipCode()がnullを返す可能性があるのなら、こうなっているかもしれない。

  public static class Person {
    public Optional<Address> getAddress() { ....; }  
  }
  public static class Address {
    public Optional<ZipCode> getZipCode() { .....; }
  }
  public static class ZipCode {    
  }

ここから、ZipCodeを取得するには、mapの代わりにflatMapを使うようだ。

Optional<ZipCode>zipCode = person.flatMap(p->p.getAddress()).flatMap(a->a.getZipCode());

どうもflatMapとは「一皮むく」というイメージらしい。

結果に対して何かするなら、こうする。

person.flatMap(p->p.getAddress())flatMap(a->a.getZipCode()).ifPresent(z->何かする);

まさにパイプのように処理を続けていくのだが、パイプと異なるところは、途中でnullになった場合にはそれ以降何もされないことだ。

(続く)