groongaを使う

昨日はいきなりマルチスレッドとかいい始めてしまいましたがやっぱちゃんと書かないとだめだなと気持ちを改めました…

インストー

http://groonga.org/ja/docs/install.html
こちらを参照。WindowsMacLinux各種で使える

ソースコードからビルド

昨日書いたとおりだが、githubからクローンしてくる

$ git clone --recursive git@github.com:groonga/groonga.git
$ cd groonga
$ cmake .
$ make && sudo make install

installしたくない場合はmake installは不要

アクセス方法

ちょっとよくわかってないんだが、groongaへのアクセス方法は3つある

デフォルトではGQTPが使われるらしい。このGQTPはマルチスレッド対応しており(HTTPとかもしてるけど)、特に指定しなくてもマルチスレッドで使用できる。なおサーバーを特に起動させずスタンドアロンで使用する場合もマルチスレッドで動くので、DBをopenしたらスタンドアロンサーバーが立ち上がってるんじゃないかな?よくわかんないけど。

HTTPはポートがデフォルトだと10041みたいなので自分で適当なポートを設定したい時は

$ groonga -p 8085 --protocol http -d /tmp/test.db

などとする。 -dオプションはデーモン化するやつ。

DBを作成する

DBはファイルベースになっている。簡単なのはコマンドから作るやつ

$ groonga -n /tmp/test.db

デフォルトだと/var/lib/groonga/db/に作られる…のかな?サーバー立ち上げると特に指定しない場合はそこにできるみたい。

Cのソースコードから作る場合は下記の通り。

#include <groonga.h>

int main(void)
{
    grn_ctx ctx;
    grn_obj* db;
    grn_init();
    
    grn_ctx_init(&ctx, 0);
    //オープンする。なかったら作る
    GRN_DB_OPEN_OR_CREATE(&ctx, path, 0, db);
    //確実にある場合はオープンするだけでもよい
    //db = grn_db_open(&ctx, path);

    grn_obj_close(&ctx, db);
    grn_ctx_fin(&ctx);
    return 0;    
}

テーブルを作成する

色々とオプションはあるのだが多分簡単に使うだけなら、もととなるテーブルと全文検索用インデックステーブルさえ作っておけば良さそう。
ただし全文検索用インデックスはカラムに対して紐付けを行うので、テーブルを作成する時はnormalizer_typeとtoken_bigramだけをしていしておけばよい。

コマンドラインで作る時は

> table_create --name "main_table" --flags "TABLE_HASH_KEY" --key_type "UInt32"
> table_create --name "index_name" --flags "TABLE_PAT_KEY_TYPE" --key_type "SHORT_TEXT_TYPE" --normalizer "NormalizerAuto" --default_tokenizer "TokenBigram"

おまじないみたいなもんだとおもっとけ。キーのタイプだけは指定しないといけないのとインデックス作ろうと思うとHASH_KEYタイプにしなきゃいけないみたいだが、インデックスいらないならキーのないタイプもあるのでそのあたりはよく読もう。
オープン・クローズするソースコードにテーブル作るのを足してみる。ちなみに存在確認用のAPIはないので自分で工夫しないといけない

bool create_grn_table(grn_ctx* ctx, char *_name, char *_flags, char *_key_type, char *_norm, char *_token)
{
    grn_obj *command, *name, *type, *flag, *norm, *token;
    char* result;
    uint32_t result_length;
    int32_t recv_flags;
    
    //grn_ctx_getで存在しない名前を指定した場合はNULLがかえる。これを利用して存在チェックする
    if(!grn_ctx_get(ctx, _name, strlen(_name))
    {
        command = grn_ctx_get(ctx, "table_create", strlen("table_create"));
        name = grn_expr_get_var(ctx, command, "name", strlen("name"));
        flag = grn_expr_get_var(ctx, command, "flags", strlen("flags"));
	type = grn_expr_get_var(ctx, command, "key_type", strlen("key_type"));
	norm = grn_expr_get_var(ctx, command, "normalizer", strlen("normalizer"));
	token = grn_expr_get_var(ctx, command, "default_tokenizer", strlen("default_tokenizer"));

	grn_obj_reinit(ctx, name, GRN_DB_TEXT, 0);
	grn_obj_reinit(ctx, flag, GRN_DB_TEXT, 0);
	grn_obj_reinit(ctx, type, GRN_DB_TEXT, 0);
	grn_obj_reinit(ctx, norm, GRN_DB_TEXT, 0);
	grn_obj_reinit(ctx, token, GRN_DB_TEXT, 0);

	GRN_TEXT_PUTS(ctx, name, _table_name);
	GRN_TEXT_PUTS(ctx, type, _key_type);
	GRN_TEXT_PUTS(ctx, flag, _flags);
	GRN_TEXT_PUTS(ctx, norm, _normalizer);
	GRN_TEXT_PUTS(ctx, token, _tokenizer);

	grn_expr_exec(ctx, command, 0);
	grn_ctx_recv(ctx, &result, &result_length, &recv_flags);
	grn_expr_clear_vars(ctx, command);
        //unlinkでメモリから解放しておかないとメモリーリークが発生する。
        //再帰的に開放されるのでgrn_ctx_getで取ってきたオブジェクトだけで良いみたい
        grn_obj_unlink(ctx, command);
	return memcmp(result, "true", result_length) == 0;        
    }

    return true;

}


int main(void)
{
    grn_ctx ctx;
    grn_obj* db;
    grn_init();
    
    grn_ctx_init(&ctx, 0);
    //オープンする。なかったら作る
    GRN_DB_OPEN_OR_CREATE(&ctx, path, 0, db);
    //確実にある場合はオープンするだけでもよい
    //db = grn_db_open(&ctx, path);

    //create table
    create_grn_table(&ctx, "main_table", "TABLE_HASH_KEY", "UInt32", "", "");
    create_grn_table(&ctx, "index_name", "TABLE_PAT_KEY", "ShortText", "NormalizerAuto", "TokenBigram");
    
    grn_obj_close(&ctx, db);
    grn_ctx_fin(&ctx);
    return 0;    
}

この時点ではテーブルには_idと_keyがある(かくれて_scoreとかもあるが)。まだインデックステーブルとテーブルの関係性はできていないので次はカラムをつくる

なお、grn_table_createというAPIもあるのでこっちを使っても良い。が、noramlizerオプションとかdefault tokenをどうやって指定するのかよくわからない

カラムをつくる

基本的にはスカラー型でつくるのだが、インデックスの場合だけは指定する型が違う。あと関連をもたせたりとかもしないといけない。実はカラムってのが重要なんじゃないか?

まずはメインテーブルにname, valueのカラムを追加する

> column_create --table "main_table" --name "name" --flags "COLUMN_SCALAR" --type "ShortText" 
> column_create --table "main_table" --name "value" --flags "COLUMN_SCALAR" --type "UInt32"

インデックステーブルにカラムを追加する
main_tableのnameは文字列なので全部検索のためにインデックスを作成してる。

> column_create --table "index_name" --name "index_name_col" --flags "COLUMN_INDEX|WITH_POSITION" --type "main_table" --source "name"

注意するのはtypeに指定するのがもととなるテーブル名、sourceにインデックスを作成したいカラム名を指定することくらいかな。これでmain_tableにデータを追加あすると自動的にindex_nameテーブルも更新されるようになる

Cで書くとこんな具合。

bool create_grn_table(char *_name, char *_flags, char *_key_type, char *_norm, char *_token)
{
(省略)
}

bool create_grn_column(grn_ctx* ctx, char* _table, char* _column, char* _flags, char* _type, char* _sources)
{
    grn_obj *command, *table, *name, *flag, *type, *source;
    char* result;
    uint32_t result_length;
    int32_t recv_flags;

    if(!grn_ctx_get(ctx, _column, strlen(_column))
    {
        command = grn_ctx_get(ctx, "column_create", strlen("column_create"));
        table = grn_expr_get_var(ctx, command, "table", strlen("table"));
        name = grn_expr_get_var(ctx, command, "name", strlen("name"));
        flag = grn_expr_get_var(ctx, command, "flags", strlen("flags"));
        type = grn_expr_get_var(ctx, command, "type", strlen("type"));
        source = grn_expr_get_var(ctx, command, "source", strlen("source"));

        grn_obj_reinit(ctx, table, GRN_DB_TEXT, 0);
        grn_obj_reinit(ctx, name, GRN_DB_TEXT, 0);
        grn_obj_reinit(ctx, flag, GRN_DB_TEXT, 0);
        grn_obj_reinit(ctx, type, GRN_DB_TEXT, 0);
        grn_obj_reinit(ctx, source, GRN_DB_TEXT, 0);

        GRN_TEXT_PUTS(ctx, table, _table);
        GRN_TEXT_PUTS(ctx, name, _column);
        GRN_TEXT_PUTS(ctx, flag, _flags);
        GRN_TEXT_PUTS(ctx, type, _type);
        GRN_TEXT_PUTS(ctx, source, _sources);

        grn_expr_exec(ctx, command, 0);
        grn_ctx_recv(ctx, &result, &result_length, &recv_flags);
        grn_expr_clear_vars(ctx, command);
        grn_obj_unlink(ctx, command);
        return memcmp(result, "true", result_length) == 0;
    }
    return true;
}


int main(void)
{
    grn_ctx ctx;
    grn_obj* db;
    grn_init();
    
    grn_ctx_init(&ctx, 0);
    //オープンする。なかったら作る
    GRN_DB_OPEN_OR_CREATE(&ctx, path, 0, db);
    //確実にある場合はオープンするだけでもよい
    //db = grn_db_open(&ctx, path);

    //create table
    create_grn_table(&ctx, "main_table", "TABLE_HASH_KEY", "UInt32", "", "");
    create_grn_table(&ctx, "index_name", "TABLE_PAT_KEY", "ShortText", "NormalizerAuto", "TokenBigram");

    //create column
    create_grn_column(&ctx, "main_table", "name", "COLUMN_SCALAR", "ShortText", "");
    create_grn_column(&ctx, "main_table", "value", "COLUMN_SCALAR", "UInt32", "");
    create_grn_column(&ctx, "index_name", "index_name_col", "COLUMN_INDEX|WITH_POSITION", "main_table", "name");
    
    grn_obj_close(&ctx, db);
    grn_ctx_fin(&ctx);
    return 0;    
}

こっちもgrn_column_createというAPIがある。ただしsourceの指定はどうやってするのかよくわからない。typeは指定できるんだが、、、

マルチスレッドでgroongaを使う

いきなりgroongaってなんぞやって感じですが、ElasticSearchみたいなもんです。ライブラリがCなのでCから高速に使いたいときはこっちを使うと楽です。日本人が開発してるので日本語完全対応もうれしい。もうちょっとドキュメント整理してほしいけど、、、

http://groonga.org/ja/docs/index.html

インストー

インストールは

sudo apt-get install groonga groonga-http groonga-server-gqtp

でできますが、詳しくはマニュアルを参照のこと。

(ちょっとおまけ)static ライブラリを作りたい

staticライブラリを作りたい時もありますよね?ありますよね?(白目
groongaはcmakeで楽ちんに作れますが、--help-property-listで出力してもstaticライブラリを作れそうなプロパティが出てきません。configureしてもいいけどどうしてもcmakeでやりたい。。。大丈夫。GRNG_EMBED=onとしてビルドすればできます。

まずはソースコードをダウンロードします。

git clone --recursive git@github.com:groonga/groonga.git

オプション付きでビルドします

cmake . -DGRNG_EMBED=on

libの下にlibgroonga.aができます。

ちなみにこれで作ったlibgroonga.aをリンクする際は一緒につくられるonigmoライブラリへのリンクもいっしょに追加しないといけません(onigmoを使わない場合は不要です)
例)

gcc -o sample main.c -L(groonga root)/lib -L(groonga root)/vendor/onigmo -lgroonga -ldl -lstdc++ -lonigmo -lz -lm

libstdc++, libmはbuild-essentialをインストールしたらだいたい一緒に入ると思うがlibdl, libzはlibltdl, zlib1gをインストールしないとだめみたい。

C-APIの基本的な使い方

C-APIは色々あるんだが、コマンドラインから使うのと同じ感じで使えるのが楽だと思う。というかまだよくわかっとらん。
使い方はこちらが詳しいです

qiita.com

マルチスレッドで使う

よーやく本題。
groongaはマルチスレッド対応で特になにか設定をする必要はないようなことを書いてあるが、よーく読むとgrn_ctxは各スレッドに対して一つ与えてやらないといけないらしい。
ためしにinsert/deleteをマルチスレッドでやるサンプルを書いてみます。

#include <stdlib.h>
#include <stdio.h>
#include <groonga.h>
#include <pthread.h>

#define DATA(str, key) sprintf(str, "[{¥"_key¥":¥"%d¥",¥"value¥":¥"value¥"}]", key);

grn_ctx ctx_insert;
grn_ctx ctx_delete;

grn_obj* open(grn_ctx* ctx, const char* path)
{
    grn_obj* db;
    int i = 0;
    //grn_ctxオブジェクトの初期化
    ctx = grn_ctx_open(0);
    grn_ctx_init(ctx, 0);
    //DBが存在しない場合は作成する。ある場合は開いてctxと関連付ける
    GRN_DB_OPEN_OR_CREATE(ctx, path, 0, db);
    //JSONで出力させる
    grn_ctx_set_output_type(ctx, GRN_CONTENT_JSON);
    return db;
}

int close(grn_ctx* ctx, grn_obj* db)
{
    grn_obj_close(ctx, db);
    return grn_ctx_fin(ctx);
}

int insert_record(grn_ctx* ctx, char* data)
{
    //レコードを追加する処理
}

int delete_record(grn_ctx* ctx, char* key)
{
    //レコードを冊書する処理
}

void *insert(void* arg)
{
    int i = 0;
    char str[128];
    for(i = 0; i < 100; i++)
   {
        DATA(str, i);
        insert_record(&ctx_insert, str);
   }
}
void *delete(void* arg)
{
    usleep(100);
    int i = 0;
    char index[2];
    for(i = 0; i < 100; i++)
    {
        sprintf(index, "%2d", i);
        delete_record(&ctx_delete, index);
    } 
}

int main(void)
{
    grn_init();
    grn_obj* db_insert = open(&ctx_insert, "db");
    //おなじDBなので同じオブジェクトを使い回せば良いのではと思ったが今のところうまく行ってないので
    grn_obj* db_delete = open(&ctx_delete, "db");

    //pthreadでスレッドを作り、joinさせて動かす
    pthread_t delete_th, insert_th;
    pthread_create(&insert_th, NULL, insert, NULL);
    pthread_create(&delete_th, NULL, delete, NULL);

    pthread_join(insert_th, NULL);
    pthread_join(delete_th, NULL);

   close(&ctx_insert, db_insert);
   close(&ctx_delete, db_delete);
   
    //一回で良い
    grn_fin();
    return 0;
}

割りと省略しまくりですが、大事なのはgrn_ctx, grn_dbをそれぞれのスレッドに対して作ること。

複数のクライアントからThriftサーバに接続する方法(C++)

あんま書いてなかったので。Javaでは見つかったんだけどC++だと需要ないの?

  • calculator.thrift
namespace cpp example
service Calculator
{
    i64 add(1:i32 num1, 2:i32 num2);
}

適当に足し算するメソッドを用意する

  • server.cpp
#include "gen-cpp/Calculator.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/processor/TMultiplexedProcessor.h>
#include <iostream>
#include <thread>
#include <chrono>

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

using namespace ::example;

class CalculatorHandler : virtual public CalculatorIf{
public:
    CalculatorHandler(){}
    //addの実装
    int64_t add(const nt32_t num1, const int32_t num2){
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
        return num1 + num2;
    }
};

int main(int argc, char** argv)
{
    int port = 9090;
    shared_ptr<CalculatorHandler> handler(new CalculatorHandler());
    //実行するプロセスのインスタンスを複数用意する
    shared_ptr<TProcessor> processor(new CalculatorProcessor(handler));
    shared_ptr<TProcessor>remoteProcessor(new CalculatorProcessor(handler));

    shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
    shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
    shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
    //メインのプロセス
    shared_ptr<TMultiplexedProcessor> mprocessor(new TMultiplexedProcessor());
    //名前をつけてメインプロセスに実行プロセスを登録する
    mprocessor->registerProcessor("Calculator", processor);
    mprocessor->registerProcessor("remoteCalculator", processor);

    TSimpleServer server(mprocessor, serverTransport, transportFactory, protocolFactory);
    server.server();

    return 0;
}

  • client.cpp
#include <iostream>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transportt/TSocket.h>
#include <thrift/transport/TTransportUtils.h>
#include <thrift/protocol/TMultiplexedProtocol.h>

#include "gen-cpp/Calculator.h"

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace ::example;

int main(int argc, char** argv)
{
    //ホントはちゃんと処理しないとだめだけど第一引数がプロセス名とする
    string protocolName = argv[1];

    boost::shared_ptr<TTransport> socket(new TSocket("localhost", 9090));
    boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
    boost::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(transport));
    //複数接続する用のプロトコルを使う。名前で識別する
    boost::shared_ptr<TMultiplexedProtocol> mp(new TMultiplexedProtocol(protocol, protocolName.c_str()));
    CalculatorClient client(mp);

    transport->open();
    int val = client.add(1, 2);
    std::cout << val << std::endl;
    transport->close();
    return 0;
}

マルチスレッドじゃないんで先に撮った方のプロセスの処理が終わるまで待ちが発生するけど、同じポートに二つのクライアントプロセスから接続して処理させるのはできました。あ、ビルド時にlibthread.aにリンクしてね

おしゃれなCLI Argument Parser、Docopt.cppを使ってみた

GitHub - docopt/docopt.cpp: C++11 port of docopt

Pythonとかの情報はあるのだがC++はあんまりないので書いとく。

Docoptはgetoptやprogram_optionsのようなCLIから実行するときのコマンドやオプションを解析してくれるパーサなのだが、とにかく使い方がエレガントだ。注意点はGCC4.8だとなんかいろいろ大変ってことだろうか。GCC4.8を使っている場合については後述。

基本

かきかたはgithubにも書いてあるし、ここにもあるが、違うサンプルでかいてみる。

#include <docopt.h>  //別にコードと同じ階層にdocoptのライブラリをコピーしてきても良いが綺麗じゃないのでわけた
#include <iostream>

//USAGEで定義する文字列の中にUsage: Options: でUsageとOptionsを指定するだけであとはよろしくやってくれる
//exampleではなぜかcharを使っているが実装はstd::stringだった。いやまぁ暗黙で変換してくれるけどさ…
static const std::string USAGE =
R"(test
    Usage:
        test start (o <option> -p <option2> | -q <option3> [-d dir -v]
        test stop [now]
        test (-h | --help)
        test --version

    Options:
        -o option         Set option
        -p option2        Set option2
        -q option3        Set option3
        -d dir            Set directory
        -v                Debug print
        -h --help         Print help
        --version         Print version
)";

int main(int argc, const char** argv)
{
    //コマンドラインで指定する引数を最初のバイナリ名だけは飛ばして最後までvectorに格納するらしい
    std::vector<std::string> arg {argv + 1, argv + argc};
    //最後の引数に表示するバージョンスクリプトを指定
    std::map<std::string, docopt::value> args = docopt::docopt(USAGE, arg, true, "SampleProgram v0.0.1");

    //mapのキーはOptionsで指定したオプション名。値を取らないオプションの場合、docopt::valueはboolになる。それ以外はstd::string
    //helpとversionは特に実装しなくていいが、それ以外のオプションについては以降で処理を記述する
    //例
    if(args["start"].asBool() == true)
    {
        std::cout << "stat" << std::endl;
        if((std::string)args["-o"].asString() != "")
        {
            //do something
        }
    }
    
    return 0;
}

Usageを書けばそれにそってパースしてくれるというのがスマートでよい。コンパイルする場合はlibdocopt.aをリンクしよう。

GCC4.9以上が普通に入ってる場合

$ g++ -o test test.cpp -I<docoptdir> -ldocopt

GCC4.8以下の場合でGCC4.9を無理やり入れた場合は

$ g++-4.9 -o test test.cpp -I<docoptdir> -L/usr/lib/gcc/x86_64-linux-gnu/4.9 -lstdc++ -ldocopt 

libstdc++のリンクをしないといけない。

GCC4.9を無理やり入れてGCC4.9でdocoptをコンパイルする方法

C++11 regex using g++ | GeekWentFreak

ここにかいてあるとおりやればよい。

$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test
$ sudo apt-get update
$ sudo apt-get install g++-4.9

無理にgcc4.9を使うのでCMakeList.txtをいじってもいいけど自分でコンパイルすることにする(cmakeはよくわからん)

$ g++-4.9 --std=c++11 -D__cplusplus=201103L -D__GXX_EXPERIMENTAL_CXX0X__ -o docopt.o docopt.cpp docopt.h docopt_value.h docopt_util.h docopt_private.h 
$ ar -r "libdocopt.a" ./docopt.o

あれ?これでいいのかな?まぁEclipseで適当に設定してShared Libraryを作ってください
これでできたlibdocopt.aを使わないとregex関係のリンクが上手くできない


ちなみにgcc4.9以上が入っててもCMake3.2未満だとコンパイルできないのでとりあえずcmake3.2は入れる。

$ sudo apt-get install build-essential
$ wget http://www.cmake.org/files/v3.2/cmake-3.2.2.tar.gz
$ tar xf cmake-3.2.2.tar.gz
$ cd cmake-3.2.2
$ ./configure
$ make
$ sudo apt-get install checkinstall
$ sudo checkinstall
$ sudo make install

そんでおもむろにライブラリを作る。

$ cmake .
$ make

一度つかえるようになればかなり相当楽になる。

文化がアジャイルを壊す

Twitterでもブツブツ言っていたが、最近アジャイルのトレーニングを受けた。海外で。
参加してたメンバーは主にEU圏内とポーランド・ロシアあたりの人々で講師はアメリカ人。参加者はプロダクトマネージャからジュニアプログラマまで全員みたいな感じ。CTOもいましたね…
欧州と米では開発文化はかなり違っていて、一口に言ってしまえば欧州はプロセスがプロセスとして成り立たないレベルで自由なことが多いようだ。あんまりプロセス自体に興味がないかんじ。もちろん納期とかもゆるゆるでアメリカ以上にリリース日が遅れまくる。会社の人達は基本的なスキルは高いけど属人性を好むところがあり、いつまでたっても自分の手からタスクを離さない。情報の共有文化があまりない。コミュニケーションは基本口頭で、Skypeとかは使うけどあんまり好きじゃないようだ(これはいろんな国から来ているということもあるが、欧州人は基本的にあまり文字のコミュニケーションが好きじゃない。母国語が英語でない場合が多いせいだと思う)。

トレーニングではScrumとKanbanがメインだった。ウォータフォールからなかなか抜け出せない古い開発プロセスで開発を回している日本の会社ならScrumはわりと馴染みが良いかなと僕は思っているが、ごりごりにピュアScrumをやろうとすると失敗するだろうと思ってた。基本的にScrumってスプリントの部分ばかりフォーカスされて、その前のとこは?っていったら「完璧な仕様があります」「完璧な要求があります」みたいな感じで紹介されるじゃないですか。あれって非現実的だよね、みたいな。


ちなみにこのトレーニングではそこら辺が曖昧な状態での開発方法を扱っており、結局スプリント開始前のフェーズではみんなで膝を突き合わせて大きめのバックログを作るところにフォーカスをおいていたので、アメリカではすでに死屍累々を乗り越えて今に至るのだろうと思われる。
バックログの作り方は基本的に昔の要求分析と同じかなぁと思うが、ひとまずミッションステートメントを作って最終的なゴールと優先順位をおおまかに明確化して、そこから必要なバックログ(いわゆる要求ですね)を出していくので、新規開発ではここをしっかりやっていればあまりブレることはなさそう。要求を作っていく作業ってのは誰でもできるわけではなく、こういう時にSysMLとかは役に立つのだが、欧米ではそういうツールがあんまりはやってないのでそのへんの説明はなし。後で書くけどツールを使わない彼らのやり方は今後やっぱり問題になっていくだろうなと僕は思っている。
バックログができたらあとは日本でよく紹介されているScrumのやり方ですね。基本的に残業をしない欧州ではデイリーミーティングをやれば工数と期日がだいたい予測できるが、残業の多いアメリカと日本じゃどうなんだろうなと僕は思ったりする。


そんで面白かったのがもう一個のカンバン。変な日本語の読み方がいっぱい出てくる(カンバンもケインバンとか呼んでる)からいちいち笑うのだが、要するに優先順位をつけて上からタスクを流れ作業で処理していきましょうという工場の業務フローをソフトウェア開発に持ち込んだものだ。利点は期日どおりにリリースできること(もし終わっていないタスクがある場合も気にせずリリースする)。
講師がこれはすごいって賞賛していたのだが、みなさんどう思いますかね。僕は全然すごくないと思います。ってか当たり前すぎるじゃんみたいな。そもそもこれはフローであってプロセスじゃないから、Scrumの問題点を解決してくれるすごい開発プロセスには成り得ない。そもそもすでに優先順位が定まっているタスクが存在していることが前提であって、そうじゃなかったらグダグダになりまくりだろうという。もちろんその辺はScrumのスプリントに入る前の作業できちんと議論したうえでスプリント中にKanbanを使用するということらしいのだが、それでもねぇ…っていう。そもそもタスクを分解して流れ作業にするより、ある意味のある単位での要求を一人または数人で最初から最後まで仕上げるほうが生産性も品質も高まるってのが最近の日本のはやり(?)じゃないですか。なに逆行してんだっていう。



ただこのトレーニングを一番後ろから見てたんだけど、Kanbanに対する食付きがすごいのね。細々とグループワークがあったのでその様子を見る限りも、なんかKanbanは新しい何か、みたいな受け止められ方をされている。
でもよく考えると、欧米の人たちって、というか特に欧のひとたちっていうのは「プロセスを守る」という部分が非常におろそかなので、逆にKanbanみたいな思想を導入したほうが失敗が少ないのかもしれないなと思ったりした。これはどういうことかっていうと、彼らはプロセスやコスト・期日・仕様・設計ありとあらゆるものをおろそかにするが、その代わり要求されているものをできるだけ最善な形で出そうとする努力は惜しまない。品質に関しては元がいい加減なのでそれなりなのだが(テスト仕様とかひどいもんだ)、それでも彼らなりに最善をつくすべきだとは思ってるみたい。
つまり「要求を満たすこと>品質>その他>>>>>>期日」で、要求を満たすための議論は非常に活発に行われる。上下関係は一応あるけれども、上下関係があるから言えないというのもないし(中傷はしませんけどね)、関係性がフラットなのも日本とは違う。


いくつか問題があるとすれば、基本的にみんなスキルが高いという前提で話が進むので(なのでプロセスが不要)、抽象的な話のまま議論が終わったりすることと、エビデンスを残す習慣がない。議事録は残さないし、ドキュメントは嫌いだし、あとこれは欧州限定なのだがアメリカ人よりものいいが抽象的なので、欧州系の文化で育っていないと彼らがなにに満足して、なにで合意に至ったのかよくわからないことがある。そしてそれゆえにその後ちょっと具体的な話になった時に意見の相違が出てくることがあるのだ。これでXMLでもなんでもいいんだけどツールを使って限りなく具体化するということをやっていればよいのだが、議論しちゃえば早いでしょという感じでつかわない。そしていつの間にか仕様が変わっている。
基本的なスキルが高ければそれでいいのだが、要求をきちんと実装に落とせないタイプがいたり、ジュニアデベロッパーのほうが多かったりすると問題が続出する結果となる。テストも穴だらけなので品質は微妙なものが出てくる。
これが欧州のやり方だ。


アメリカはもうちょっとプロセスがしっかりしてるんじゃないかなと思うのだが(飽きもせず開発プロセスを発明してるし)、議論が当たり前のように行われるフラットなチームではこういうことが起こりやすいのかもしれない。
逆に日本だと絶対にいる「プロセス通りじゃない!」って怒るタイプの人がプロセス通りに物事をすすめることに貢献するし、テストがクソ緻密すぎてコストがかさみまくったりするが、一応あんまりにも予想できないものが出てくることはないような感じがする。もちろん開発中に明らかにダメなものを指摘できずにクソ化していく経過を見ることはできるし、出てくるものが妙にしょぼかったりもするのだが、一応は出した要求通りのものが出てくるのが日本だ。期日通りかどうかは微妙なところだが、そもそもスケジュールがタイトすぎる開発現場のほうが多いと思うのでその辺は気にしなくていい気がする。というか日本のスケジュール感で見積もったものを欧州でやらせようとすると多分三倍くらい時間がかかるな。出てくるものはちょっと豪華になるかもしれないが、どうしようもないクソになってしまっている可能性もあるので注意が必要だ。小さな開発チームでビジョンを共有していれば逆に素晴らしい物が出てくる可能性もあるが、ちょっとチームが大きくなるととたんに欧州は仕事が回らなくなる。



ぼくは思うのだが、結局ダメなプロジェクトはどんなプロセスを使っても駄目だし、文化に合わないプロセスはどんなに安全なプロジェクトでもダメにする。そしてその国や文化圏に応じて気をつけるべきことは違っている。
欧州に足りないのは決まったことを守るという視点、日本に足りないのはダメなものをダメだと指摘して、建設的な議論をするチームの文化。逆にいえばそれさえあれば、たとえウォータフォールでも、というかプロセスさえなくたってものづくりはうまくいくんじゃないだろうか。

いまさらか!なんだけどWindowsバッチのはなし

それでもやっぱり時々は書かなきゃいけないWindowsバッチ。複雑なことはやらせるなという話なのかもしれないが、他のスクリプト実行環境をどうしても入れられない場合などは書くほかないのである。
あとちょいちょいかいてあるとおりにならないんだけどこれはなんでなのかな?

文字列を切り刻む

なん文字目から切ればいいということがわかっている場合はかんたんなのだが、そうでない場合はfor文まわしてdelimsでスプリットしていかねばならないらしい。しかもfor文の中のステートメントは先に展開されてしまうので、取得した文字列をこの配列に代入して…というのは案外難しいらしい。

set n=0
for /f "tokens=1-3 delims=_ " %%i in ('command') do @call :subset %%i %%j %%k

:subset
    set /a n+=1
    set data[%n%]=%1

けどこの場合echo data[n]とやりたい場合の書き方がよくわからなかったりするのである…素直にグローバルスコープの変数に入れて使いまわしたほうが良いのかもしれない(モダンなプログラムをかいている人には気持ち悪いと思うが)


ちなみにdelimsはデリミターのことだが、マニュアルには複数指定可、デフォルトはタブとスペースがきくとかいてある。じゃぁスペースとカンマで分けたい場合はどうするの?という場合は結構探したけど書いてなかった。やらないわけ無いと思うんだが…

正解は

for /f "tokens=* delims=, " ほにゃらら

スペース以外でも分割したい場合、スペースはパラメータの区切りとして認識されるので、そうされないために最後にdelimsを指定すること。ほいでもってdelims= ,とするとカンマがわけわかりませんょといわれてしまうので、delims=, である。エスケープ使えればいいんだけどね

ループに引数をわたす

:subset %%i %%j

このブロック名の後ろにいる人が引数。使うときは番号で呼ぶ。

:subset
    set result=%1
    set value=%2

ブロックというかメソッドと思えば良いらしい

ブロックを出てからの挙動

ふつうの文が最後の場合はいいのだが、for文の場合は一旦forの終端に戻り、そのままシーケンシャルに実行されるので、ブロックの最後に次に行きたいブロックの場所を指定しておかねばならない。つまり

for /f "token=1,2 delims=,; " %%a in (test.txt) do @call :nextLoop %%a %%b


:nextLoop
set a=%1
for /f "token=1,2 delims= " %%i in ("%a%") do @call :nextnextLoop %%i %%j

とした場合、最初にメインループ→nextLoop→もとのループにもどる→もう一回nextLoopとなるようだ。呼び出し元に戻ってもらいたいものだがそういえばプログラムとはそういうものだった。