PINE64 と Xamarin.Forms の組み合わせを考える

この記事は、Xamarin Advent Calendar 2018 17日目の記事です。

PINE64 ってのは 3,000 円級で買えるラズパイ互換の組み込みボードです。ラズパイ互換というといささか語弊があるのですが、メモリが1GB タイプならば秋月で買えるし Raspberry Pi 3 Model B よりも高機能なところがあります。

image

Android 7.1 を入れる

最初に PINE64 を買ってみたのは Windows IoT Core を動作させたいためだったのですが、最初の頃は LAN などが上手く動かなくて諦め状態だったのです。今のバージョンは試していないので、それはまた今度。

でも、Android がほどよく動きます。Raspberry Pi 3B に Android を入れるとちょっと重たくて使い物にならないのですが、PINE64 に入れるとまずまずのスピードで動くようになります。さすがにアクションゲームは難しいのですが、カードゲームっぽい RPG ものならば大丈夫だし、この記事のように Xamarin.Forms のようなテキストであれば十分なスピードがでます。このあたり GPU が乗っているのが大きいとは思うのですが、詳しいことは不明。

それでも、普通の Android スマホとはちょっと変わった形で Android が使えるのがミソです。

デメリットの解説を

先に PINE64 + Android の組み合わせのデメリットの話をしておきましょう。

  • ラズパイより情報が少ないので構築に苦労する
    → ふつうに Ubuntu で使うにしても苦労するし、まあ差額考えてもラズパイのほうが楽
  • タッチパネルが使えない
    → そもそもモニタがないので、別途液晶モニタがないと何もできないです。
    → USB にマウスを付けないと使えないです
  • バッテリーがないので、電源を引っこ抜くと消える
    → 電源を USB 経由で供給しないと駄目なので、引っこ抜くと Android ともども落ちます。モバイルバッテリーだといけるかもしれません。
    → Android を動かすのに、5V2A が必要です。これはパソコンからの USB 給電では動きません。

あと、Android から GPIO を操作するのは root が必要で微妙に面倒です。面倒というかまだ試していないので、試さないと。すでに買ってから1年以上経っていますが。

ちょっとだけメリットを

実運用するにはデメリットが多いのですが、ちょっとだけメリットがあります。

  • 液晶モニタを HDMI 接続できる
  • 超安価な Android 機として扱える。
    → 中古でも1万円するので、2GB タイプでも 5,000円弱です。
    → Android 7.1 が用意されています。SDK からビルドすれば 8.1 も動きそうな気がするのですが試していません。
  • Android を microSD として入れられる。
    → 初期データを入れたり、データをコピーしたりできます。
    → 最初から root です。
  • Google Play が使えます。
    → PINE64 + Android では Google Play が使えます。

そんなこんなで試しに動かしてみると面白いかもしれません。Android SDK から自前ビルドをして動作させることができるので、それなりにハックはできるんですが、SDK のビルド環境を整えるのが結構大変で。もともと構築済みの人だといいんだと思うのだけど、初手からやるのはなかなか辛いかもしれません。それだけのメリットが得られるのか?というとかなり疑問で、だったらふつうにラズパイ+Raspbian で動かしたほうが無難かも、とおもったりもしますが。

それでも、実験的に PINE64 + Android + Xamrin.Forms というのをたまに試してみたりします。

環境構築

環境構築をざっと書いておきます。

  • PINE A64-LTS ボードを購入。左上の Store から直販で $32 で買えます
  • microSD のイメージは SOPINE Software Release – PINE64 の Android 7.1 Community Build Image をダウンロードする。
  • PINE64 + Android 7.1 を起動したら WiFi ADB を入れて起動。Android 側の adbd が起動して、PC から adb connect できるようになります。

adb から PINE64+Android で接続できるようになったら、準備完了です。

image

ちなみに、半年前に動かしたアズレンがこれです。今やキャッシュが壊れているのか画像がうまくでないんですが。再構築したら出るかも。

image

Xamarin.Forms を動かしてみよう

Android が動くのだから、Xamarin.Android も動くだろうし、Xamarin.Forms も動くだろうとう話です。Xamarin.Forms のメリットは、Android, iOS の両方のスマホを一気に作れるというものが大きいのと、もうひとつ C# でさっくりとした画面をさっくりと作れるというメリットがあります。私が Android IDE に慣れていないからというせいもあるかもしれませんが、スマホのような小さな画面ではなくて液晶モニタのように大きな画面でレイアウトを作る場合は、Xamarin.Forms で使われている Grid が効率的です。

なので、大き目のタブレット画面 + Android という組み合わせで業務アプリを作る場合に、Xamarin.Forms の Grid と MVVM がベストだろう、と考えているわけですが…世の中、大き目の Android タブレットという動きにはならなかったんですよね。今後はどうなんでしょう?

それはさておき、Xamarin.Forms のサンプルを動かしたのがこれです。

image

image

これ自体は、Visual Studio で Xamarin.Forms のテンプレートを使っているだけなので、説明は省略。

image

image

これだけだといまいちなので、github から xamarin.forms のサンプルを動かしてみましょう。

xamarin-forms-samples/CatClock
https://github.com/xamarin/xamarin-forms-samples/tree/master/CatClock

image

ほかのサンプルも

image

image

image

元が Android なので、UI はそのまま XAML を使えます。スマホの場合には画面が小さいので全画面で作ることになるのですが、23インチ液晶位になるともう少しレイアウトを考える必要がありますよね。そのあたりにうまくハマるのではにかな、と考えています。

カテゴリー: Android, Xamarin | PINE64 と Xamarin.Forms の組み合わせを考える はコメントを受け付けていません

M5Stack から Slack に Yo! する

この記事は、M5Stack Advent Calendar 2018 – Qiita  の 10日目の記事です。
以前つくってみた M5Stack から Slack へ投稿する記事の焼き直しなのですが、もう少し詳しく書きます。というか、以前作ったやつをコピペしたら動かないということで、ちょっと書き直し。

仕組み

Slack へ投稿する方法として、Web APIを使って直接投稿することもできるのですが、なかなか面倒ので WebHook を使います。HTTP で手軽にアクセスできる API を用意しておいてアクセスする方法ですね。WebHook 内で複雑なアクセスを肩代わりしてくれるので、組み込み側に専用のライブラリを用意する必要がなくなります。

というわけで、

  • SlackのWebHookを作る
  • 作った WebHook に HTTPS で送る

2手順で実現します。

SlackのWebHookを作る

ブラウザで slack にログインしている状態で左上の「Manage」をクリックします。

検索ボックスで「WebHook」を入力して「Incoming WebHooks」を選択。

「Add Configuration」をクリックして WebHook を作ります。

投稿先のチャンネルを指定して、WebHook の URL を作成します。

投稿するデータは JSON 形式で送信して、宛先の URL が https://hooks.slack.com/services/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 な感じになります(このURL自体はすでに無効にしてあるので大丈夫)。

投稿するのがテキストだけであれば、次のように text だけに設定

{"text": "This is a line of text in a channel.\nAnd this is another line of text."}

投稿時のアイコンやユーザー名も指定できます。実はチャネル名も指定できるので、WebHook 自体はチャンネルごとに作らなくてもひとつだけ作っておけばOKです。

{
 "text":"From M5Stack Yo!",
 "icon_emoji":":ghost:",
 "username":"m5stackpost"
}

これで WebHook の準備は完了

証明書をハードコードする

色々調べると M5Stackから HTTP 接続する場合に HTTPClient を使うのですが、Slack の WebHook は HTTPS 接続になっています。セキュリティ接続になる場合、公開鍵の証明書が必要になってきます。

実は例のうんこボタンやエリクソンの証明書の期限切れの問題もここに関わる訳ですが、証明書の使い方が「暗号化」と「ライセンス期限」との2つを混在させてしまっている問題があります。この部分、暗号化だけ使うのであれば有効期限は無視してよいし、ライセンス期限の制限だけなれば証明書の有効期限だけをチェックすればよいわけです。

さて、今回は Slack のサーバーとの暗号化通信となるので、サーバー側の証明書を M5Stack 側に埋め込んで暗号化できるようにします。

Google Chrome で slack のサイトを開いた状態でサーバーの証明書を表示させます。

「証明書のパス」のタブを開いてルート証明書か中間証明書を選択して「証明書の表示」をさせます。どちらの証明書を使っても構いません。

証明書をテキストファイルに保存します。

C++ のコードに埋め込んでしまうので、BASE64 形式で保存します。

中身をメモ帳で開くと、こんな感じになっています。

この証明書を M5Slack のコードに埋め込んで、HTTPS 接続するときに使います。

M5Stackのコード

Slack に Yo するコードはこんな感じです。
WiFi に接続するための SSID とパスワードは適宜変えてください。
Slack に送るための WebHook のところ「/services/XXXXX/YYYYY」も書き換えます。

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <M5Stack.h>

WiFiMulti wifi;
HTTPClient http;

#define WIFI_SSID "<WiFiのSSID>"
#define WIFI_PASS "<WiFiのパスワード>"
const char *server = "hooks.slack.com";
const char *json = "{\"text\":\"From M5Stack Yo!\",\"icon_emoji\":\":ghost:\",\"username\":\"m5stackpost\"}";
const char *json2 = "{\"text\":\"From M5Stack Hello!!!\",\"icon_emoji\":\":ghost:\",\"username\":\"m5stackpost\"}";

// ルート証明書
const char* slack_root_ca= \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh\n" \
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" \
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" \
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT\n" \
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" \
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG\n" \
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB\n" \
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97\n" \
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt\n" \
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P\n" \
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4\n" \
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO\n" \
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR\n" \
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw\n" \
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr\n" \
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg\n" \
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF\n" \
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls\n" \
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk\n" \
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=\n" \
"-----END CERTIFICATE-----\n" ;
  
// 中間証明書
const char * slack_sub_ca = 
"-----BEGIN CERTIFICATE-----\n" 
"MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh\n" 
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" 
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n" 
"QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT\n" 
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg\n" 
"U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" 
"ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83\n" 
"nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd\n" 
"KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f\n" 
"/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX\n" 
"kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0\n" 
"/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C\n" 
"AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY\n" 
"aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6\n" 
"Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1\n" 
"oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD\n" 
"QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v\n" 
"d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh\n" 
"xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB\n" 
"CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl\n" 
"5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA\n" 
"8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC\n" 
"2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit\n" 
"c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0\n" 
"j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz\n" 
"-----END CERTIFICATE-----\n" ;

void init() {
  M5.begin(true, false, true);
  M5.Lcd.clear(BLACK);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(65, 10);
  M5.Lcd.println("Slack example");
  M5.Lcd.setCursor(3, 35);
  M5.Lcd.println("Press A button");
}

void setup() {
  init();
}

void sendYo() 
{
  // put your setup code here, to run once:
  wifi.addAP(WIFI_SSID, WIFI_PASS);
  M5.begin();
  M5.Lcd.println("send slack");
  
  while (wifi.run() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.printf(".");
  }
  M5.Lcd.println("wifi connect ok");
  // post slack
  // https://hooks.slack.com/services/XXXXX/YYYYY
  M5.Lcd.println("connect slack.com");
  http.begin( server, 443, "/services/XXXXX/YYYYY", slack_sub_ca );
  http.addHeader("Content-Type", "application/json" );
  http.POST((uint8_t*)json, strlen(json));
  M5.Lcd.println("post hooks.slack.com");
}

void sendYa() 
{
  // put your setup code here, to run once:
  wifi.addAP(WIFI_SSID, WIFI_PASS);
  M5.begin();
  M5.Lcd.println("send slack");
  
  while (wifi.run() != WL_CONNECTED) {
    delay(500);
    M5.Lcd.printf(".");
  }
  M5.Lcd.println("wifi connect ok");
  // post slack
  // https://hooks.slack.com/services/XXXXX/YYYYY
  M5.Lcd.println("connect slack.com");
  http.begin( server, 443, "/services/XXXXX/YYYYY", slack_sub_ca );
  http.addHeader("Content-Type", "application/json" );
  http.POST((uint8_t*)json2, strlen(json2));
  M5.Lcd.println("post hooks.slack.com");
}

void loop() {
  M5.update();
  if (M5.BtnA.wasReleased()) {
    sendYo();
  } else if (M5.BtnB.wasReleased()) {
    sendYa();
  } else if (M5.BtnC.wasReleased()) {
    init();
  }
}

Aボタンで「Yo」を送って、Bボタンで「Yha!」を送ります。
Cボタンで画面クリアですね。

いちいち WiFi にログインしているのが悪いのか(たぶん接続を切っていないから、WiFiMulti がクローズされていないのだと思う)、ボタンを2回押さないと Yo が送れません。そのあたりは今後改良ということで。

実行結果

M5Stack の表示

Slack クライアントの表示

カテゴリー: 開発 | M5Stack から Slack に Yo! する はコメントを受け付けていません

SQL Server を SUM/AVG でオーバーフローさせてみよう

ちょっと誤差関係で気になったことを試してみる。

統計学では平均は分散を使うことが多いのだけど、標本の数が多い場合にデータベースの sum や avg は有効に働くのだろうか?という疑問がでてくる。単純に言えば、SQL Server などのデータベースで扱える数値はどのくらい大きいあるいは精度が出るのだろうか?

以下は、SQL Server 2016 で試した結果

計算結果は38桁まで有効

単純に有効数値を確認するためにはオーバーフローさせればよい。例の0.01をどこまで掛けられるかを「0」を打ちながら試してみる。

こんな感じで、入力するときに38桁でエラーになる。

同じように、計算結果が38桁を超えるようにすると、やっぱりエラーになる。

どうやら、内部での計算は numeric 型の38桁の計算(これはint型よりもずっと大きい)でなされているようだということが分かる。

numeric( 8,3 ) で制限して10万回足したらどうなる?

numeric ってのは、有効桁数と小数点以下の桁数で指定するので、numeric( 8,3 ) は、有効桁数が8桁で小数点以下が3桁になる。99999.999 までが有効になる。

これで、1.999 のようなランダムな数を作って 10 万回足したらどうなるだろうか?と思ってやってみると、以下のようにふつうに sum の結果がでる。合計値は 535469.446 となるので、 numeric( 8,3 ) の範囲を超えているが、特に問題ないらしい。カラムの精度と計算結果の精度とは違うことがわかる。

image

たぶん、sum の計算結果は numeric の 38桁に拡張されているはずだ。

フルで 38 桁の精度を持たせた場合はどうなるのか?

では、カラムの設定を numeric( 38, 28 ) のようにして、有効桁数38桁、小数点以下28桁が有効にするとどうなるだろうか?

データとしては、1999999999.0000000000000000000000000001 を 20個ほど入れておいて、加算するとオーバーフローするように仕掛けておく。

insert into TT ( v, vv ) values ( 1, 1999999999.0000000000000000000000000001 );

こんな風にデータを入れておく。

image

そして sum する。

image

結果は予想通り、numeric の 38桁の精度を超えてしまうので計算できない。

じゃあ平均(AVG)の計算は?

平均を計算してみよう。同じ値しか入れていないので、平均は 1999999999.0000000000000000000000000001 で明白なんだけど、SQL Server の AVG 関数を使うと、以下のようにオーバーフローになる。

image

これ、前回の記事にも書いたが平均を計算する対象の最大値/最小値を適当に取ってきて、類推できる平均な値を使えば、オーバーフローにならずに計算することができる(当然、幅によってできないこともある)。だから、SQL Server の AVG 関数は、(おそらく)内部で SUM を使っていて個数で割ることをしているのだろう。だから、計算途中でオーバーフローしてしまうのだ。

「ドメイン知識」大切ですね、にもつながる。

カテゴリー: 雑談 | SQL Server を SUM/AVG でオーバーフローさせてみよう はコメントを受け付けていません

0.01を10000回足したら100.00にならない話

元ネタの引用リツイートが3件ほど立て続けてに回ってきたのと、「俺たちはテストコードにいつまで ε を書かないといけないのだろうか?」というツイートがあったので、それを絡めて。

最初に TDD の話だけ書いておこう(一連のツイートには書かなかったので)。

xUnit のテストコードに誤差範囲を指定する

自動単体テストでは、Assert.AreEqual( 期待値, 実測値 ) な形で、数値や文字列を比較するけど、これ実数(double)の場合は、Assert.AreEqual( 期待値, 実測値, 誤差 ) という形で誤差の許容範囲を指定しないといけない。これ、文字列や整数値に場合にはピッタリ一致するのだけど、実数の場合はそうはいかない。科学計算のユニットテストを書くとわかるが、計算順序や計算の精度などを上げると、微妙に値がずれる。そのたびにいちいち期待値を変えていられない、というか期待値を変えてしまってはテストコードにならないので、許容範囲を指定する。大抵の場合は有効桁数を書くことになるので、

Assert.AreEqual( 100.00, 実測値, 0.01)

な形で有効桁数を示すことが多い。この場合は、99.99001(99.99より大きい) から 100.00999…(100.01より小さい) を 100.00 と扱うことができる。不等号を使えば、

99.99 < 実測値 < 100.01

の範囲となる。両端を含まない「開区間」になるんだが、まあ細かいことはどうでもいい(本当は重要だけど)。こんな感じで真の値から幅のある値を取る。

コンピュータで扱う実数とは何か?

知識として整数値(int型)と実数値(doubleやfloat)を分けてコーディングをしないといけない、というのは知っている人が多いと思うのだけど、じゃあ double や float を使う場合、コンピュータ上でどう扱っているのか?というのを知っている人は…理/工学系には「教養」だよ!という話だったりする。

倍精度浮動小数点数 – Wikipedia https://ja.wikipedia.org/wiki/%E5%80%8D%E7%B2%BE%E5%BA%A6%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0

IEEE 754 Double Floating Point Format.svg

倍精度の「倍」の意味は、単精度の倍だから。なので、単精度が32ビット、倍精度が64ビットになる。8の倍数になっているのはコンピュータで扱いやすいからで、ここに当然ながら誤差が存在する。ざっと覚えておくとよいのは倍精度で8桁位まで有効に働く。10進数の有効数字としては、12桁位まであるんだけど、科学計算したりすると桁落ちが発生するので、経験上8桁までは有効になる。

元ネタが30年前の本を参照しているので、1990年代ぐらい。ちょうど情報科学学科ができ始めた頃で、C 言語が徐々に広まっていたころ。科学計算だと Fortran が主流でまだ一般的なプログラムとしては BASIC が主流だった頃の話だ。90年代としてひとくくりにしてしまうとかなり面倒なんだけど(インターネット接続も含まれるから)、Excel VBA が広まり始めた時期でもある。

メモリとバス幅の制約

ちょうど1990年代は、コンピュータ言語がいろいろできた時期でもあるので、このあたりの背景を説明すると、

  • 浮動小数点付きの計算は「高価」で、専用のコプロセッサで動かしていた
  • 高価だったから、普通の CPU はライブラリで浮動小数点計算をしていた。ので遅かった。
  • まだ、32ビットマシンが出たばかりで、レジスタやバス幅は 32 ビットだった
  • バス幅の関係上、単精度(32ビット)は早かったが、倍精度(64ビット)は遅かった。
  • ストレージ(フロッピー、HDD)が小さかったので、64ビットを保存するのは勿体なかった

という関係上、実数の計算は主に単精度(float)が使われていた。科学計算を専門に扱う Fortranはデフォルトが倍精度…と思っていたけど real(単精度)ですね。Fortran 入門: 定数と変数 http://www.nag-j.co.jp/fortran/FI_4.html を参照。自分の場合、炉心計算で倍精度にしていたらしい。

BASIC は 90年代よりも前からあるので、実数の場合「単精度」が使われている。なので、表現上は8桁位でるのだが、計算を繰り返しているうちに有効桁数は4桁位になってしまう。

これが1万回くりかえすと…の話になる。

ちなみに、Excel 2013 の VBA を使って計算をすると以下の計算は、100.000000000014 になる。デフォルトで倍精度(double)になっている。

image

実運用上問題はないけど、これを 100.00 と比較すると「偽」になる。当然だ。実数の比較するときには、先の xUnit の例で述べた通り有効範囲を指定しないとダメだからだ。

加算の繰り返しで誤差が累積する

先の計算で、手っ取り早く誤差をうまないためには「実数を整数の範囲に直す」という計算方法が使われる。科学計算の場合は有効桁数で済ませられるのだが、金融関係ではそれではダメだったりする。お金の問題は最後の1円まで有効になる。

お金の計算はいくつか特殊で、

  • 消費税で円以下は切り捨て
  • 為替計算では銭単位まで計算(だったとはず)
  • 利息では銭まで計算
  • 利子は円で切り捨て
  • 四捨五入ではなく銀行まるめを使う

という特有なルールがある。なので、0.01ドル(1セント)のような小数点以下2桁までは重要だったりする。これが1万回加算されたとき(積み立てたとき?)に100.00ドルぴったりにならないと困るわけだ。

最近のシステムでは、decimal や money などのお金を扱うための型が用意されているが(中身も10進数で計算する)、90年代にはそういうものがなかったので、COBOL のように10進数計算ができるプログラム言語を使うか、自前で作るしかなかった。

ただし、0.01 単位というのが分かっていれば、手っ取り早く 100 倍して整数に直し、結果を100で割ればよい。

image

な感じに書き換えると、答えは「100.00」ぴったりになる。

浮動小数点をコンピュータで扱うと有効桁以下のところの誤差が当然でてくる。これを100回加算すれば2桁誤差が増えるし、1万回加算すれば4桁誤差が上がってくる。なので、実数をあつかうときに注意しないといけないのは、

  • 有効桁数
  • 小数点以下の固定の桁数

とは違うということだ。ソフトウェア工学の教科書の最初に出てくる問題なので「教養」の範疇だと思うし、統計をコンピュータ処理するときの常識=「知っておくべき知識」だと思うのだが、どうだろうか?

有効桁数はよく聞かれるので、有効桁数4桁といえば「1234000」な形や「1.234」な形で示される数字の部分だ。小数点以下でゼロをつけるて明示的に「1.000」として有効桁数を表す場合も多い。

これと混同しやすいのは、小数点以下の固定の桁数で「100.234」みたいな形のもの。有効桁数は6桁と考ええても良いけど、この場合は小数点以下の桁数のほうが重要で、固定小数点で小数点以下3桁有効、という言い方をする。浮動小数点は指数表示で表してもいいけど(有効桁数のほうが重要なので)、固定小数点の場合は小数点以下の桁数が重要になるので指数表現するとまずい場合が多い。

100.234 の表現の場合、100 の回りに値が散らばっていて 101.000 とか、98.123 とかの値があるということだ。この場合、測定値から 100.00 を引いてやれば、桁あふれの危険がすくなくなる。0.234 とか 1.000 とか –2.123 とかにならうからだ。これだと有効桁数もわかりやすい。

統計を計算するときに、平均とか分散を計算するのだが、単純に測定値を加算していくつ桁あふれをしてしまう。1万位なら大したことがないとおもうだろけど、100万件のデータの平均を出すときに単純に加算してしまうと桁あふれ(オーバーフロー)を起こしてしまうのは明白だ。だから、先のように 100 の周辺に値が集まっている(身長などはそれ)場合には、だいたい平均っぽい値を先に引いてしまう。これを「ドメイン知識」という(というのを心理統計法で最近知った)。いわゆる、統計をとったときの特性(ドメイン知識)がないと単純に加算してオーバフローを起こすか誤差を累積させてしまうが、適度な知識があれば、誤差を抑えて精度のよい計算ができる。

物理的な有効桁数は意外と少ない

キログラム原器から プランク定数 をもとに計算するように変わったわけだが、プランク定数 6.626 070 040(81)×10-34 J s の桁数がそのまま計測値に適用できるわけではない。というか、たくさんの桁数があっても意味はない(測定誤差があるので)。

なので、測定誤差も含めて材料力学では2,3桁の有効数字で十分だし(安全係数が、1.2 のように実質小数点以下1桁ぐらいなのから推測できる)、天文学に至っては log で計算してしまうので1桁あれば十分だったりする。そもそも、物理の世界では、値が倍ぐらい違ってもびくともしないので、有効桁数は0.5桁という、有効桁数が小数点以下という感覚もある。電子工学でも抵抗の値は10%位の誤差を許容する(誤差自体が書いてある)し、温度や湿度、電流によって特性グラフがデータシートとしてあるので、ぴったりとした値が決められる訳ではない。むしろ、誤差なり特性なりを含む許容範囲が必要となる。

こんな感じに、値が分布していて許容できる上限下限を決める。部品や測定値は真ん中に集中する(正規分布など)グラフになる。

image

なので、計算誤差が累積したり測定誤差があったりするのを前提として、この範囲内で「正しい計算」を使用とするのが科学計算である。だから、実数を扱う場合と整数を扱う場合はちょっと違うことを意識しないといけない。

乗除算でも誤差を減らすためのテクニックがあって、

  • A / B * C
  • A * C / B

この2つの答えは同じように見えるが、後者の「A * C / B」のほうが精度がよく計算できる。前者のように割り算を先にやってしまうと、割り切れない場合があるので誤差がでる。それを次の C を掛けることで増幅させているのだ。だから、数値のオーバーフローに注意してという前提はあるが、A * C で誤算なく乗算をした後に、B で割る(割ったとき割り切れなくてよい)という式変形をする。

数式上(無限に有効桁数があると考えらる)では同じだが、コンピュータ上の有限な有効桁数があると制限がでてくるという話である。

p値(有意確率)と誤差伝播

一気に統計値の話になって、AI な話に振っていきたいのだが、この誤差の部分と分布をうまく扱っていかないと、AI で言われるところの「検定」がうまく働かない。学会や論文的には p 値が重要になってくるのだが、測定誤差とか標本分布の確率の話がないのに、一気に p 値にはいってしまうのはどうかと思う…という話を書きたいところなのだけど、まだそこまで勉強が至っていないので今回は割愛。誤差伝播に関しては、隠れマルコフモデル(HMM)で画像解析をやろうとした時期があって、ベイズは少し慣れているんだが。

プロジェクト管理手法の優位性検定とか不具合検出の標本分布とかは計算しておきたいところ。それはまた後日。

カテゴリー: 雑談 | 0.01を10000回足したら100.00にならない話 はコメントを受け付けていません

小学生の自由研究に最適…かもしれない中央卸売市場の卸売数量をMySQLで扱う

築地市場から豊洲市場への移転に関して、諸々を調べていた時にちょうどよさそうな大量データをみつけたので、紹介がてら。

中央卸売市場の卸売数量は公開されている

いわゆる築地とか豊洲とかの卸売市場は都が経営をしていて、もろもろの情報は以下で公開されています。

東京都中央卸売市場日報
http://www.shijou-nippo.metro.tokyo.jp/

2004年から記録があるので10年以上のデータを取得できます。が、あまり多くても仕方がないのでひとまず5年間分(2013年1月から2018年11月)までの青果と水産のデータをとってきました。

ページを探っていくと、こんな形でテーブルで表示されています。これをCSV形式でダウンロードができます。

CSV形式とはいえ、こんな風にテーブルからとってきたような形のCSV形式なので(一応カンマ区切りにはなっている)ちょっと扱いにくいのです。

公開されているデータ整形する

このままではデータとして扱いづらいので、データベースに挿入できるように本当のCSV形式に直してしまいます。

open System
open System.IO

// 1行目: 販売結果(青果・全市場) → 棟
// 2行目: 平成30年08月01日(水曜日) → 日付
// "(単位:キロ)" で → 分類
// "品名..." で → 市場
// 以降、1行ごとに 品物, 販売方法, 卸売数量(市場ごと)
// ただし、小計、全分類合計は取り込まない

// 出力フォーマット
// 日付, 棟, 分類, 市場, 品名, 販売方法, 卸売数量

let rec readtable (sr:System.IO.StreamReader) 棟 (日付:DateTime) 分類 (市場:string[]) 品名_ =
    let 数量 = sr.ReadLine().Split(",")
    let 品名 = if 数量.[0] <> "" then 数量.[0] else 品名_
    if 数量.[0] <> "小計" then
        for i in [4..市場.Length-1] do
            let 市場名 = 市場.[i]
            let 卸売数量 = if 数量.[i] = "−" then "0" else 数量.[i]
            let 販売方法 = 数量.[2]
            printfn "%s" (
                String.Format("{0},{1},{2},{3},{4},{5},{6}", 
                    日付.ToString("yyyy/MM/dd"), 
                    棟,
                    分類,
                    市場名,
                    品名,
                    販売方法,
                    卸売数量  ))
        readtable sr 棟 日付 分類 市場 品名
    ()

let readblock (sr:System.IO.StreamReader) 棟 日付 =
    let 分類 = sr.ReadLine().Replace("(単位:キロ)","")
    let 市場 = sr.ReadLine().Split(",")
    if 分類 <> "全分類合計" then
        readtable sr 棟 日付 分類 市場 ""
    ()

let readcsv (sr:System.IO.StreamReader) =

    let 棟 = sr.ReadLine().Replace("販売結果(","").Replace("・全市場)","")
    let ci = new System.Globalization.CultureInfo("ja-JP")
    let 日付 = DateTime.Parse( 
                sr.ReadLine().Substring(0,11), 
                new System.Globalization.CultureInfo("ja-JP"), 
                System.Globalization.DateTimeStyles.AssumeLocal)
    sr.ReadLine() |> ignore // 空行
    while sr.EndOfStream = false do
        readblock sr 棟 日付
        // 空行まで読み捨て
        while sr.EndOfStream = false && sr.ReadLine() <> "" do
            () 
    ()

[<EntryPoint>]
let main argv =
    if argv.Length = 0 then
        printfn "ex. tukizi sui_20181105.csv"
    else
        // printfn "%s" argv.[0]
        for fname in argv do
            let sr = new System.IO.StreamReader( fname )
            readcsv sr
            sr.Close()
    0

コードがF#になっているのはいつものことですね。この手の整形をやるのはC#よりもF#で書いたほうがやりやすかったりします。一番内側の readtable 関数内で再帰を使って品名の部分を読み込んでいます。もっと頑張れば for ループを無くせるはずなのでしょうが、ひとまずこれ動いたのでokということで。

何ができるのか?

データ量はざっと200万件近くあります。200万件あると検索が重たくなって大変なのでは?と思っていたのですが、MySQL に突っ込めば全然平気ですね。ひとつのテーブルしか扱わないというのもあるのですが(面倒なので敢えて正規化していません)、一千万件ぐらいあると検索に時間が掛かるのかもしれませんが、200万件位だったら sum を使って集計しても全然あっという間に終わります。

実は200万件の実データを集めるのは結構大変なのです。調査するにしてもデータベースの勉強をするにしても、これだけのデータを扱えるのは結構貴重ではないかな?と思うのです。

5年間分のデータは https://1drv.ms/u/s!AmXmBbuizQkXgpUYeqUVYd7WhAl4uw からダウンロードしてください。MySQLに入れてからエクスポートしたデータと、先のツールで成形したCSV形式のデータが入っています。

テーブル構造以下の通りです。面倒だったので列名は日本語のままw

CREATE TABLE `tt` (
  `日付` datetime NOT NULL,
  `棟` varchar(45) NOT NULL,
  `分類` varchar(45) NOT NULL,
  `市場` varchar(45) NOT NULL,
  `品名` varchar(45) NOT NULL,
  `販売方法` varchar(45) NOT NULL,
  `卸売数量` double NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

去年のさんまの卸売数量を調べてみよう

例えば、去年1年間の「さんま」の取扱量をみてみましょう。

select 日付, sum(卸売数量) from TT 
where 品名 = 'さんま'
  and 日付 between '2017/01/01' and '2017/12/31'
group by 日付
order by 日付
;

こんなクエリを書いて日付でソートすれば、日単位の卸売数量がわかります。

結果をExcelに貼り付けてグラフを追加すれば、ほら自由研究のできあがり。

ちなみにフォントは「全児童フォント(フェルトペン)」 http://tanukifont.com/zenjido/ を使っています。お試しあれ。

カテゴリー: 開発 | 小学生の自由研究に最適…かもしれない中央卸売市場の卸売数量をMySQLで扱う はコメントを受け付けていません

プロジェクトマネジメントに科学的根拠を付加する仮説

PMBOK に科学的根拠がない、というのは「知識体系」として網羅はしているけれど、そこに数学的な根拠とか確率の話がでてこないからだ。何をもってして「プロジェクトの成功」を決めるのかは実はそれぞれなんだけど、プロジェクトの最初に QCD(機能、コスト、納期)を決めておいて、

  • プロジェクト終了時に要件や機能を満たしている
  • 予算/コストをオーバーしていない。
  • 納期が守られている

ということをプロジェクトの「成功」と仮定してよいだろう。実際のプロジェクトは時間とともに変化することが多いから、プロジェクトの最初に決めた「要件」がプロジェクト終了時に満たされていたとしても、時間の差からニーズにあっているかどうかは分からない。これは建築業界でもあることで、計画時に綿密に設計をしたとしても建築物自体ができあがるのはタイムラグがあるから出来上がったときには時代のニーズから外れている(あるいは流行おくれになる)というパターンも多い。公共物を建てるときはどうしてもその矛盾に立ち向かわないといけないし、ニーズ自体が進行中に代わっていくことに追随するためのアジャイル開発手法もあれば計画に沿うかたちで進める計画駆動もあれば(特に建築物は途中で変更が効かないことが多い)それは業界や最終的な利用者(ステークホルダー)に合わせて変化するしかない。

プロジェクト完成時に顧客のニーズを完全に解決することはできない

極端な話、プロジェクトというのは時間の概念から離れることはできないので、計画時の利用者ニーズと完成時の利用者ニーズは変わる可能性がある。車のように購入してすぐに使う、既製服のように吊るしを買う、という場合には買ってから使うまでのタイムラグが短いから差はあまりでないのだけれど(それでも、使ってみたら「思ってたのと違った」パターンがあるのだけど)、プロジェクトの場合は一回性のものでもあるから、ぴったりとはいかない。逆にいえば、「完全に解決する」ことは不可能なので、「おおむね解決できる」という矛盾に立ち向かわないといけない。

プロジェクト開始時の要件や機能(プロジェクト途中の変更も含めて)の規定は、顧客との契約と同時に、作る側としては開発規模を確認/確保するために必須な作業であったりする。が、契約という行為がプロジェクト開始前にある以上ずれが必ずでる。

  • 顧客視点からすれば、プロジェクト開始前に契約して予算が分かることが望ましい
  • 開発視点でいえば、プロジェクト開始時(要件、機能の定義前)に規模はわからない

という話もあり、先のプロジェクトの QCD を決めようとしても矛盾だらけになる。それでも何とかするのがプロジェクトマネジメントの役目だったりする。逆にえば、この「なんとかする知識体系」が PMBOK の根底にはある。

極端な話をすれば、プロジェクトの予算や納期は、プロジェクト終了時点で明確になる。

image

イメージを描くとこんな感じになる。プロジェクト開始時には予算/コストは、上下大幅なずれがあり分布している、当然のことながらプロジェクト終了時には、コストは確定しており納期も確定している。

この上下のブレは、もしプロジェクトが複数あったらという確率の話であるが、実際はプロジェクトは一回こっきりなので、この予算や納期をどうやって予測するのか?というのが PMBOK のプロジェクト計画になる

プロジェクトの予算や納期は分布している

個人にとっては1回こっきりのプロジェクトなのだが、会社にとって複数のプロジェクトが動いていれば、赤字もあり黒字もある、納期遅れもあれ納期より早いものもある。コストが増えたものあれば予算内でおさまったものもある。つまり、複数のプロジェクトでみれば、コストと納期は分布していると言える。

image

プロジェクトが納期/コスト通りに終わる確率は、正規分布なのかカイ二乗分布なのか色々憶測があると思うが「分布」があることが変わりない。少なくとも目標値の前後に広がりがある。

実際のところ納期よりも前にプロジェクトが終わることは稀(これには理由がある)なので、納期遅れが頻発するので適当な指標が必要になるが、大雑把いえば、次の図のように「本来の納期」から遅れが生じたとしても8割の確率で納期遅れが発生しない時点を、プロジェクトの開始時に「納期」として提示すればよい。

image

この部分を80%ラインに置くのか、95%ラインに置くのかは色々だが。納期遅れがシビアな場合は95%が望ましいし、会社の赤黒だけを考えれば8割方プロジェクトが成功するならばまずまずの成績と言える。

この「本来の納期」と80%ラインである顧客に提示する「納期」との差は「保険」と呼ばれ、プロジェクト計画時のマージンとなる。よく言われる「計画時の1.5倍すると大丈夫」というのがそれになる。この確率の話は、CCPM には書いてあるんだけど、PMBOK には出てこない。逆に言えば、どんなに WBS をしっかり作ったとしても納期やコストは分布(ひろがり)を持つので、納期ぴったりに終わる確率は極めて少なく、納期前に終わる確率は50%となる。

じゃあ、なんでプロジェクトは納期通りに終わるのか?というと、納品日に合わせて人が努力をしているからだ、と言える。遅れ気味になったら努力をして残業をして納品日にあわせようとするし、逆に早すぎる場合は待ちの時間を入れて納品日にあわせようとする(早めに納品したりしない)。極端な話、努力の余地がないサイコロを振って出た目だけ進むプロジェクト(実際に CCPM の解説として使われる)の場合は、納品日が前後する。当たり前だ。調節の余地がないからだ。

なので、プロジェクトは実際のところコストや納期に関しては分布を持つので、ぴったりとした値にはならない。というのが、科学的な根拠である。

プロジェクトの QCD はどのように分布するのか?

Q(要件や機能)は分布するのか?というと、まあアジャイル開発の例もあるし、先に書いた時間とともに顧客のニーズ自体も変化するので、実は QCD そのものが分布し、広がりを持つ。

さて、ここの80%成功ラインだが、最初のグラフを参照すると「プロジェクト終了時」には納期が決定している(納品しているから当たり前だ)。当然、コストも一意に決まる。

image

これを確率のグラフと重ね合わせると、時間とともに青線の分布が、オレンジ色の線の分布と変わってくる。80%ラインも時間とともに中央に寄って来る。つまり「本来の納期」に収束するように動く。

これが何を意味するかと言うと、プロジェクトが進行する(時間が経つ)に従って、未確定なものが確定となるので不確定部分は減り、分布が狭くなることを意味している。当たり前といえば当たり前。前に進めば進んだだけゴールに近づいている、ということを示している(なんだかよくわからない例えかもしれないが)。プロジェクト開始時点では未確定であったものが、プロジェクトを進めることにより確定に代わるので、分布が絞れるという形になる。

これを逆に発想すると、プロジェクト開始時に未確定な部分に対して「仮に確定事項」を与えてやれば、分布は絞られる、という話になる。そう、事後分布の話と同じだ。PMBOK とか品質システムで言えば、事前にリスクを抽出しておいてそのリスクが発生したときいどうなるのか?対処はどうするのか?ということを考えておくことにあたる。

そこで、もう少し話を進めると、この未確定事項(リスク管理でもいいし QCD の各項目でもよい)が決定したとしてシミュレーションをすれば、プロジェクトの成功確率を逆算できるのではないか?というが仮説である。また、プロジェクトが進行するうちに未確定事項やリスク項目が減ってきたら、それを確定として再びシミュレーションすれば、プロジェクトの成功率を再計算することが可能になるだろう。

このあたりの発想は、もともと CCPM にあるのだけど、ベイズの定理をあてはめて後はシミュレーションによって求めようというのが自説。実際、プロジェクトシミュレーションの話は、ワインバーグ氏やデマルコ氏のシステムダイナミクスの話にも出てくるし、TOC でも出てくるので、古くからある考え方だと言えるだろう。この続きは、また後日。

カテゴリー: 開発, OpenCCPM | プロジェクトマネジメントに科学的根拠を付加する仮説 はコメントを受け付けていません

PHP7 上に WordPress を入れたときは php-xmlrpc を有効にする

タイトル通りですが、半日ほどハマったのでメモ書き

WordPress から XML-RPC を使って投稿するとエラーになる

以前、作っていた WpPost https://github.com/moonmile/WpPost を markdown に対応しようかと思って、新しく建てた openccpm.com につなげようとすると、エラーになりました。このブログ moonmile.net/blog は XML-RPC で投稿できているので大丈夫なんですが、なぜか新しく立てたほうにはできない。

試しに Open Live Writer を使ってつなげようとするも、アカウント作成のときや URL を指定するときに「parse error. not well formed」と出て「-32700」なエラーコードが返されます。どうやら、送っている XML のフォーマットがダメということなんですが、どういう風にダメなのか分からず。

試しに WordPress の英語版を入れたりバージョンを変えたりしてみたのですが、XML-RPC 経由で投稿できない、なぜ?最近の WordPress は XML-RPC が初期値で有効になっているはずなのに。

PHP7 の初期値では XML-RPC が有効になっていない

WordPress 側のコード(class-IXR-message.php や class-IXR-server.php)にログを入れつつ確認していったところ原因が分かりました。

apache2 + PHP7 を入れているときに、PHP 拡張の XML-RPC が初期値では有効になっていません。このために、IXR_Message::parse() でエラーになっているらしいですね。xml_parser_create が呼び出せないようです。

openccpm.com は Ubuntu 上で動いているので、

sudo apt-get install php-xml php-xmlrpc

でインストールした後に、/etc/php/7.0/apache2/php.ini を開いて

extension=php_xmlrpc.dll

のように XML-RPC を有効にします。

すると、以下のように mt.supportedMethods で有効なメソッド一覧などが取れるようになります。

カテゴリー: 開発 | PHP7 上に WordPress を入れたときは php-xmlrpc を有効にする はコメントを受け付けていません

ユーザーコントロール内で更新した値を親の ViewModel に反映させる方法

MVVM でユーザーコントロールを作るときに微妙にハマるのがユーザーコントロールです。例えば、ユーザーコントロール内に TextBox を貼り付けておいて、外側の ViewModel からユーザーコントロールに値を設定することはできるのですが、逆にユーザーコントロール内の TextBox に入力された値を ViewModel の反映しようとすると難儀します。というか、難儀していたのだけど、解決できたので書き残しておきます。

image

UserControl に対して ViewModel 側からバインドしたい場合は、DependencyProperty を使って依存関係プロパティを作って設定します。表示のための TextBlock とか DataGrid を使ったリスト表示の場合は、これで十分なのであまり問題はないのですが、さて、UserControl 内に貼り付けた TextBox から ViewModel のプロパティに反映させるためにはどうしたものか?と考えてしまうわけです。

色々調べていくと、ElementName を使って TextBox.Text に直接バインドする仕組みが紹介されているのですが、この方法だと UserControl の独立性が低くなって汎用性が低くなります。じゃあ、標準の TextBox やその他のコントロールのがりがりにカスタムで組んでしまえばできそうなものなのですが、これもちょっと大変すぎる。

BindingOperations.GetBindingExpression を使う

.NET Framework の WPF の TextBox コードを直接眺めていくと https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/TextBox.cs,77a66fa4f401a49f? BindingOperations.GetBindingExpression というメソッドがあって、バインド関係の情報を取得することができます。バインド先のプロパティ名を示す Path プロパティや、結びついているオブジェクトを示す Target プロパティなどがあります。

コントロールから ViewModel への値の設定(コントロールからソースへの反映)は、主にコントロールからフォーカスが外れたときになるので LostFocus イベントで設定すると以下のようにシンプルに書けます。

this.text.LostFocus += (_, __) => {
    BindingExpression beb = BindingOperations.GetBindingExpression(this, YourNameProperty);
    if (beb != null)
    {
        beb.Target.SetValue(YourNameProperty, text.Text);
        beb.UpdateSource();
    }
};

バインドをしていないと、BindingOperations.GetBindingExpression メソッドは null を返すのでそれだけチェックをします。

バインド可能なユーザーコントロールを作る

image

2つのテキストボックスと1つの日付コントロールを持つユーザーコントロールを作ります。ユーザーコントロール内で MVVM してしまうと、MainView の DataContext と競合してしまうので、ユーザーコントロール内では x:Name で名前を付けて参照します。


public partial class PersonControl : UserControl
 {
     public PersonControl()
     {
         InitializeComponent();
         this.Loaded += PersonControl_Loaded;
     }

    public static readonly DependencyProperty YourNameProperty =
         DependencyProperty.Register(
             "YourName",
             typeof(string),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 "",
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         string v = (string)e.NewValue;
                         uc.text.Text = v;
                     }
                 })));
     public static readonly DependencyProperty YourAddrProperty =
         DependencyProperty.Register(
             "YourAddr",
             typeof(string),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 "",
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         string v = (string)e.NewValue;
                         uc.addr.Text = v;
                     }
                 })));
     public static readonly DependencyProperty YourBirthdayProperty =
         DependencyProperty.Register(
             "YourBirthday",
             typeof(DateTime),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 DateTime.Now,
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         DateTime v = (DateTime)e.NewValue;
                         uc.date.SelectedDate = v;
                     }
                 })));
         
     public string YourName { get => this.text.Text; set => this.text.Text = value; }
     public string YourAddr { get => this.addr.Text; set => this.addr.Text = value; }
     public DateTime? YourBirthday { get => this.date.SelectedDate; set => this.date.SelectedDate = value; }

    private void PersonControl_Loaded(object sender, RoutedEventArgs e)
     {
         this.text.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourNameProperty);
             if (beb != null)
             {
                 beb.Target.SetValue(YourNameProperty, text.Text);
                 beb.UpdateSource();
             }
         };
         this.addr.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourAddrProperty);
             if (beb != null)
             {
                 beb.Target.SetValue(YourAddrProperty, addr.Text);
                 beb.UpdateSource();
             }
         };
         this.date.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourBirthdayProperty);
             if (beb != null)
             {
                 if (date.SelectedDate != null)
                 {
                     beb.Target.SetValue(YourBirthdayProperty, date.SelectedDate);
                     beb.UpdateSource();
                }
             }
         };
    }
}

3つのコントロールが貼ってありますので多少コードが長くなっていますが、

  • DependencyProperty で、外側から設定できるプロパティを作る
  • YourName 等で、簡易アクセスができるプロパティを作る
  • LostFocus イベントで、内側から外の ViewModel に値を設定する

ということをやっています。ユーザーコントロールは3つのプロパティ

  • YourName
  • YourAddr
  • YourBirthday

を持ちます。

謎だったユーザーコントロール内の TextBox 等の入力コントロール(これ自体はバインド機能がなくてよい)から、外側の ViewModel にデータを渡すためには、

  • BindingOperations.GetBindingExpression でバインド先を探す
  • Target.SetValue で値を更新する
  • UpdateSourceでソース(バインド先のViewModel)に通知する

という手順になります。通知するタイミングは、サンプルの通り LostFocus イベントでもよいし、TextBox 内のテキストが変更されたタイミング、キータイプされたタイミングでも良いでしょう。

MainWindow から使う

image

メインウィンドウにユーザーコントロールといくつかのボタンを貼り付けます。ユーザーコントロールへのバインドは、独自に作ったプロパティにバインドさせます。

<local:PersonControl 
 YourName="{Binding Name}" 
 YourAddr="{Binding Addr}" 
 YourBirthday="{Binding Birthday}" 
 Margin="4" Grid.Row="1" Height="99" VerticalAlignment="Top"/>

後はふつうの TextBox などと同じように MVVM でバインドするだけですね。これだとユーザーコントロールは一般的なコントロールと変わらないので、


public partial class MainWindow : Window
 {
     public MainWindow()
    {
         InitializeComponent();
         this.Loaded += (_, __) =>
         {
             _vm = new MainViewModel();
             this.DataContext = _vm;
         };
     }
     MainViewModel _vm;

    private void clickSave(object sender, RoutedEventArgs e)
     {
         _vm.Output = $"{_vm.Name}  {_vm.Addr}  {_vm.Birthday.ToString()}"; 
     }

    private void clickLoad(object sender, RoutedEventArgs e)
     {
         _vm.Name = "MASDUA";
         _vm.Addr = "OSAKA";
         _vm.Birthday = DateTime.Parse( "2000/1/1");

    }
 }
 public class MainViewModel : ObservableObject
 {
     private string _name ;
     private string _addr;
     private DateTime? _birthday;
     private string _output;

    public string Name { get => _name; set => SetProperty(ref _name, value, nameof(Name)); }
     public string Addr { get => _addr; set => SetProperty(ref _addr, value, nameof(Addr)); }
     public DateTime? Birthday { get => _birthday; set => SetProperty(ref _birthday, value, nameof(Birthday)); }
     public string Output { get => _output; set => SetProperty(ref _output, value, nameof(Output)); }
 }

実行すると、こんな感じになります。

image

参考にしてみてください。

カテゴリー: C# | ユーザーコントロール内で更新した値を親の ViewModel に反映させる方法 はコメントを受け付けていません

正しい「PDCAサイクル」の知識を得るために

どうも巷で「PDCAを回す」ってのが相当誤解されているようなので、正しい知識としての「PDCAサイクル」の話を書き残しておきます。カッコつきなのは、正しい知識のほうに重きがあるので、PDCA サイクル自体が有用であるか否かはここでは論じません。というか、ノウハウってのは適材適所があるので、うまく開発プロセスの中(会社の独自文化もあるだろうし各々の状況もある)にはまり込めば「PDCA は使えるよ」って形になるけど、はまらなければ「PDCA は使えない」って形になるだけ。うまいところに使って正しい形で PDCA を活用してください(場合によっては活用しないでください)ってのが主旨です。

PDCA とは何か?

そもそも PDCA とは何か?という前提があるので(いろいろなパロディを面白がるためには元ネタの知識が必要なのだ)、念のため書いておきます。

  • Plan(計画)
  • Do(実行)
  • Check(計測、評価、検証)
  • Action(再計画)

ですね。おそらく、「Action」のところが曖昧になってしまい、PDCA 回すといいつつ、PDCPDCPDC になっているパターンが多いので、これは PDCA サイクルとは言いません。「サイクル」と言うからには、計画→実行→評価、をした後に、この評価や検証をもとにして計画を見直す(あるいは新しい計画を盛り込む)というアクションを取る、という意味での「Action」です。

この PDCA って言葉自体は、私が就職したころ(1980年代)から日本にあるので、結構前からあります。本家?アメリカではどういう扱いをしていたのか定かではないのですが、改善が「カイゼン」になる前からあったような気がします。

で、PDCA を「回す」という言い方をするので、この PDCA をくるくると回転させる(ときには高速に回転させると効率がよさそう?)という話もあるでしょうが、いえいえ、そういう意味での「回す」ではなくて、単純に循環しているという意味での「サイクル」です。なので、この Plan, Do, Check, Action という各プロセス(昔は、それぞれの工程のことを「プロセス」と呼びました。今だと、工程の流れや繋がりを「プロセス」と言いますね)を循環させてつなげていく、特に Action を入れることによって、評価されたものを次の計画に反映させるという「循環/サイクル」という意味で、「PDCA を回す」という言い方をします。

PDCA はイテレーション開発のことなのか?

No. 違います。「イテレーション開発」はできあがったものを顧客に見せて、次の開発のステップへ進むというスタイルで、開発プロセスそのものを示しています。PDCA サイクルは、評価から次の再計画へ、という小さなステップを踏む改善のサイクルでしかないので、開発プロセスとはイコールになりません。

具体的に PDCA の活用の仕方をみていきましょう。

  1. 計画を立てる。主にスケジュールや、何をやるとか、何を目標にするとかを決める。
  2. 実行する。計画に沿って実行する。実行するときは途中であれこれ変更しない。だって、計画に沿わないと計画自体の意味がない。
  3. 実行を計測する。どういう風にやったとか、どういうことをやったとか、どのくらいの時間がかかったとかを記録して、評価できるようにする。
  4. 記録をもとにして、次の計画に役立てる。記録は次に役立てるために記録しているのであって、次の計画に何にも関わらないのであれば、記録している時間が無駄。
  5. 最初の計画を、実行して記録した結果をもとに見直す、再計画する、修正する。またはそのままでよいと確認する。

というプロセスになります。なんで、ここに顧客の意見とか、アジャイル的に顧客を巻き込みながら実行時に意見を取り入れて、ということはしません。だって、計画からずれるじゃないですか?計画通りに実行しないと、計画自体が妥当であったかどうかが分からないので、きちんと計画通りに進めて途中で記録を取ります、そして計画通りに進んでいるかどうかをチェックして、次に役立てるわけです。ここで計画を見直します。実行して記録していたものを見たときに、無理な計画であったとか、ぞれは意味がない計画であったとか、無謀だったとかを「計画通りに実行する」ことによって確認するのです。

そう、ある意味では初期のスクラム開発に似ています。スプリントの期間を決めてしまい、最悪「スプリントの2週間が完全に無駄であってもよい」という考えのもとに開発を行います。これはスクラムマスターが外部からの雑音をメンバに伝えないための防護壁となるためです。同時に、開発者はスプリントの最初に建てた計画(この計画は2週間のうちに間違っていることがわかったとしても)の最大限の努力をもってして進めます。現在のスクラム開発はどうかよくわからないのですが、少なくとも初期のスクラム開発はこれでした。

PDCA で何がダメなのか?

PDCA サイクルの適用範囲外の部分を先に話しておきましょう。範囲外のところに適用しようと思っても無駄です。意味がないので、やめましょう、って話ですね。

  • 計画通りに実行しないと意味がありません。なので、計画がゆるゆるだと実行時に勝手に最適化するよう動いてしまいダメです。
  • なんらかの形で記録を取れないとだめです。勤務時間だとかステップ数の進捗度とかなんかの記録を取らないと、計画が無茶だったのか妥当だったのかがわかりません。
  • Action のところに重きがあるので「再計画」ができない計画に適用してはいけません。計画を見直す、計画の妥当性を見極めて改善するのが PDCA サイクルの主旨なのですが、そもそも最初の計画が動かない(短期スケジュールとか人を増やせないとか)という厳しい条件の場合には PDCA サイクルは意味を成しません。

なので、程よく計画プロセスと実行プロセスが分かれており、実行プロセスをチェックするする方法が確立して、それを踏まえたうえで次の再計画に意見できる、という組織でないと PDCA サイクルを使おうとしても無駄です。

PDCA の適用範囲は?

では、PDCA サイクルをうまく活用するためにはどういう条件が必要でしょうか?この「条件」を書いていない本や記事が多くて閉口するのですが、ノウハウというものは必ず適応条件というものが存在するのです。

  • 実行可能な計画を立てる。スケジュールや手順を作る。
  • スケジュールや手順に従って、実行してみる。途中で勝手にやり方を変えたりしない。
  • 実行には記録が伴う。コード量や開発スピードなどの記録を取る。
  • 記録をもとにして、計画が妥当であったのか、不具合があったのかを評価する。時にはよりよいアイデアを出して、より効率的に「実行」できる案を考える。
  • 再計画する。手順やスケジュール、タスクの組み方などが改善されている。

というのが PDCA サイクルの効率的な取り入れ方です。

PDCA サイクルは、そのサイクルが長いか短いかは関係ありません。問題になるのは Action から Plan へサイクルになっていることであって、PDCA を回すスピードは問題ではないのです。なので、PDCA サイクル自体がひと巡りするの1年以上かかるかもしれないし(例えば、決算や年度末への改善案を出せば、年1回しかプロセス改善機会がないですよね)、1日や1時間程度の単純なものかもしれません(筋力トレーニングの計画を立てて、それにそってトレーニングをやる。疲れすぎたり、楽すぎたりすれば、筋力トレーニングの計画を見直して新しいトレーニングをしますよね)。

これらを知っていれば、正しい PDCA サイクルの知識をもってして、取り入れるか取り入れないかの判断ができますよ、というお話でした。

カテゴリー: 開発 | 正しい「PDCAサイクル」の知識を得るために はコメントを受け付けていません

ジェラルド・M・ワインバーグ追悼

随分間が開いてしまったが、書かないとそのまま通り過ぎてしまいそうなので1時間ほど書き留めておこう。

「ライト、ついてますか」で有名なワインバーグではあるけれど、私にとっては「コンサルタントの道具箱」と「プログラミングの心理学」のワインバーグだったりする。一時期、彼の主催するコンサルタントの集まりに行きたいと思ったこともあったが、まあ、それは昔のことではある。

おそらく私がワインバーグ氏を知ってきちんと彼の本を読み込み始めたのは、ワインバーグの各著書がブームを過ぎた頃だと思っている。2000年頃に会社の受託開発での各プロセスに疑問を持った私は、なんらかの形で開発プロセスを改善したいと模索した。アジャイル開発のはしりもあり XP があり TDD もありオブジェクト指向もあり UML もありと様々な道具立てが出てきた頃でもあるのだが、私の興味はもっと人間の動きに近いところの「ピープルウェア」と心理学な分野であった。当時、すでにコンサルタントとして名を聞いていたワインバーグ氏のっ著書で最初に手に取ったのは「コンサルタントの道具箱」だったと思う(本棚には「スーパーエンジニアの道」や「ワインバーグのシステム変革法」などひととおり購入したので、どれが最初だったか覚えてはいない)。現在はコンサルタントではあるが、昔はプログラマだったというワインバーグに興味があった。ただし、あとで知ったところだが(プログラミングの心理学を読んだときだったが)、プログラマとして働いたのは1年半位の話で、そのあとはマネージャやコンサルタントとして働いているので「プログラマ」としてのワインバーグの意見はあまりあてにならない。時代も違うのだが。しかし、コンサルタントとしての視点は有効であろう、ということで「道具箱」なのである。

「道具箱」や「道具立て」というところが私にとって魅力的なのは、そういう暗喩を好んで使うところが好きだったからでもある。直接的なもの言いではなく(駄目とか良いとかではなく)、婉曲な言い回しはアメリカ人にしてはちょっと不思議な感じでもあったし、なんせよ「文学的」であった。言い回しを工夫することによって、相手が理解するように仕向けるという方法は「ライト、ついてますか」にたくさん書かれている。ちなみに、本のタイトルとなっている「ライト、ついてますか?」の意味だけが私にはかなりの間(5年間位?)わからなかった。これ「ライトをつけてください」とか「ライトをつけろ」という直接的な言い回しではなく、「ライトは、点灯しまいますよね?」という婉曲な確認の意味でなんだけど、もともと「ライト、ついてますか?」の婉曲な思考の持ち主だったので、これが婉曲な言い回しかどうかわからなかった、というオチだった。時にして、私の言うことがよくわからない、回りくどい、と言われることが多かったのだが、そもそもが婉曲に言うことに慣れていた(婉曲な言い回しを使うことによって、距離を置くという方法なのだけど)ので、それ自体に気づかなかったということでもある。自分の話なのに、自分のことではないような言い回しをする、ということだ。それは自分としては客観性を示しているつもりなんだけど、他人から見れば主体性のない言い逃れをしているように見えるらしい。

後から考え直したが、他人の婉曲な用法が分からないゆえに、自分独自の婉曲な用法とのずれのため、コミュニケーションが取れていない、という推測もできるし事実としてはそれっぽい。これは私自身では判断がつかない。

それはさておき、道具立てというのは、人の感情の起伏とは離れたところにある客観性を持つ。定規のようなものだ。物理的な計測というものが絶対的な価値を持つように(誰が測っても同じだし、時代が変わっても1mは1mだ)、目の前の出来事を一定の道具を使って比較をすれば、それは主観に引っ張られずに客観的にとらえられる。そして、客観的な事実をとらえたうえで主体的に判断すればよいのだ。2000年頃は絶対的な価値を求めて、TDD の正しさや一連の「ワインバーグのシステム何とか」を読み込んで目の前のプロジェクトからあれこれと読み取ろうとしていたものだが、今は違う。もっと緩やかなものだし、それぞれの多様な方法は多様な方法としてあるだけでいいと思う。だから、成功も失敗もその人あるいはグループあるいは会社に属するもので、誰かがとやかくと強制はできないものだ。だからこそ、誰からも強制される訳ではなく、ほどよく理論武装をしほどよくガードを保つ。正攻法で受けるのではなく、いなす。まあ、それこそが道具立てではあるので、そこは道具(冶具)の使いどころというところだろう。

「コンサルタントの道具箱」や「コンサルタントの秘密」には様々な道具立てが掛かれており、独自な語り口調で数々の「法則」が語られているのであるが、実のところは「孫子の兵法」や「風水」や「タロットカード」を知ることによって、ワインバーグ独自の「法則」をなめることをしなくてよいことが今の私はわかっている。これらのどれも「まんべんなく俯瞰して状況をみること」を示している。目の前の事実から何かを読み取ろうとするとき、人は何かを読み取ろうとするがゆえに、固定の事物に縛られがちになる。だから、ばらっと視野を広くして無理矢理にでも他の事象を合わせてみることが必要なのだ。進化論的に。このあたりのは話は、どんどんと突き詰めてしまうとあらぬ方向に行ってしまいそうなので、そこは「ワインバーグの道具箱」を引用させて貰うのがベターなところだろう。残念なことに「プログラミングの心理学」には、現在の心理学的な正しさ(主に実験心理学の統計的なアプローチ)は全くでてこない。正しいと思われるような「モデル」を組み立てたうえで、都合のよい事象に当てはめてみるという「似非心理学」に近いものあるのだが、それはそれで十分だ。彼の業績がそれによって薄められるということはない。以前、TED で話していたオリバー・サックス(すでに亡くなってしまったが)を見たときに「古さ」を感じたと同じものを、ワインバーグにも感じる。それはそれでよい。私たちは(少なくとも私は)、ワインバーグの語る心理学や数々の道具立てが古臭く見える程度には進歩してきたのだといえる。そして、ワインバーグの業績は、まさしくそれ自体を古くするための礎になっているといえる。だから、彼の言う心理学てなテストや数々の統計ちっくなものや気づきのポイントみたいなものは、そのまま現在に持ってくると古臭くて場合によっては間違っている。けれども、それらをアレンジして語ることはできる。本質を変えてしまって彼の言葉だけを借用してもよいし(ワインバーグを信奉する人ならばその方法は有用だ、若い人にはちんぷんかんぷんだろうが)、逆に本質だけよりわけて言葉を現代風にアレンジしてもよい。

「ワインバーグの道具箱」で私が好きな道具は「ジャムの法則」と「くすぐりの羽根」だ。サティアに信奉してたワンバーグは、実に前向きに物事を進めるのが好きだったようなのだが、ちょっとユーモアに頼りすぎる。でも、カート・ヴォネガット・ジュニアもユーモアが好きだったので、その程度は許するのが良いだろう。厳しい状況にあって悲壮な感じで努力をするよりも、楽観的に斜に構えて努力するが気は楽だ。怒号の中でひたすら平伏するよりも、正義の御旗を振りかざしながら反発するよりも、シニカルな目で客観視をして事実をとらえる方法が私には向いている。だから、ちょっとした想像力とユーモアが必要になる。オブラートに包むためにも。

ぼちぼち1時間になる。ワイバーグを中心にしたコンサルタント集団やカンファレンスはどうなったのだろう。そのあたりはあまり問題ではない。それこそが「イチゴジャムの法則」なのだから。

カテゴリー: 雑談 | ジェラルド・M・ワインバーグ追悼 はコメントを受け付けていません