社内se × プログラマ × ビッグデータ

プログラミングなどITに興味があります。

java.util.stream のソースを読んでみる filter 編

stream については、package java.util.stream; の JavaDoc には以下のように書かれています。

 * A sequence of elements supporting sequential and parallel aggregate
 * operations.

並んでいる要素に対して、直列または並列処理をサポートするみたいなイメージでしょうか。

そもそも、Java8 ではなぜ List から stream() を使って Stream に変換が出来るのだろう?
> Collection インタフェースに stream() が用意されていて、List ではそれを継承しているようです。
https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html

では、filter の JavaDoc を見てみます。

    /**
     * Returns a stream consisting of the elements of this stream that match
     * the given predicate.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *                  <a href="package-summary.html#Statelessness">stateless</a>
     *                  predicate to apply to each element to determine if it
     *                  should be included
     * @return the new stream
     */
    Stream<T> filter(Predicate<? super T> predicate);

うーん。Predicate ですか。
では、Predicate の JavaDoc を見てみます。

/**
 * Represents a predicate (boolean-valued function) of one argument.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #test(Object)}.
 *
 * @param <T> the type of the input to the predicate
 *
 * @since 1.8
 */

これだけでは、ピンと来なかったですが、用意されているメソッドを見てみると、test(T t) というものがあります。

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

与えられた引数が条件(直訳だと述語?)に一致した場合は、true 一致しない場合は false を返します。
interface Predicate は他にもメソッドが定義されていますが、最終的にこの test(T t) が呼ばれるようです。

つまり、Stream filter(Predicate predicate); の中の Predicate predicate に関しては
与えたデータ型 T が boolean に変換されるとも捉えられるので、
Stream filter(T -> boolean);
こんな感じに変換(おおざっぱに)捉えることにします。

例として、

List<String> before = Arrays.asList("a", "b", "c");
List<String> after = before.stream().filter(e -> e.equals("b")).collect(Collectors.toList());
after.forEach(System.out::println); // b

filter(T -> boolean) と filter(e -> e.equals("b")) のような関係にこじつけられます。

JGiven のレポート機能を試す(Java, JUnit)

JGiven ではいくつかのレポート機能が用意されているようです。

テキストのレポートを無効にする

デフォルトでは、コンソール上にテキストのレポートが表示されます。

 Something should happen

   Given サーバーが止まっている時に
    When プログラムを実行すると
    Then エラーになる

しかしながら、これを Java system property にて無効化することが出来ます。

$ mvn test -Djgiven.report.text=false

わざわざ無効化する用途は思い当たらないのですが。

JSON のレポートを無効にする

デフォルトでは、jgiven-reports ディレクトリ以下に JSON ファイルでテスト結果のレポートが出力されます。
私の環境では、eclipse 上からテストを実行した場合、project/jgiven-reports にレポートが出力されました。
mvn test でテストを実行した場合、target/jgiven-reports/json にレポートが出力されました。
後者については、Maven surefire plugin を用いた場合の挙動になるようですが、あまり理解できていません。
しかしながら、これも無効にすることが出来ます。

$ mvn test -Djgiven.report.enabled=false

この property を指定した場合、コンソール上へのテキスト形式のレポートも表示されなくなります。
※レポート機能自体が無効化される
これも、わざわざ無効化する用途は思い当たらないのですが。

JSON のレポート出力先を変更する

/tmp に変更しています。json のテキストファイルが直接出力されます。

$ mvn test -Djgiven.report.dir=/tmp

HTML のレポートを出力するために

このような感じのレポートが出力できるようです。
まず、JSON のレポート出力は必須とのこと。
そして、Given report generator と呼ばれるツールを html フォーマットを指定して実行すれば出力してくれるようです。
このツールは、jgiven-html5-report と呼ばれるモジュールの一部らしいです。

java com.tngtech.jgiven.report.ReportGenerator \
  --format=html \
  [--sourceDir=<jsonreports>] \
  [--targetDir=<targetDir>] \

maven プラグインを使えば、このようなコマンドを別途実行してあげる必要もなさそうです。
pom ファイルで指定するためには、

<build>
  <plugins>
    <plugin>
      <groupId>com.tngtech.jgiven</groupId>
      <artifactId>jgiven-maven-plugin</artifactId>
      <version>0.17.0</version>
      <executions>
        <execution>
          <goals>
            <goal>report</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <format>html</format>
      </configuration>
    </plugin>
 </plugins>
</build>

注意点は、reporting の配下ではなくて、build の配下に設置すること。

そして、mvn verify コマンドを実行しますと、以下のようにレポートが target/jgiven-reports/html 配下に自動的に生成されます。

[INFO] --- jgiven-maven-plugin:0.17.0:report (default) @ practiceJava ---
[INFO] JGiven HTML report source directory: /home/xx/workspace/Java/target/jgiven-reports/json
[INFO] JGiven HTML report output directory: /home/xx/workspace/Java/target/jgiven-reports/html
[INFO] Generating HTML reports to /home/xx/workspace/Java/target/jgiven-reports/html...
[INFO] Generating HTML5 report to /home/xx/workspace/Java/target/jgiven-reports/html/index.html
[INFO] Written 1 scenarios to data0.js
[INFO] Cannot read /home/xx/workspace/Java/src/test/resources/jgiven/custom.css, skipping
[INFO] Cannot read /home/xx/workspace/Java/src/test/resources/jgiven/custom.js, skipping
[INFO] -------------------------------------------------------------------
[INFO] Generated JGiven HTML reports to directory /home/xx/workspace/Java/target/jgiven-reports/html
[INFO] -------------------------------------------------------------------

ブラウザで開いてみると、こんな感じに見れます。

f:id:blueskyarea:20181128233126p:plain
sample_jgiven_report

まとめ

レポート機能は HTML (ブラウザで確認)のためにあるような印象です。
今までテスト結果をコンソール上や、IDE上でのみか確認出来なかった場合、外部向けに説明するには何らかの加工をしてあげる必要がありましたが、ブラウザ上でのレポートであれば見やすく、外部向けにも説明がしやすくなると思います。
ついでに、テストを書くのが楽しくなれば良いと思います。

JGiven をユーザーガイド見ながら試す(Java, JUnit)

JGiven とは

公式サイト から引用すると

JGiven は、開発者にフレンドリーで実用的な Java の BDD ツールです。
ドメインエキスパートにとって読みやすいレポートを生成してくれます。

JGiven を使おうとしたきっかけ

Java のプロジェクトに関しては、普段 JUnit を使用してテストを書いていますが、以下のような問題に悩まされています。
1. 既存テストの要点を掴むのが難しいことがある
他人が書いたテストコードはもちろんのこと、自分で書いたテストですら数ヶ月も経つと、テストの要点(目的)などが分かり難くなってしまっていることがあります。
テスト内でテストデータを作ったり、条件を作ったりしていると、それをトレースするのが大変になったりします。

2. テストのレポートがわかりにくい
JUnit でテストを実行した結果、例えその結果が全てパス(成功)だったとしても、どういった仕様を満たしているのかわかりにくい場合があります。
特にテストケースが数十、百以上になってきたりすると、どのようなテストケースがあるのか把握するのが難しくなります。

3. 開発者(プログラマー)しか読めない
上記と類似することですが、テストの仕様を理解するのに、プログラム(ソースコード)を読まなければならない場合があるため、開発者以外が仕様を理解することが難しくなります。
場合によっては、開発者以外のために、別途ドキュメントを用意する必要があります。

結局のところ、以下の2点が満足できれば、これらの問題を解消する手助けになりそうです。
1. 読みやすいテストが書けること
2. 分かりやすいレポートが出力されること
これらが、JGiven フレームワークに期待しているポイントになります。

ユーザーガイド

それでは、ユーザーガイドに沿って、インストールから最初の実行までやってみます。
JGiven User Guide

インストール

JUnitmaven を使用しているので、以下を pom に指定してあげます。

<dependency>
	<groupId>com.tngtech.jgiven</groupId>
	<artifactId>jgiven-junit</artifactId>
	<version>0.17.0</version>
	<scope>test</scope>
</dependency>

JUnit テストクラスを作る

何はともあれ、テストクラスがなければ話が始まらないので作ります。
JUnit の場合は、ScenarioTest を継承する必要があるようです。
更に ScenarioTest は3種類のパラメータを要求しており、それぞれ Given-When-Then と呼ばれます。

import com.tngtech.jgiven.junit.ScenarioTest;

public class MyShinyJGivenTest
        extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
}

日本語で言うと、以下のようなステージ構成になるようです。
Given : 何かの状態のとき
When : 何かをしたら
Then : 何かの結果になる

これを上手く表現することが出来れば、そのまま仕様のように読み取れるかもしれません。

Given, When, Then クラスを作る

それぞれのステージを構成するためのクラスを生成します。
Given

import com.tngtech.jgiven.Stage;

public class GivenSomeState extends Stage<GivenSomeState> {
    public GivenSomeState some_state() {
        return self();
    }
}

When

import com.tngtech.jgiven.Stage;

public class WhenSomeAction extends Stage<WhenSomeAction> {
    public WhenSomeAction some_action() {
        return self();
    }
}

Then

public class ThenSomeOutcome extends Stage<ThenSomeOutcome> {
    public ThenSomeOutcome some_outcome() {
        return self();
    }
}

Stage クラスの継承を必須としているわけではないそうですが、and() や self() などの有用なメソッドが提供されているため、推奨されているようです。
※何が有用なのか現時点では不明ですが

テストシナリオを書く

それではいよいよテストシナリオを書いていきます。
最初に作ったテストクラスに追記しています。

public class MyShinyJGivenTest extends ScenarioTest<GivenSomeState, WhenSomeAction, ThenSomeOutcome> {
    @Test
    public void something_should_happen() {
        given().some_state();
        when().some_action();
        then().some_outcome();
    }
}

ScenarioTest を継承したことにより、given(), when(), then() がそれぞれ指定したクラスのインスタンスに紐づいているようです。
なので、インスタンス.メソッド名 という形でメソッドの呼び出しが可能となっています。

実行してみる

実行してみると、以下のような結果が出力されました。
Given, When, Then は固定で、some xxx は、メソッド名(some_state など)から来ているようです。

Test Class: test.java.practice.bdd.jgiven.MyShinyJGivenTest

 Something should happen

   Given some state
    When some action
    Then some outcome

なので、例えば日本語でメソッド名を作成してみた場合、こんな感じの出力が期待できます。
どんなテストをやっているのか分かりやすいですね。

   Given サーバーが止まっている時に
    When プログラムを実行すると
    Then エラーになる

もし ScenarioTest を継承することができない場合

すでに他のクラスを継承しているなどで、ScenarioTest を継承することができない場合は、JGiven の JUnit rule を使用すれば良いとのこと。
具体的には、JGivenClassRule と JGivenMethodRule を定義すること。
Given-When-Then それぞれのクラスを @ScenarioStage アノテーションを用いて注入してあげること。

public class UsingRulesTest {

    @ClassRule
    public static final JGivenClassRule writerRule = new JGivenClassRule();

    @Rule
    public final JGivenMethodRule scenarioRule = new JGivenMethodRule();

    @ScenarioStage
    GivenSomeState someState;

    @ScenarioStage
    WhenSomeAction someAction;

    @ScenarioStage
    ThenSomeOutcome someOutcome;

    @Test
    public void something_should_happen() {
        someState.given().サーバーが止まっている時に();
        someAction.when().プログラムを実行すると();
        someOutcome.then().エラーになる();
    }
}

ScenarioTest のソースを見てみると、似たようなことをしているので、継承する代わりに自分たちで定義してあげるような格好になっていると思われます。

public class ScenarioTest<GIVEN, WHEN, THEN> extends ScenarioTestBase<GIVEN, WHEN, THEN> {
    @ClassRule
    public static final JGivenClassRule writerRule = new JGivenClassRule();

    @Rule
    public final JGivenMethodRule scenarioRule = new JGivenMethodRule( createScenario() );

    @Override
    public Scenario<GIVEN, WHEN, THEN> getScenario() {
        return (Scenario<GIVEN, WHEN, THEN>) scenarioRule.getScenario();
    }
}

まとめ

以上、ユーザーガイドに沿って試行してみましたが、当初期待していた2点
> 1. 読みやすいテストが書けること
> 2. 分かりやすいレポートが出力されること
について、JGiven が手助けになる可能性を感じることができました。
もちろん現時点では、テストケースも少なくシンプルなので分かりやすいですが、実際にシナリオが増えてきた場合に、上手くシンプルな状態を保ったまま管理が出来るかは分かりません。
とは言え、テスト結果が以下のように表示されるだけでも強力です。

   Given サーバーが止まっている時に
    When プログラムを実行すると
    Then エラーになる

たとえば、以下のように Given だけ差し替えることも容易に出来ると思われます。

   Given ネットワークが止まっている時に
    When プログラムを実行すると
    Then エラーになる

レポート出力に関しては、機能が充実しているようなので、機会があれば試行してみたいと思います。

terminator(端末)が起動しなくなった except (KeyError,ValueError)

Ubuntu14.04 上でいつの間にか terminator が起動しなくなりました。
デスクトップ上のランチャーを叩いても、何の応答もありません。
仕方なく、他の端末(XTerm)を起動し、/usr/bin/terminator を直接実行してみたところ、以下のエラーが表示。

  File "/usr/bin/terminator", line 103
    except (KeyError,ValueError), ex:

SyntaxError: invalid syntax

SyntaxError ? なぜ、terminator のソースをさわってもいないのに突然そのようなエラーが出るのか。
答えは、python 3 をデフォルトに設定していたためでした。
※確かに、この問題が発生する直前に変更していました

/usr/bin/terminator の1行目に、#! /usr/bin/python と指定されています。
そのため、python 3 で terminator を起動するようになったと思われますが、それが互換性がなくて SyntaxError になってしまったと思われます。

回避策として、1行目を #! /usr/bin/python2 として、python2を適用することを明示すれば、問題なく起動するようになりました。
この問題は初めて遭遇しましたが、前々からよくありそうですし、報告されています。
bugs.launchpad.net

タイガースとジャイアンツが含まれるツイート件数をカウントしてみた

データ取得期間

2018/11/11 - 11/13 内の数時間

データ取得方法
  • Twitter API(検索) を叩くバッチを1分間隔で実行。
  • 検索条件のキーワードとして、”タイガース” もしくは、”ジャイアンツ”が含まれること。
  • リツイートは取得対象外
  • たまに単に球団名が列挙されているツイートが存在するが、特に除外はしていない(取得対象内)
集計方法

Apache spark の spark-shell を使う。
とは言っても、特別な分析ライブラリを使用するわけでもなく、単にRDDとして取り込んで極めて基本的な関数を呼び出すだけ。

集計ログ

取得した総ツイート件数

scala> val rawData = sc.textFile("タイガース_ジャイアンツ.txt")
rawData: org.apache.spark.rdd.RDD[String] = タイガース_ジャイアンツ.txt MapPartitionsRDD[19] at textFile at <console>:27

scala> rawData.count
res24: Long = 1650

ツイートにタイガースが含まれる件数

scala> def isTigers(line: String): Boolean = line.contains("タイガース")
isTigers: (line: String)Boolean

scala> rawData.filter(isTigers).count
res25: Long = 551

ツイートにジャイアンツが含まれる件数

scala> def isGiants(line: String): Boolean = line.contains("ジャイアンツ")
isGiants: (line: String)Boolean

scala> rawData.filter(isGiants).count
res26: Long = 1196

1つのツイートにタイガースとジャイアンツの両方が含まれているものもある。
しかし、ジャイアンツが含まれているツイート数がタイガースよりも、およそ2倍という結果に。

ツイートに矢野監督が含まれる件数

scala> def isYano(line: String) = line.contains("矢野監督")
isYano: (line: String)Boolean

scala> rawData.filter(isYano).count
res30: Long = 5

ツイートに原監督が含まれる件数

scala> def isHara(line: String) = line.contains("原監督")
isHara: (line: String)Boolean

scala> rawData.filter(isHara).count
res31: Long = 11

もちろん、これだけで世間の注目度を示しているわけではないし、集計期間や集計方法にも問題がたくさんあると思いますが、それにしても思っていた以上に数値に差が出る結果となりました。
もう少しサンプルデータを増やした上で、別の角度からも集計をしてみたい。

grep -E が正規表現対応オプションであると勘違いしていた

grep って、global regular expression print の略なので、それ自体が正規表現で一致したものを抽出するコマンドなんですね。
正規表現を使いたい時に、-E オプションを付けるものだと勘違いしていました。

いま、以下のように2行が書かれたファイル(test.txt)があるとします。

$ cat > test.txt
1024
hatena
$ grep ^1 test.txt 
1024

はい。特に -E オプションを付けなくても正規表現でマッチしてくれました。

ところが。

$ grep '^(1|h)' test.txt

これは何も返してくれません。

そこで、-E オプションを付けてみると。

$ grep -E '^(1|h)' test.txt 
1024
hatena

期待する結果を返してくれました。
E オプションをつけることで、拡張正規表現と呼ばれる式が使用出来るようになります。

egrep でもOKです。

$ egrep '^(1|h)' test.txt 
1024
hatena

拡張正規表現を使える = 正規表現を使えると間違って覚えてしまっていたようです。
何気に使用しているオプションについて、意味を正しく理解していないのは宜しくないですね。

開発者が知っておくべき Couchbase についての 10項目

はじめに

公式ブログによって、こうやってまとめておいてくれると、読みやすいし初学者にとって助かります。
blog.couchbase.com

第10位
Document access in Couchbase is strongly consistent, query access is eventually consistent

Couchbase のデータには強い一貫性があると主張されています。
key / value アクセスなので、そこは保証されやすいところなのかと思います。
View (恐らくインデックス)についても、最終的には一貫性が保たれるということですが、逆に言うと一貫性がない瞬間(インデックスが生成されるまで)もあるということですね。

第9位
Writes are asynchronous by default but can be controlled

書き込み処理は基本的に非同期で行われるようです。
レプリカの生成と、データの永続化(メモリからディスクへの書き込み)はバックグラウンドで行われ、クライアント側はその結果の通知を受け取れるようです。
クライアント側で先に通知を受け取るか、通知よりも先に非同期にレプリカを作成させるかなどはクライアント側で選べるようです。

第8位
Couchbase has atomic operations for counting and appending

カウント処理と追加処理において、不可分操作をサポートしているようです。
例にあるように、incr は書き込み処理と結果を返す処理を行います。
要は追加に失敗したのに、追加されたものとしてカウントされてしまったりすることは無いということかと思います。

cb.set(“mykey”, 1)
x = cb.incr(“mykey”)
puts x #=> 2

第7位
Start with everything in one bucket

Bucket というものがデータベースのようなものであり、RDBMSでいうところテーブルというわけではない。
そのため、RDBMSからそのままデータを移すとしたら、複数のテーブルが1つのBucket に全て投入されることになります。
ただし、"typte" という属性が恐らくデータ毎に設定が可能であるため、それを設定することで同じBucket内にあるデータであっても差別化ができるようです。
とにかく1つのBucketでスタートすることが推奨されているようですね。

第6位
Try to use 5 or less buckets in Couchbase. Never more than 10.

データは固定のスキーマを持つわけではないので、色々なスキーマのデータを同じBucketに入れることが可能。
ソフトウェアとして限界値は定められていないものの、10 Bucketにもなると、CPUやDiskIOに問題が発生することが確認されているようです。
この辺りは Couchbase のバージョンアップによって、改善される可能性はあるかもしれませんが、出来る限り少ない数の Bucket で運用する方が良さそうですね。
まぁ、もしどうしても Bucket をたくさん作って管理を分けたいのであれば、別のクラスタに切り離してしまった方が良いのかもしれません。

第5位
Use CAS over GetL almost always

CAS は "Check and Set"の略のようです。
KVSにおけるトランザクション処理を行うために必要な操作のようです。
要は楽観的ロックと悲観的ロックの話をしていますが、基本的に楽観的ロックを使用するべきだということでしょうか。
Couchbaseにおけるロック機能というものを知らないので、これ以上は理解が出来ていません。

第4位
Use multi-get operations

Couchbase には、キーのリストから複数のレコードを同時に?検索できるようです。
個々のキーを一つずつ検索するよりも、高パフォーマンスが期待できるようです。

第3位
Keep your client libraries up-to-date

これは、Couchbase に特化した話でもないと思いますが、使用するライブラリは最新のものが良いよということですね。
バージョンアップが頻繁に行われている製品であれば、何であっても同じことが言えると思います。

第2位
Model your data using JSON documents

格納するデータはJSON形式で。
CouchbaseはJSON形式や、バイナリ形式のドキュメントをサポートしていますが、まずはJSON形式で試すことを推奨しているようです。
JSON形式で保存しておくと、インデックスが作成できたり、特殊な?クエリを投げられるなどメリットがありそうです。

第1位
Use indexes effectively

インデックスを効果的に使うこと。
出来る限り、プライマリキーでのアクセスを行うこと。キーとメタデータをメモリ上に持っているので、アクセスは速いはず。
セカンダリ・インデックスでのアクセスは、パフォーマンスを必要としない分析用などで使用するべきとのこと。

この時点で、セカンダリ・インデックスのパフォーマンスは、それ程期待しない方が良いのかもしれません。
4つの design documents と、1つの design documents あたり、view は10個以下に抑えること。
それでも多いようなきもしますが。
インデックスデータに対して、何の "reduce" 処理を行う必要がないのであれば、value に "null" を設定しておきべきとのこと。

おわりに

これを読んだだけでは表面的な部分しか分かりませんが、使用していく内に「あれはそういう意味だったのか」のように、それぞれの意味をより深く知ることが出来るのかもしれません。
”Use indexes effectively” というのが一見当たり前のように見えて、Couchbase を使う上で実はとても大切なことのような気がします。

新しく使用する技術に対して、「取り合えず使ってみる」アプローチと、「概念をまず理解する」アプローチどちらが良いかは人それぞれかと思います。
自分の場合、取り合えず使ってみて、分からないところが出てきたら調べることが多いように思います。
ただ、エラーを回避することに精一杯になってしまって、結局その製品のことをあまり理解できていない事に後で気づくので、手を少し止めてインプットする時間も意識して作れるといいなと思います。