こまぶろ

技術のこととか仕事のこととか。

社外の人にしてもらった1on1を振り返る

随分と日が空いてしまったが、2月の中頃、ブログ仲間(?)のnitt-san(@nitt_san)さん*1に、1on1の機会を設けていただいた。nitt-sanさんによる記録はこちら。

nitt-san.hatenablog.com

社外の人と「いきなり1on1」?

今回、1on1をしてもらったnitt-sanさんは、僕の会社の上司でも何でもない。繋がりは、「write-blog-every-week」*2と、「エンジニアの登壇を応援する会」という2つの社外コミュニティであり、リアルでお会いしたのは今回の1on1が3回目だった。

nitt-sanさんは、先月から「いきなり1on1」という企画を実施されていて、僕の前にすでに6人の方と1on1をされている*3。今回の僕の1on1も、その企画の一環として行われた。nitt-sanさんが「いきなり1on1」を行うに至った経緯については、下に掲げた記事を参照していただきたい。

nitt-san.hatenablog.com

この記事では、nitt-sanさんとの1on1を、2週間が経とうとしている時点から改めて振り返る。

「30歳のときにどうしていたい?」

冒頭の方で受けた質問だ。この質問が出たのは、当日の最初に記入したヒアリングシートに僕が「目標がない」と記入し、しかもそれを最高の優先度としたからだろう。目標がうまく持てていないことが一番の関心事であり、この日の1on1に対してもこの辺りのモヤモヤが少しでも晴れればと思って臨んでいた。

そんな僕であったが、上に掲げた質問を投げかけられて、固まってしまった。ここで固まってしまうようであるからこそうまく目標設定ができないでいると言えばそれまでなのだが、一発の質問で何も考えられていなかったことに気づかせられた。この質問になぜ自分が固まってしまったのかを考えてみると、そこに「30歳」という具体的な数字が入っていることが大きいのではないかと思う。

以前、このブログで、プログラマとして生き、技術の進歩にキャッチアップしていくことへの不安を前面に出した記事を書いた。

ky-yk-d.hatenablog.com

このような記事を書くのであるから、ある程度長い期間、たとえば10年くらいは、プログラマとして生きていくことを意識していたはずである。そうであるとすれば、たかだか4年後である30歳のときのことはある程度具体的に考えていても良さそうなものである。しかしながら、僕は20年や30年、下手したら40年後のことについて不安を覚えてばかりだった。

長いタイムスパンでものを考えるのは時として必要なことだが、それが意味を持つのは、より近い将来や現在の行動を考えるのに役立ててこそだろう。将来への不安で現在が埋め尽くされ、近い将来を具体的に考えることから目を背けるようでは、せっかくの「長期的視野」も害あって利がない

今回、「30歳」という具体的な数字を伴った質問をされたことで、あちこちに散逸しがちな自分の意識を、特定の一点に集中させるきっかけを得ることができた。まだ、問いに対する答えを練り切れてはいないが、貴重な問いかけだったと感じている。

「どうすれば経験から学んだことになる?」

これは、僕が「経験から学ぶのが苦手なんです」というようなことを言ったことを承けての質問だ。僕は、本をよく読む方で、そこから得た学びを活かすこともそれなりにできていると思っている。しかし一方で、自分が実際に見た、体験した事柄から教訓や認識を抽出し、定式化する、あるいは別の機会に活かす、ということにはあまり自信がない。

ブログや日常の会話といった場面においても、書籍や人の話からの引用がソースであることが多い。他の人のブログや勉強会での発表に、現場でその人が経験した事柄に根ざした知見が表現されていると、「自分にはこれは無理だ・・・」と勝手に落ち込んでしまうこともある。いわば「経験からの学習コンプレックス」である。

以上のような自己認識を持っていた僕に、上掲の質問が持った意味は何だっただろうか。この質問は、「自分は経験から学べない」という僕が持っている信念に対して、「それを反証するような事象はどのようなものか」、と問うものだ。自分についての信念というのは、それが自分のことであるがゆえに、他者による検証を受け付けない独断的なものになりがちだ。

「自分は〇〇なんです」というネガティブな言及に対して、「そんなことないよ!」と否定するのは簡単だが、あまり有効でない場合がある。その信念が、自分の行動にロックを掛けてコンフォートゾーンに留まることを許すような働きを持っていたり、セルフ・ハンディキャッピングのために作り出されていることがあるからだ。

「経験から学べない」という自分の発言も、経験から学ぶために必要な事柄ーーとりわけ、不都合な現実に向き合うことーーへの億劫さに由来しているもののように思う。「本を読んで得た知識を行動に写したり表現したりできる」というポジティブな面ではなくあえてネガティブな面を訴えるのには、自分でもまだよくわからない心理が働いているのだろう。

自己についての信念が、歪んでいたり、有害であったりする場合に、それを矯正するのには、当人の口から語らせることが有効なのだと思う。今回のnitt-sanさんの質問は、僕の提示した信念が反証される条件を僕自身に語らせることで、その条件を満たす事例を見つけたときに信念を修正せざるを得ない状況を作り出す効果があった。

自らの口で語った反証の条件を満たす場面に直面してなお、その信念を守ろうとするのであれば、その信念はその事実性ゆえにではなく有用性において指示されているものだということが明らかになるだろう。

感想:「社外」であることの意味

冒頭に書いたとおり、nitt-sanさんと僕は同じ会社に勤めているわけではない。会社を異にする者が1on1を行うことには、それゆえの制約がある。コンプライアンス的に話せない事柄があったり、お互いに仕事ぶりを含む普段の振る舞いを知らないためにコミュニケーションに齟齬を生じやすかったりというのは、十分に想定されることだろう。

しかし、いい面もある。まず、利害関係がないので、自己防衛的な語りをせずに済む。そのため、素直に発言できるというだけでなく、会社では取り繕っている(つもりの)自分に都合の悪い部分も他人の前に晒すことになり、フィードバックが受けやすい。上司からであればムッとさせられるようなアドバイスも、素直に受け入れやすい。また、社内ではありがちな「フィードバックと評価が密着してしまう問題」の懸念もない。そして、転職という話題も躊躇なく持ち込める。

今回の1on1は、自分にとって気づきの多い時間だった。以上に感想を記したのは、そのごくわずかな一部分である。言及したところ以外にも、ハッとさせられたやり取りが多くあったことのみ記述しておきたい。これからゆっくりと消化し、今後の自分の行動に活かしていきたい。読者の方におかれては、くれぐれにここに書いた事柄をもって、nitt-sanさんの1on1の全体を評価されることがないようにお願いしたい。

nitt-sanさん、本当にありがとうございました。

この1冊ですべてわかる 新版 コーチングの基本

この1冊ですべてわかる 新版 コーチングの基本

*1:これが「さかなクンさん」と同等の表現になるのかどうかはよくわからない。

*2:週一回ブログを書く会。

*3:本記事の執筆時点で、12人の方と実施されている。

StreamとOptionalに共通して存在するメソッド(その3・完結篇):map / flatMap

StreamとOptionalに共通して存在するメソッドの第三弾。 第一弾・第二弾はこちら。

ky-yk-d.hatenablog.com

ky-yk-d.hatenablog.com

これまで、下記のメソッドについて書いてきた。

  • filter()
  • of()
  • ofNullable()
  • empty()

完結篇となる今回は、map()flatMap()について。これまでに出てきたメソッドとも関連させながら書いていきたい。

マッピング関数を引数にとるメソッド

メソッド Stream Optional
map() Stream map​(Function<? super T,​? extends R> mapper) public Optional map​(Function<? super T,​? extends U> mapper)
flatMap() Stream flatMap​(Function<? super T,​? extends Stream<? extends R>> mapper) public Optional flatMap​(Function<? super T,​? extends Optional<? extends U>> mapper)

map()flatMap()は、いずれも「マッピング関数」を引数にとるメソッドだ。マッピング関数とは、一定の入力を一定の出力に対応づけする関数で、関数型インタフェースFunction<T, R>という型で指定されていることから明らかなように、ラムダ式やメソッド参照を代入することができる。

map()flatMap()のいずれも、元のStream/Optionalの要素を取り出し、マッピング関数で変換し、その返り値を要素として含む新たなStream/Optionalを返すメソッドだ。異なるのは、マッピング関数が返す値の型と、それをどのように新たなStream/Optionalに流すかだ。それぞれ見ていこう。

map()のマッピング関数は「要素そのもの」を返す

まず、わかりやすいmap()から。このメソッドは、Stream/Optionalの要素を取り出し、引数で指定されたマッピング関数を適用し、その返り値を要素とする新たなStream/Optionalを返す。挙動としては非常にわかりやすい。少し注意が必要なのは、マッピング関数の返り値がnullだった場合だ。

Streamのmap():マッピング関数の結果のnullも1つの要素として扱う

Streamの場合は、nullも一つの要素として新たなStreamに含められるのに対して、Optionalの場合は、nullをラップしたOptionalではなく、空のOptionalが出力される。コードを示す。

/* 動作確認用のstaticメソッド定義 */
class Sample {
    static Integer stringToInteger(String str){
        try {
            return Integer.valueOf(str);
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
/* JUnit5を用いた動作確認 */
@Test
void Streamのmap() {
    List<String> src = List.of("123", "456", "変換できない文字列");
    List<Integer> list = src.stream().map(Sample::stringToInteger).collect(Collectors.toList());
    assertEquals(3, list.size()); // テストはグリーン(nullも要素の一つとして含んでいる)
    list.forEach(System.out::print); // 123456null と出力される
}

Streamのmap()は、マッピング関数の返り値がnullだった場合、それをそのまま新しいStreamの1つの要素として挿入する。map()の前後でStreamの要素の数は増減していない。

Optionalのmap():マッピング関数の返り値がnullの場合は空のOptionalを返す

これに対して、Optionalのmap()で、マッピング関数の返り値がnullだった場合は、(元のOptionalが空だった場合と同じく)空のOptionalを返す。

@Test
void Optionalのmap() {
    Optional<String> opt = Optional.of("123");
    Optional<Integer> result = opt.map(Sample::stringToInteger);
    System.out.println(result); // Optional[123] と出力
    opt = Optional.of("変換できない文字列");
    result = opt.map(Sample::stringToInteger);
    System.out.println(result); // Optional.empty と出力(Optional[null]とはならない)
}

これは、map()の実装において、マッピング関数の返り値をOptionalに包む際に、Optional.ofNullable()を利用しているためだ。実装は以下のようになっている。

public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();  
    } else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

map()のまとめ

まとめると、以下のことが言える。

  • StreamとOptionalのmap()は、元のStreamないしOptionalの要素を置き換えた新しいStreamないしOptionalを返す
  • ただし、マッピング関数の結果がnullの場合に、Streamはnullをそのまま要素に含めるが、Optionalは空のOptionalを返す(nullをラップしたOptionalが返ることはない)

flatMap()のマッピング関数は「要素の入れ物」を返す

map()に渡すマッピング関数の返り値の型は? extends R(U)であるのに対し、flatMap()に渡すマッピング関数の返り値の型はStream(Optional)<? extends R(U)>である。map()マッピング関数が「要素そのもの」を返したのに対し、flatMap()マッピング関数はStream/Optionalであり、いわば「要素の入れ物」を返す。マッピング関数の入力がそもそも、(map()と同様に)元のStream/Optionalという「入れ物」の中身である要素だから、「入れ物の要素1つから新しい入れ物を作る」というのがマッピング関数の役割になる。

そして、flatMap()全体としては、マッピング関数の出力であるそれぞれの「入れ物」から要素を取り出し、新しいStream/Optionalに送り込む。マッピング関数の出力をそのままStream/Optionalに送り込むのではなく、「入れ物」から要素を取り出すことにより、flatMap()の戻り値はネストしたStream/Optionalにはならない(これがフラット化の意味だ)。

StreamのflatMap():「1対多の変換」はミスリーディング

Streamの場合、Streamという「入れ物」には複数の要素を入れることができるから、flatMap()の典型的な利用例として、入力のStreamの各々から複数の出力を取り出してそれを新しいStreamの要素とする、という「数を増やす」形の変換に利用される。実際、StreamのflatMap()APIドキュメントの「APIの注」という項目に、「1対多の変換」という表現がある。

flatMap()操作には、ストリームの要素に対して1対多の変換を適用した後、結果の要素を新しいストリーム内にフラット化する効果があります。

しかし、ここでいう「1対多の変換」が、必ずしも「数を増やす」ものではないということには注意が必要だ。例を示そう。StreamのflatMap()が使われるシンプルなケースの1つとして、以下のように、ネストしたコレクションからそれぞれの要素を取り出して処理の対象とする場合がある。

List<List<String>> src = List.of(List.of("123","456"), List.of("789"), List.of());
List<String> dest = src.stream().flatMap(List::stream).collect(Collectors.toList());
dest.forEach(System.out::print); // 123456789 と出力

上の処理において、flatMap()に渡されているマッピング関数List::stream()は、ラムダ式では list -> list.stream()と書くこともでき、List型の入力を受け付けて、Stream型の出力を返す関数となっている。このStreamの要素であるString型の値を、flatMap()が(List::streamではなくflatMap()自体の)戻り値であるStreamに流しこむ。

ここで、srcの要素であるList型のオブジェクトのうち、最初のものであるList.of("123","456")からは"123""456"という2つの文字列を取り出す1対2の変換を行なっているが、第三要素であるList.of()からは要素を取り出していないため1対0の変換になっている。「1対多の変換」の「多」というのは「0以上」という意味だということがわかる。この意味で「多」を利用することはプログラミングの世界では珍しくないが、ミスリーディングな説明だと思う。

1対0の変換:Java9で導入されたStream.ofNullable()とOptional.stream()

上で殊更に「1対0」の場合があると書いたのには意味がある。その意味は、Java 9 で導入されたメソッドである、StreamのofNullable()とOptionalのstream()を見てみるとわかる。

前回の記事で紹介したofNullable()は、引数がnullであれば空のStreamを返すファクトリメソッドだ。一方、Optionalのstream()は、Optionalが空であれば空のStreamを、空でなければその要素を含むStreamを返すメソッドであり、以下のような実装を持つ。

public Stream<T> stream() {
    if (!isPresent()) {
        return Stream.empty();
    } else {
        return Stream.of(value);
    }
}

これら2つのメソッドは、似たような場面で用いることができる。具体的には、下記のコードにおいて、2つのテストケースは同じ動きを示す。SampleクラスのstringToOptionalInteger()メソッドは、先に登場したstringToInteger()メソッドとほぼ同じで、異なるのは変換が失敗したときに、stringToInteger()がnullを返すのに対して、stringToOptionalInteger()は空のOptionalを返すという点だ(したがって、後者は返り値の型がOptional<Integer>になっている。

class Sample {
    /* 変換失敗時に空のOptionalを返すstaticメソッド */
    static Optional<Integer> stringToOptionalInteger(String str){
        try {
            return Optional.of(Integer.valueOf(str));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}

動作確認用のコードは以下の通り。

@Test
void StreamのofNullableの利用() {
    List.of("123", "456", "変換できない文字列").stream()
            .flatMap(str -> Stream.ofNullable(Sample.stringToInteger(str)))
            .forEach(System.out::print); // 123456 と出力
}
        
@Test
void Optionalのstreamの利用() {
    List.of("123", "456", "変換できない文字列").stream()
            .flatMap(str -> Sample.stringToOptionalInteger(str).stream())
            .forEach(System.out::print); // 123456 と出力
}

上記のテストメソッド、StreamのofNullableの利用()Optionalのstreamの利用()は同じ動きをする。前者ではnullを返すメソッドを呼び出し、それをStream.ofNullable()に渡すことでStreamを作っているのに対し、後者では空のOptionalを返すメソッドを呼び出し、それをstream()によってStream化している。いずれもマッピング関数の返り値はStream型になっていて、その要素がflatMap()により新しいStreamに投入されている。

ここでは、flatMap()は、(先に見た例のように)入力のStreamの各要素から複数の要素を生み出す(数を増やす)ために用いられているのではなく、むしろ数を減らすために用いられている。この例で用いているStream.ofNullable()Optional.stream()の両方がJava9で導入されたメソッドなのは、利用方法から考えると納得がいくことではないだろうか。

OptionalのflatMap():1対0 or 1対1の変換

さて、以上のように考えると、OptionalのflatMap()も理解しやすい。これもまた、StreamのflatMap()同様に「1対多の変換」を行うマッピング関数を受け取るものであり、たまたまOptionalというものの性格上、数を増やすためには用いることができないのだ、という理解の仕方もできるだろう。flatMap()は、以下のように実装されている。

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent()) {
        return empty();
    } else {
        @SuppressWarnings("unchecked")
        Optional<U> r = (Optional<U>) mapper.apply(value);
        return Objects.requireNonNull(r);
    }
}

map()マッピング関数の返り値がnullだった場合の挙動と、flatMap()マッピング関数の返り値が空のOptionalだった場合の挙動は同じで、いずれも空のOptionalが返る。「要素ありのOptional→空のOptional」という置き換えで行われている変換を「1対0の変換」と呼ぶならば、Optionalのmap()flatMap()はいずれも1対0の変換を行うことができるものだと言える。どちらを使うかは、マッピング関数で利用したい関数がnullを返すのか空のOptionalを返すかによって決めればいい。これは、Streamのmap()がnullを要素として扱い、要素の数を減らすような動きはしない(nullを消すためには先述のようにflatMap()を利用する)のと異なっている。

まとめとモナドに関する参考記事

3回に渡って、StreamとOptionalに共通して存在するメソッドについて書いた。自分自身の理解を作るためという趣旨で書き始めた記事だったが、今回記事にするために挙動を一つずつ確認していくなかで、StreamとOptionalの同一名称のメソッドが似ているようで少しずつ違うのがわかった。同じ名称だと仕様の違いに鈍感なまま雰囲気で使ってしまいがちだが、今後は細かい挙動の違いも意識してコードを書くことができると思う。

なお、今回の記事を書いている過程で、モナドというキーワードに接した。Haskellを学ぶと出会うことになる概念という印象を持っていたが、OptionalとStreamというJava関数型プログラミング的な側面においてはこの概念が顔を出すようだ。詳しくは下記に列挙した記事をご覧いただきたい。

d.hatena.ne.jp

qiita.com

StreamとOptionalに共通して存在するメソッド(その2):of / ofNullable / empty

StreamとOptionalに共通して存在するメソッドの第二弾。 第一弾はこちら。

ky-yk-d.hatenablog.com

前回の記事では、filter()について書いた。今回は、of()ofNullable()について書……こうと思ったら実はempty()も共通して存在するメソッドだったことに気づいたので、これも併せて3つのメソッドについて書く。

ファクトリメソッド: of()ofNullableempty()

メソッド Stream Optional
of() static Stream of​(T t) public static Optional of​(T value))
ofNullable() static Stream ofNullable​(T t) public static Optional ofNullable​(T value)
empty() static Stream empty() public static Optional empty()

of()ofNullable()empty()は、StreamとOptionalのいずれにおいてもstaticのファクトリメソッドだ。Streamはクラスではなくインタフェースだが、Java 8からはインタフェースにstaticメソッドが書けるようになっているため*1、ファクトリメソッドを定義することができる。実際には、下記のように定義されている。

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> ofNullable(T t) {
    return t == null ? Stream.empty()
                     : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

public static<T> Stream<T> empty() {
    return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}

対して、Optionalのそれぞれのメソッドの定義は下記の通りだ。

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

こうして並べてみると、StreamとOptionalのそれぞれにおいて、of()ofNullable()という2つのメソッドの関係が同じであることがよくわかる。of()が引数で受け取った何らかの値をそのままStreamないしOptionalに包んで「返そうとする」のに対して、ofNullable()は引数がnullだった場合は空のインスタンス(それぞれのempty()メソッドで取得する)を返し、nullでない場合はof()と同じ結果を返すようになっている。空のインスタンスについては、その挙動を含めて次回に回す。

of()にnullを渡した場合の挙動の差異

それでは、of()の引数にnullを渡した場合の挙動はどうなるのか。これはStreamとOptionalで異なっており、Streamのof()が、単に(nullが実体を持ったオブジェクトであるかのような表現には語弊があるが)「null 1つ」を要素とするStreamを返すのに対し、Optionalのof()NullPointerExceptionをスローする*2。下記のコードで実際に動作を確認できる。

/* JUnit5を利用 */
@Test
void Streamのofはnullを含むStreamを返す() {
    List<Object> list = Stream.of((Object)null).collect(Collectors.toList());
    assertEquals(1, list.size()); // 要素数は1
    assertNull(list.get(0)); // その要素はnull
}
@Test
void Optionalのofは例外をスローする() {
    final String helloWorld = "Hello World";
    Optional<Object> opt = Optional.of(helloWorld);
    Exception exception = null; // スローされたら格納しておく
    try {
        opt = Optional.of(null);
        fail("例外がスローされるので到達したらいけないコード");
    } catch (Exception e) {
        exception = e;
        assertEquals(NullPointerException.class, e.getClass());
    }
    assertEquals(helloWorld, opt.get()); // 当初の値のままであるはず
    assertNotNull(exception); // 例外がスローされていることを確認
}

その他の違い

以上のように、of()にnullを渡した際の挙動はStreamとOptionalとで異なっている。その他に、異なる点としては、

  • Optionalのof()には(Optionalの性質上)単一の引数のものしか存在しないのに対して、Streamのof()にはof​(T... values)という可変長引数のものも存在している。
  • これら6つのメソッドのうち、StreamのofNullable()だけは導入されたバージョンがJava 9であり、一世代新しい。

という点が挙げられる。

感想と次回予告

じっくりと仕様と実装を見てみると、普段いかに雰囲気で使ってしまっているかに気づくことができた。とはいえ、ファクトリはファクトリでしかないので、それぞれの生成の仕方にどのような意味があるのかは、生成が利用される場面とセットでなければわかりにくい。次の記事では、map()flatMap()を紹介しながら、今回の記事で扱ったファクトリをどのように利用するかにも言及したい。

*1:また、インスタンスメソッドについてはdefaultキーワードを用いてデフォルト実装を定義することもできるようになっている。

*2:スローするのはof()の中で呼び出しているOptionalのコンストラクタ、更に言えばそこで呼び出しているObjects.requireNonNull()だ。

StreamとOptionalに共通して存在するメソッド:filter(Predicate<? super T> predicate)

Java 8 で導入されたStreamとOptional。業歴は浅いのに古いJavaで学んでしまったので、いざ使える状況になったにもかかわらずいまいち使い方がよくわかっておらず、書籍やWeb上の資料で知識を補っている最中だ。

調べているうちに、Stream(インタフェース)とOptional(クラス)との間には、(名称が)共通のメソッドがいくつか存在することに気が付いた。APIドキュメントを見比べると、以下のものがそれに当たる。

  • filter()
  • flatMap()
  • map()
  • of()
  • ofNullable()

StreamとOptionalについての勉強の手始めに、これらについて調べてみることにした。これらのうち、filter()of()ofNullable()は引数の数と型も一致(=シグネチャが一致)している。今回は、このうちのfilter()について、StreamのそれとOptionalのそれとの比較を試みる。

述語を満たす要素を残す

filter()の引数は、いずれもPredicate<? super T> predicateだ。Predicateは関数型インタフェースで、ひとつの入力に対してbooleanの判定結果を返すtest()メソッドを持つ。具体的な事例では、下記のような形で用いられる。

number.filter( n -> n % 2 == 0) // number は Optional or Stream

上のコードでは、numberの要素に対して、それが偶数であるかを判定している。判定結果がfalseとなる場合、Optionalでは判定結果がfalseであれば空のOptilnal(Optional.empty()の返り値)が返却されるのに対し、Streamでは当該要素を除いた要素で新たなStreamが返却される。

いずれにおいても、判定結果がfalseになる要素を除外する(trueになる要素のみを残す)という意味合いは共通していることがわかる。OptionalもStreamも中に要素を持つ「入れ物」としてイメージできるが、その要素をなんらかの基準によって選別するという機能をfilter()は持っている。

OptionalとStreamの違い

ただし、両者の間には当然ながら違いもある。「入れ物」としてのOptionalとStreamの違いは、Optionalは要素を1つしか持たないのに対し、Streamは0から多数の要素を持つことができる、という「要素の数」の違いとして整理しても間違いではないと思う。しかし、filter()の挙動からすると、両者の間には要素の数以上の違いがあることも見えてくる。

Optionalは、1つの要素を持つというよりは、1つを収めるスペースを持っていると表現すべきだろう。判定結果がfalseの場合のfilter()の返り値は、空のOptional、すなわち「1つ分のスペースが空席の」Optionalである。そこには、空席にはなっているものの、要素1つ分のスペースが確かに残っている。これに対し、Streamのfilter()は要素の数が単純に減った新しいStreamが返される。filter()を受ける前に存在していた要素の痕跡は残らない。

次回予告

今回は、filter()について調べた。言及したOptionalとStreamの違いは、その他の共通のメソッドにも反映されているはずなので、これからまた調査したい。

関数型プログラミング未経験者がREPLでClojureに触ってみた

下記の記事を読んでいたら、Clojureというプログラミング言語の名前がちょこちょこと登場しているのが目に入り、少し調べてみることにした。

techracho.bpsinc.jp

Clojureとは

Clojureとはどんな言語なのだろうか。Wikipediaをみてみる。

Clojure (発音は/'klouʒər/[2], クロージャー)はプログラミング言語であり、LISP系の言語の方言の一つである。関数型プログラミングのプログラミングスタイルでのインタラクティブな開発を支援し、マルチスレッドプログラムの開発を容易化する汎用言語である。

LISP系の言語の方言」とある。LISPというのは名前だけ聞いたことがある程度だ。プログラミング言語で「方言」というと、SQL方言が真っ先に思い浮かぶが、LISPというのも標準語と複数の方言が存在する言語なのだろうか、とLISPについて調べ始める。なるほどLISPというのは歴史のある言語で、計算機プログラムの構造と解釈』(SICPという有名な教科書でも「Scheme」というLISP方言の言語が使われているようだ。

SICPは、プログラマ、というか計算機科学に関わるならば読んでおきたい書籍という扱いがされているようだ。日本語版のPDFでも700ページを超える重厚なもので、読み切るのは大変だろうが、とても勉強になりそうというのもまた確か。感想記事にはたくさんのはてブが付いている。いつか読んでみたい。

kinokoru.jp

と、やっているとキリがないので、本題に戻って、Clojureを少し触ってみることに。しっかり環境構築をしようと思うと最初で挫折するので、手軽に試してみようと考え、公式ドキュメントで紹介されているREPLによる入門をやってみることにした。

Clojureの対話型実行環境「REPL」

様々な言語にREPLが存在するが、ClojureにもREPLがある。これは、ClojureのCLIツールに同梱されているので、簡単に使い始めることができる。Mac環境では、ClojureをHomebrewでインストールすれば使えるようになる。

$ brew install clojure
$ clj
Clojure 1.10.0 
user=> 

これで、REPLが立ち上がり、Clojureのプログラミングがでできるようになっている。下記の公式ドキュメントにしたがって、いくつか操作を試してみる。

パッとみて、括弧が激しく使われるなぁ、と思っていたら、REPLでは)を入力したときに対応する(がハイライトされるようだ。これで少しは助かる。

f:id:ky_yk_d:20190126160330g:plain

Clojureでは+も関数

さて、手始めに足し算か、と思ったら、足し算からしてまず違う。演算子は前置記法で書く。

user=> (+ 2 3)
5

これは、関数と同じ書き方になるということでもある。というか、Clojureでは+は立派な関数らしい。Javaがメインのプログラマにとって、演算子は通常のメソッドとは違う特別な存在だが、このClojureでは演算子も関数として定義されている。C++では演算子オーバーロードできるし、プログラミング言語によって、同じような機能を持っているものの位置付けが違うのは面白いなと思う。

関数を定義するdefnは「マクロ」

次は関数の定義。ここでは階乗の関数を定義している。

user=> (defn factorial [n]
(if (= n 0)
  1
  (* n (factorial (dec n)))))
#'user/factorial
user=> (factorial 10)
3628800

defnというのは関数ではなく、「マクロ」というものらしい。C言語から入っているので、マクロというと、#defineなのだけど、LISPのマクロは一味違うらしい。なるほどわからん

ifは評価値を持つ「特殊形式」

また、面白かったものとして、ifがある。C言語Javaで制御構造の一つをなしているifが「文」を構成する(評価された値を持たない)のに対し、Clojureifは関数のように評価された値を持つ「式」ということになる*1Clojureifは厳密には関数ではなく、「特殊形式」というものに当たるらしい。

Clojureifは、(if test then else)という書き方をして、testの評価値がnilfalseでない場合はthenを評価し、nilfalseの場合はelseを評価するというものになっている。関数的な言い方をすると、引数を3つとるわけだが、これはC言語Javaの条件演算子Hoge ? Fuga : Piyoに似ている。

CとJavaを学んだときは、「なんだよこれわかりにくいな、なんでこんなんあるんだ」と感じ、最近は、「これ使うと行数少なく書ける」くらいに感じていた条件演算子だったが、関数型の世界でのifがこのような形になるのはなるほど自然で、にわかに条件演算子が由緒正しいものに思えてきた。

APIドキュメントを表示するマクロdoc

また、REPLには、docという便利な機能がある。(doc name)とREPLで入力すると、nameで指定したもののAPIドキュメントが表示される。いくつか例を示す。

f:id:ky_yk_d:20190126225447p:plain
「+」の説明も存在する

f:id:ky_yk_d:20190126225520p:plain
「doc」自体はマクロ

この機能があれば、REPLを使っていて関数などの使い方がわからなくなったときも、わざわざブラウザでドキュメントを見る必要がない。とても便利な機能だと感じた。

Clojureにおける「REPL駆動開発」

このdocをはじめとして、ClojureのREPLは単なる「入門用の遊び場」に止まらない機能を有している。そのためか、Clojureにおいては「REPL駆動開発」が書籍でも言及されているほどらしい。

感想

プログラミング言語は、C言語Java(Stream API不使用)と学び始めて、それからある程度書いたことがあるのもPythonRubyJavaScript(TypeScript)あたりで、関数型言語は全く経験がない。Scala去年の8月に入門的レベルをほんの少しだけ触ってそのままになっていて、本格的に関数型らしいところまでたどり着かなかったので、今回は初っ端から関数型らしさを感じられて面白かった。

現在、約1年振りにJavaを仕事で書くことになり、前回は使わなかったStream APIも使い始めている。先日のJJUGのイベントで、GoFデザインパターンの「振る舞いに関するパターン」をラムダ式で置き換えるというライブコーディングを見たこともあり、関数型プログラミングに対する関心が高まっている。オブジェクト指向もよくわかっていないのだけれど、せっかくStream APIを使えるのだから、関数型の考え方も取り入れながらプログラムが書けたらいいなと考えている。

計算機プログラムの構造と解釈 第2版

計算機プログラムの構造と解釈 第2版

プログラミングClojure 第2版

プログラミングClojure 第2版

*1:触ったことがある言語では、RubyScalaのifも「式」だった。