Practice of Programming

プログラム とか Linuxとかの話題

GraphViz::DBI で ER図を吐き出す

この記事は, Perl Advent Calendar 201920日目の記事です。 19日は、doikojiさんの、「WINI & cal: perlベースの新しい簡易マークアップ言語WINIで来年のカレンダーを作りましょう! 」でした。

この記事は、もともとWanoグループのAdvent Calendar に書くつもりでしたが、Perl の Advent Calendarに空きがあったので、 会社の方はRustの記事を載せました。よろしければ、そちらもどうぞ。

qiita.com

閑話休題

最近、DocBaseGrowiGitlabWiki (設定すれば)などPlantUMLでの表示をサポートするものが増えてきています。 自分の手でGraphVizのER図を書くのも可能ですが、正直、だいぶ面倒ですし、テーブル数が多いとやる気が出ません。

PerlGraphViz::DBIでさくっと書いてみましょう。

ですが、僕の担当している、とあるシステムはテーブル数が1000近くある関係で、はきだされたものをそのままPlantUMLに渡すと、長すぎて壊れてしまいました。 そらそうですね。

これは同種のテーブルが負荷対策の関係で分けたりしているのが原因で非常に多くなっちゃってるのですが、もし同様なケースでしたら、似たようなテーブルはグルーピングするなりの処理をしたり、 ER図をいくつかのグループに分割するなどしたほうが良いですね(例えば、カテゴリだけをグルーピングするとか、特定機能に関わるテーブルだけをグルーピングするなど)。

下記のようにグルーピングの定義をしてやると良いと思います。 この時に、似たようなテーブルがいくつかある場合は、1つのテーブルのみをグループの中に入れてやれば良いでしょう。

tie my %GROUP_TABLES, 'Tie::IxHash';
%GROUP_TABLES = (
    # グルーピング => 表示したいテーブルの正規表現を書く(※正規表現は、`schema`.`table_name` にマッチするようにする)
    'グループ1'       => qr{},
    'グループ2'       => qr{},
  );

出力時に下記のようにすれば、各グループ毎にER図を出力できます。

foreach my $group (keys %GROUP_TABLES) {
    my $filter = $GROUP_TABLES{$group};
    my $g = __PACKAGE__->new($dbh);
    $g->{tables} = [ grep {$_ =~ $filter} $g->get_dbh->tables];
    my $txt = $g->graph_tables->as_text;

 # これは、表示を簡略化するためなので、グルーピングとは関係ないです。 
   # `schema`.`table_name` => table_name だけにしています
    $txt =~ s{`$SCHEMA`\.`(.+?)`}{$1}g;
    print "## $group\n\n";
    print "```plantuml\n\@startuml\n", $txt, "\n\@enduml\n```\n\n";
}

他にも、削除フラグとか更新日時とか、ほとんどのテーブルにあるけど、特に表示したくないものもあるでしょうから、

my %IGNORE_COLUMNS = (
  updated_at => 1,
  delete_flag => 1,
  created_at => 1,
);

のように定義しておいて、graph_tables をオーバーライドして、調整するとよいかもしれません(後のコードを参照)。

また、GraphViz::DBI の外部キーの実装は、下記のようになっています。

sub is_foreign_key {
    # if the field name is of the form "<table>_id" and
    # "<table>" is an actual table in the database, treat
    # this as a foreign key.
    # This is my convention; override it to suit your needs.

    my ($self, $table, $field) = @_;
    return if $field =~ /$table[_-]id/i;
    return unless $field =~ /^(.*)[_-]id$/i;
    my $candidate = $1;
    return unless $self->is_table($candidate);
    return $candidate;
}

コメントを訳すと、

フィールド名が、"<table>_id"で、"<table>"が実際にDBに存在する
テーブルの場合、これを外部キーとします。
これは、自分の慣例なので、必要に応じてオーバーライドしてください。

とした、ざっくりな感じなので、コメントの通り継承してオーバーライドしました。

my %SPECIAL_FOREIGN_KEY = (
   '特殊な命名の外部キー1' => "`$SCHEMA`.`テーブル`", # 外部キーの参照しているテーブル
   '特殊な命名の外部キー2' => "`$SCHEMA`.`テーブル`",
   # ...
);

sub is_foreign_key {
    my ($self, $table, $field) = @_;

    my $candidate;
    if (not $candidate = $SPECIAL_FOREIGN_KEY{$field}) {
    # TABLE_NAME_ID は TABLE_NAME の外部キーとみなす
        return unless $field =~ /^(.*)[_-]id$/i;
        $candidate = "`$SCHEMA`.`$1`";
    }
    return unless $self->is_table($candidate);
    return $candidate;
}

僕も同じような慣例なので、例外的なものを追加するくらいでOKでした。

全体像は、こんな感じになります。

use strict;
use DBI;
use parent 'GraphViz::DBI';

my $SCHEMA = "スキーマ";
my $dbh = DBI->connect("dbi:mysql:$SCHEMA;host=DB_HOST", 'user', 'password');

use Tie::IxHash;

tie my %GROUP_TABLES, 'Tie::IxHash';
%GROUP_TABLES = (
    # グルーピング => 表示したいテーブルの正規表現を書く
    'グループ1'       => qr{},
    'グループ2'       => qr{},
  );

my %SPECIAL_FOREIGN_KEYS = (
   '特殊な命名の外部キー1' => "`$SCHEMA`.`テーブル`", # 外部キーの参照しているテーブル
   '特殊な命名の外部キー2' => "`$SCHEMA`.`テーブル`",
   # ...
);

my %IGNORE_COLUMNS = (
  created_at => 1,
  delete_flag => 1,
  updated_at => 1,
);


sub is_foreign_key {
    my ($self, $table, $field) = @_;

    my $candidate;
    if (not $candidate = $SPECIAL_FOREIGN_KEYS{$field}) {
    # TABLE_NAME_ID は TABLE_NAME の外部キーとみなす
        return unless $field =~ /^(.*)[_-]id$/i;
        $candidate = "`$SCHEMA`.`$1`";
    }
    return unless $self->is_table($candidate);
    return $candidate;
}
sub graph_tables {
    my $self = shift;

    my %table = map { $_ => 1 } $self->get_tables;

    for my $table ($self->get_tables) {
        my $sth = $self->get_dbh->prepare(
            "select * from $table where 1 = 0");
        $sth->execute;
        my @fields = @{ $sth->{NAME} };
        $sth->finish;

        my $label = "{$table|";

        for my $field (@fields) {
            next if $IGNORE_COLUMNS{$field};
            $label .= $field.'\l';
            if (my $dep = $self->is_foreign_key($table, $field)) {
                $self->{g}->add_edge({ from => $table, to => $dep });
            }
        }

        $self->{g}->add_node({ name => $table,
                               shape => 'record',
                               label => "$label}",
                           });

    }
    return $self->{g};
}

foreach my $group (keys %GROUP_TABLES) {
    my $filter = $GROUP_TABLES{$group};
    my $g = __PACKAGE__->new($dbh);
    $g->{tables} = [ grep {$_ =~ $filter} $g->get_dbh->tables];
    my $txt = $g->graph_tables->as_text;
    $txt =~ s{`$SCHEMA`\.`(.+?)`}{$1}g;
    print "## $group\n\n";
    print "```plantuml\n\@startuml\n", $txt, "\n\@enduml\n```\n\n";
    # Growiならこっち
    # print "\@startuml\n", $txt, "\n\@enduml\n\n";
}

こんな感じでやると、下記のようなER図になります(自分で作ってたサービスのDBです)。

f:id:ktat:20191220111301p:plain PlantUMLで表示

ちなみに、GraphViz::DBIは、13年前の更新が最後ですが、最新のPerlでも動くと思いますよ(5.8.9 でも、5.25.1でも動いたので)。

これを定期的に回して、Wikiのページも自動的に更新とかしてやれば、良いかもしれません。 ER図をDBの情報を読み取って吐き出すツールは普通にありますが、テーブル数が多いと、使い物になりませんので、こんな感じで特定部分をフィルタリングして出すのも割と有用だと思いますよ。

21日のPerl Advent Calendar 2019はMorichanさんによる「PerlだけでWebサイトを作る - Qiita」です。

tcat という行頭に日時を付加するコマンドを作った

cat のように標準入力かファイルの中身を出力するものですが、先頭行に日時がつきます。
https://github.com/ktat/tcat にあります。

なんで、こんなものが必要かというと、例えば、サーバの様子を top -b を記録して後からみたいなーとかいう時とかに...

% top -b >> top.log

みたいにしたすると、ファイルの中身はこんな感じ。

top - 00:44:33 up 16:59,  7 users,  load average: 1.76, 1.75, 1.62
Tasks: 345 total,   1 running, 344 sleeping,   0 stopped,   0 zombie
%Cpu(s): 30.3 us, 11.1 sy,  0.0 ni, 57.9 id,  0.4 wa,  0.0 hi,  0.2 si,  0.0 st
KiB Mem : 16312908 total,   409000 free,  9663728 used,  6240180 buff/cache
KiB Swap: 16657404 total, 16655344 free,     2060 used.  4595576 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 9618 ktat      20   0   42952   3800   3140 R  12.5  0.0   0:00.03 top
 5253 ktat      20   0 1745952 424904  69868 S   6.2  2.6   6:14.04 slack
 5306 ktat      20   0 1497416 376924 181836 S   6.2  2.3  17:12.10 slack
    1 root      20   0  185412   6012   3944 S   0.0  0.0   0:04.64 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.04 kthreadd

後から見ようとした時に、どの時間のものだったのか調べようとすると....

top - 00:44:33 up 16:59,  7 users,  load average: 1.76, 1.75, 1.62

ここにあるけど、とっても使いにくいし、日またいだら、どうするんだという感じなので...

% top -b | tcat >> top.log

とすることで、

2017-01-25 00:48:25: top - 00:48:25 up 17:03,  7 users,  load average: 1.29, 1.63, 1.62
2017-01-25 00:48:25: Tasks: 341 total,   1 running, 340 sleeping,   0 stopped,   0 zombie
2017-01-25 00:48:25: %Cpu(s): 30.1 us, 11.0 sy,  0.0 ni, 58.3 id,  0.4 wa,  0.0 hi,  0.2 si,  0.0 st
2017-01-25 00:48:25: KiB Mem : 16312908 total,   582792 free,  9512076 used,  6218040 buff/cache
2017-01-25 00:48:25: KiB Swap: 16657404 total, 16655352 free,     2052 used.  4772668 avail Mem 
2017-01-25 00:48:25: 
2017-01-25 00:48:25:   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
2017-01-25 00:48:25:  9850 ktat      20   0   42952   3732   3076 R  12.5  0.0   0:00.03 top
2017-01-25 00:48:25:     1 root      20   0  185412   6012   3944 S   0.0  0.0   0:04.64 systemd
2017-01-25 00:48:25:     2 root      20   0       0      0      0 S   0.0  0.0   0:00.04 kthreadd
2017-01-25 00:48:25:     3 root      20   0       0      0      0 S   0.0  0.0   0:08.62 ksoftirqd/0
2017-01-25 00:48:25:     5 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H

のように記録される(日付のフォーマットは自由に変えられます)ので、

% grep '2017-01-25 00:48' top.log

のようにして、検索しやすくなります。

で、作ってから気づいたのですが、まったく同じ名前の同様のコマンドが C で書かれていました。
https://github.com/marcomorain/tcat

orz

とりあえず、違いを出すために、cat のオプションを全て実装してみました("-n" 以外使ったことないけど)。
"-v", "-E", "-T" あたりの実装は、cat のソースコードからロジックをそのまま移した感じになります。

Go言語さわってみた

一年前(2014年5月末の作成日の.goなファイルがあった)くらいにGo Tourを途中までやったけど、ほとんど覚えてなかったです。
ディレクトリとか環境変数とかは、前若干コード書いたので、それっぽく残ってました。emacsのgo-modeも入ってた。
という状況から、ちょうど一週間くらいたった感じです。

作ったもの

https://github.com/ktat/go-coloring テキストを正規表現で色付けするもの。そういうツールってあるっけ?
https://github.com/ktat/go-pager ↑のやつにlessっぽくしようとしたpagerを組み込んでいたけど、分離した(go-termboxを利用)。

コードは、まぁ、まだまだアレというか間違ってる/分かってない可能性が高いです。

参考にしたところ/したいところ

golang.org には書いてるけど、golang.jp には書いてないこともあったので、
golang.org 見たほうが良いんじゃないかなとか思う。

作法

変数/関数の命名規則などが慣習も含め決まっているので先に読んだほうが良いと思う。

https://golang.org/doc/effective_go.html#names

例えば、セッターは

 SetParamName

だが、ゲッターは

 GetParamName

ではなく、単純に

 ParamName

とする。

パブリックなものは大文字から、プライベートなものは小文字の名前をつける(メソッドも構造体の変数も)

gofmt というコマンドがあるので、それで整形を行う(必ず)。インデントはタブです。

コンパイル時のチェック

使ってないパッケージが import されてたり、宣言されただけで使ってない変数とかがあると、コンパイル通らない。

ファイル構成

詳しくはこちらをご覧ください。
https://golang.org/doc/code.html

 go/src/github.com/ユーザー名/プロジェクト名/ソースコード.go

にしておいて、

 GOPATH=/home/USER_NAME/go

としておくと、$GOPATH/src/ $GOPATH/pkg/ 以下にソースコードコンパイル済みのライブラリがあると想定する。

 import (
   "github.com/mattn/go-getopt"
 )

のように import すると、

 /home/USER_NAME/go/src/github.com/*/
 /home/USER_NAME/go/pkg/ARCHITECTURE/github.com/*/

以下の *.go, *.a ファイルが読まれるもよう。

コンパイル

  % go build 名前.go

  名前

の実行ファイルができあがる。

即時実行したければ、

 % go run ファイル名 引数

とすればよい。今のところ go run ばっか使ってます。

クォート

シングルクォートはルーン(rune)というもの(int32のalias)で、ユニコードのコードポイント

 fmt.Println('a') // 97 == 0x61

ダブルクォートはstring 。byte(uint8 の alias)の配列

 "12345"
 "12345"[1] == '2'

以下のような比較はエラー

 "あ"[0] == 'あ' // overflows uint8

"あ"[0] の0番目のバイト(uint8)と 'あ'(rune(int32のalias))の比較になるため

バッククォートは複数行にまたがって書ける。

  fmt.Println(`...
 ...
 `)

println

プリントデバッグで使ったりしますが、長い文字列を渡すと

 [string too long]

と文句言われる。

 fmt.Println(`...
 ...
 `)

は問題ない。

main

実行したいというときは、main package に main 関数をつくればよい。

// test.go
package main
    
func main () {
    // なんか書く   
}

そして、以下で実行できる。

go run test.go

変数宣言

 var hoge string = "abcd"
 hoge := "abcd"

:= は型推論してくれる。
最初の代入で使うものなので、同一スコープで := を再度使うとエラー。var hoge の後に := 使ってもエラー。

スコープはブロックスコープ(https://golang.org/ref/spec#Declarations_and_scope)なので、以下はセーフ。

	var str string = "abcde"
	println(str[3:4])
	{
		str := "eee"
		println(str)
	}

以下のように複数の宣言もできる。

 var (
   hoge string
   foo bool
 )

トップレベルで宣言するときは、:= は使えない。

 var hoge string
 var hoge string = "aaa"

blank idenitifier

"_"はblank idenitifier(https://golang.org/ref/spec#Blank_identifier)であり、無視される。

  i, _ :=  strconv.Atoi("12")

配列

	array := make([]string, 0)

追加するときは

	array = append(array, "hogehoge")

map

ハッシュ的なもの。
https://blog.golang.org/go-maps-in-action

  dict := map[string]string {
      "key": "value"
  }

  result := make(map[string]string)
  result["a"] = "b"
キーの存在チェック
 if _, ok := dict["key"]; ok {
    println("key exists")
 }

http://stackoverflow.com/questions/2050391/how-to-check-if-a-map-contains-a-key-in-go

キーの削除
 delete(mapVar, "a")
map の値に関数

はもたせられない(nested func not allowed と言われる)

 map[string]func

みたいな定義はできるけど、代入したらエラーになる。
定義できる意味ないんじゃ...?
牧さんからコメントいただいて、出来るということです。
http://play.golang.org/p/voWrwbQ2oe

 test := map[staing]func() {}
 test["a"] = func () {println(1)}
 test["a"]()

みたいに、引数とかを書いてなかったからだそうです。言われてみれば、そらそうだーという感じがしました。
funcの定義を書くわけなので、別のタイプの関数は取れません。

	test := map[string] func() {}
	test["aiueo"] = func() {println(123)} // これはいいけど
	test["akasa"] = func(x string) {println(x)} // これはダメ

最初にググってでてきた、interface を使う方法は、どんなタイプの関数でも入れれるといえば入れれる(使いドコロがあるのかは知らないけど)

	test := map[string] interface{} {}
	test["aiueo"] = func() {println(123)}
	test["akasa"] = func(x string) {println(x)}
	test["aiueo"].(func () )()
	test["akasa"].(func (x string))("a")

追記終わり

interaface とやらを使うらしい(http://stackoverflow.com/questions/6769020/go-map-of-functions)。

	colorFunc := map[string]interface{}{
		"red":    func(s coloring.String) string { return s.Red() },
		"green":  func(s coloring.String) string { return s.Green() },
		"blue":   func(s coloring.String) string { return s.Blue() },
		"yellow": func(s coloring.String) string { return s.Yellow() },
		"white":  func(s coloring.String) string { return s.White() },
		"cyan":   func(s coloring.String) string { return s.Cyan() },
		"black":  func(s coloring.String) string { return s.Black() },
		"purple": func(s coloring.String) string { return s.Magenta() },
	}

これを呼ぶときは、

	colorFunc["red"].(func (colorling.string) string)(val)

のように呼ぶ。

メソッドリストのない空のinterface は何が来てもOKだから、func も入るということかと思う。

型変換

	[]byte(str)
	string(someByte)

数値 <-> 文字列

  str := strconv.Itoa(12)
  i, err :=  strconv.Atoi("12")

文字列連結

 "hoge" + "fuga"

関数

  func 名前 (型) 戻り型 {
       return ...
  }

戻り値が複数の時は

  func 名前 (型) (戻り型,戻り型) {
       
       return ...
  }

戻り値にも名前をつけられる

  func 名前 (型) (名前 戻り型, 名前 戻り型) {
       
       return // 変数を渡さなくても良い
  }

パッケージの関数

 packageName.FunctionName()

で呼べる。

パッケージの型とメソッド

 package hogehoge

 type String string

 func (i String) Name String{
      // ...
 }

とかすると

 hogehoge.String("hoge").Name()

とかって使える。

ループ

for しかない

ノーマル
 for i := 0; i < len(str); i++ {

 }
無限ループ
 for {

 }

break で止める
continue で次のループへ
goto ラベルへ

break, continue も ラベルを取れる。
ラベルは以下のように記述。

 Loop: 
配列の取り出し

indexのみを取る

 for i := range array { }

indexと値

 for i, v := range array { }
map取り出し

キーのみ

 for k := range array { }

キー、バリュー

 for k, v := range map { }

if

 if len(str) == 0 {
 
 } else if ... {
 
 } else {
  
 }

条件は bool でないとダメ

switch

case のところは同一タイプじゃないとダメらしい。

        switch c {
        case 'f':
                fileName = OptArg
        case 'h':
                usage()
                os.Exit(1)
        default:
		if color, ok := colorMap[string(c)]; ok {
			replace = append(replace, fmt.Sprintf("(?P<%s>%s)",  color, OptArg))
		} else {
			os.Exit(1)
		}
        }

三項演算子

ありません。

substring

スライスで取る。

	var str string
	str = "abcde"
	println(str[0:3]) // abc
	println(str[3:4]) // d
	println(str[4:5]) // e

文字列分割

    strings.SplitN(str, sep, -1)

最後の引数はsplitする数。-1だと以下と同じ。

    strings.Split(str, sep)

http://golang.org/pkg/strings/#SplitN

文字列連結(strings.Join)

    strings.Join(array, sep)

http://golang.org/pkg/strings/#Join

正規表現

関数がいっぱいあるなー。

http://tip.golang.org/pkg/regexp/
http://tip.golang.org/pkg/regexp/syntax/
https://gobyexample.com/regular-expressions

regexp.Compile

変な正規表現渡しても死なないので、自分でハンドリング。

        re,regexpErr := regexp.Compile(pattern)

        if regexpErr != nil {
                log.Fatal(regexpErr)
		// しかし、log.Fatal や os.Exit は使わないほうがいいらしい
        }
regexp.MustCompile

変な正規表現渡すとエラーで死ぬ。
どっかからもらったんじゃなくて、自分で書いた正規表現はこっちで良いでしょう。

        re := regexp.MustCompile(pattern)
置換

以下、全て同じ

   regexp.MustCompile("^(a)(b)").ReplaceAllString("abcdef", "$2$1")
   regexp.MustCompile("^(a)(b)").ReplaceAllString("abcdef", "${2}${1}")
   regexp.MustCompile("^(?P<first>a)(?P<second>b)").ReplaceAllString("abcdef", "$second$first")
 (?P<名前>パターン)

は、名前付きキャプチャ。

マッチしたものを後で使う
	re := regexp.MustCompile("^(?P<first>a)(?P<second>b)")
	match := re.FindSubmatchIndex([]byte("abcdefab"))

	var dst []byte
	dst = re.ExpandString(dst, "$second$first", "abcdef", match)
	fmt.Println(string(dst))

http://astaxie.gitbooks.io/build-web-application-with-golang/content/ja/07.3.html

regexp関係のFuncと付く関数でやっても意味ないと思われる。

フラグは先頭につける
 (?フラグ)パターン

以下のような感じ。

 (?im)^[a-z]+$

エラーハンドリング

error という型がある。log.Fatal に投げるとエラーメッセージを出してくれる

	log.Fatal(err)
	// 既述ですが、log.Fatal や os.Exit は使わないほうがいいらしい

以下を読むと良いのかもしれません。
http://blog.golang.org/error-handling-and-go

ファイルの読み取り

	whole, err =  ioutil.ReadFile(ファイル名)

http://golang.org/pkg/io/ioutil/

標準入力全部読み

	whole, err = ioutil.ReadAll(os.Stdin)

http://golang.org/pkg/io/ioutil/

一行ずつ

	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		var l = scanner.Text()
	}

http://qiita.com/hnakamur/items/a53b701c8827fe4bfec7

コマンドライン引数

flag パッケージ

標準で flag というパッケージががある(使わなかったので見てないです)

https://golang.org/pkg/flag/

go-getopt

mattnさんの mattn/go-getopt が使える

https://github.com/mattn/go-getopt

example に使い方あり。

https://github.com/mattn/go-getopt/blob/master/example/example.go

 OptEror = 0 

にすると、エラーメッセージ(おかしなコマンドライン引数を渡した時とか)がでなくなる。
おかしなコマンドライン引数を渡した時を判別する場合は、switch の default でやれば良いかも。

ポインタ

以下のような感じで戻り値を受け取らずに、files にファイルを突っ込める。
ただ、こういうケースでポインタ使うのは間違いらしい。
http://qiita.com/ruiu/items/e60aa707e16f8f6dccd8

  seekDir(&files, "./")
  
  func seekDir (files *[]string, dirName string) {
  	fileInfo, ioerr := ioutil.ReadDir(dirName)
  	errCheck(ioerr)
  	for i := 0; i < len(fileInfo); i++ {
  		if fileInfo[i].IsDir() == false {
  			*files = append(*files, dirName + "/" + fileInfo[i].Name())
  		} else {
  			seekDir(files, dirName + "/" + fileInfo[i].Name())
  		}
  	}
  }
 &変数

が、ポインタ

 *変数

で、デリファレンス

メソッド

型に対してメソッドを作れる

 type S string
 
 func (s *S) aaaa () {
      fmt.Println("=== S aaaa ===")
      fmt.Println(s)
 }
 
 func main() {
      var str S = "Type S"
      S.aaaa()
 }

struct で名前なしで型を埋め込むことで、埋め込んだ型のメソッドを委譲できる。

 type S2 struct {
      str string
      S
 }
  
 func (s *S2) bbbb () {
      fmt.Println("=== S bbbb ===")
      fmt.Println(s.str)
 }
 
 func main() {
      var str S2
      S2.str = "Type S2"
      S2.aaaa() // 委譲したもの
      S2.bbbb() // S2のメソッド
 }

以下のように "*S2" じゃなくて、"S2" のようにもかけるけど、そうすると、メソッド内でメンバを書き換えたりしても、呼び出し元は変わらない。

 func (s S2) cccc() string{
   s.str = "cccc"
   println(s.str) // cccc
   return s.str
 }
 
 var s S2
 s.str = "s2"
 println(s.cccc()) // cccc
 println(s.str)    // s2

Go言語はオブジェクト指向かどうかという話は、以下。
http://golang.jp/go_faq#Is_Go_an_object-oriented_language

インターフェイス

インターフェースはメソッドの羅列。インターフェースに定義されているメソッドを実装すれば、
インターフェースを受け取れる関数を受け取れる。
※ちなみに以下は微妙な例になっておりますが、書き直すのがだるいのでそのままです。

Animalizer インターフェースを実装してみた

Moveメソッドがインターフェースのメソッドとして定義されている。
AsAnimal メソッドは引数に Animalizer インターフェースを受け取れる。

  package animal
  
  import "fmt"
  
  type Animalizer interface {
  	Move()
  }
  
  type Animal struct {}
  
  func (a Animal) Move (){
  	fmt.Printf("move as animal %T\n", a);
  }
  
  func AsAnimal (a Animalizer) {
  	fmt.Printf("animal is passed %T\n", a)
  }
Humanizer インターフェースを実装してみた

Talk と Move メソッドがインターフェースのメソッドとして定義されている。
AsHuman メソッドは引数に Humanizer インターフェースを受け取れる。

  package human
  
  import "fmt"
  
  type Humanizer interface {
  	Talk()
  	Move()
  }
  
  type Human struct {}
  
  func (h Human) Move (){
  	fmt.Printf("move as human %T\n", h);
  }
  
  func (h Human) Talk (){
  	fmt.Printf("I'm human %T\n", h)
  }
  
  func AsHuman (h Humanizer) {
  	fmt.Printf("human is passed %T\n", h)
  }
使ってみる
  package main
  
  import (
  	"github.com/ktat/test/animal"
  	"github.com/ktat/test/human"
  )
  
  var a animal.Animal;
  var h human.Human;
  
  func main () {
  	a.Move()
  	h.Move()
  	h.Talk()
  	animal.AsAnimal(a)
  	animal.AsAnimal(h)
  	human.AsHuman(h)
  }

結果

move as animal animal.Animal
move as human human.Human
I'm human human.Human
animal is passed animal.Animal
animal is passed human.Human
human is passed human.Human

仮に、

  	human.AsHuman(a)

なんてことをすると、

 ./test.go:18: cannot use a (type animal.Animal) as type human.Humanizer in function argument:
         animal.Animal does not implement human.Humanizer (missing Talk method)

と怒られる。

インターフェイスに定義されたメソッドを実装することで、同一のインターフェースのものということになる。
ロボット型に人のインターフェースをもたせれば人として扱える。
なんか面白いですね。

ちなみに、命名規則的に interface は、-er でつけて、- なメソッドを持つ(Stringer は、Stringを持つ)となってるので、上記は変な例だと思います。

例えば、Stringer インターフェースは、

 func String string {}

なメソッドを持っています。

ついでに説明すると、適当な型に

 type AnyType {
   str string
 }
 
 func (a AnyType) String string {
    return a.Str
 }

のようなメソッドを作ってやれば
fmt.Printf は、Stringer インターフェースを受け取ることができるため、

 var a AnyType
 a.Str = "1234"
 fmt.Printf("%s", a)

と渡すことが出来ます。

なお、以下のようなメソッド定義だと、なんでも受け取れるようになります。

 func (h Human) Any (i interface{}) {
 
 }

なお、インターフェースを満たすというのはメソッドを満たせばいいだけなので、単純に型を埋め込んでやるだけでも、インターフェースのメソッドを満たすことが出来ます。

type Human struct {
	animal.Animal
}

インターフェースを満たすだけであれば、これだけでも良いわけですが、結果はこうなります。

move as animal animal.Animal
move as animal animal.Animal   // さっきは、 move as human human.Human
I'm human human.Human
animal is passed animal.Animal
animal is passed human.Human
human is passed human.Human

メソッドは委譲されていますが、もともと、

func (a Animal) Move (){
	fmt.Printf("move as animal %T\n", a);
}

という定義なので、a は Animal なのです。


次は、gorutine とかチャンネルとか触ってみると思います(書くかは分かんないけど)。

翻訳 CPAN::Meta::Spec、Module::CPANFile、Minilla、Milla::Tutorial、cpanm、Carton、plenv、plenv-contrib

CPANまわりのツールたちの開発が活発なので、翻訳しようと前々から思ってたのですが、ようやく翻訳しました。

最初は、cpanfile とMinillaだけのつもりだったんですが CPAN::Meta::Specも文章中に出てくるし、Minilla::Tutorialは、Milla::Tutorialから取ってるとのことだし...ということで、Milla::Tutorialも訳しました...と、芋づる式に色々訳す結果となりました。
追記: cpanm も訳しました。
追記2: Carton も訳しました。
追記3: plenv & plenv-contrib も訳しました。

scalarコンテキストでのsortの振る舞いは未定義

何年も使ってるのに、基本的な関数知らないとか!とか、思った。

同僚がはまっていたので知ったのですが、sort のスカラコンテキストでの振る舞いは未定義だそうです。実際問題、sort をスカラで受けるとか意味わからない。ちゃんと警告も出ます。

Useless use of sort in scalar context at -e line 1.

そもそも、リストをスカラで受けるのは、かなり嫌な感じになるため、経験的に避けます。

 my $item = qw/a b c/;

$item に入るのは、リストの最後の要素、'c' なのですが、意図してこんなコードは書きませんよね。
ちなみに、

 my ($item) = qw/a b c/;

の場合は、$item に入るのは、'a' です。

grep, map に関しては、要素数が取れるので、また違った感じですが、要素数を取る目的ではあんまり使った覚えがない。grep を scalar コンテキストで使う場合は、だいぶ昔は if の中とかで使ってた気がするけど、そういうケースでは、List::Util の any のほうが良いですね。

if (any {$_ > 1} @array) {
  # ...
}

map をスカラコンテキストで使った覚えがないなぁ。使うケースが思いつかない。


で、sort なのですが、(Ubuntu 13.04で、Perl 5.16.3 と 5.12.3 で試しました)

 my $item = sort qw/a b c/;

$item は空になります。perldoc -f sort では、

sort SUBNAME LIST
sort BLOCK LIST
sort LIST
リストコンテキストでは、LIST をソートし、ソートされたリスト値を返します。 スカラコンテキストでは、sort() の振る舞いは未定義です。

http://perldoc.jp/func/sort

「 スカラコンテキストでは、sort() の振る舞いは未定義」なので、空になる保証もないわけですが。
そもそも、リストコンテキスト以外は意味ないからsort自体の処理に入らないようです。
最初の警告の説明には、以下のように書かれています。

Useless use of sort in scalar context
(W void) こんな風に、ソートをスカラコンテキストで使いました:

my $x = sort @y;
これは全く便利ではないので、perl は現在のところ最適化して取り除きます。

http://perldoc.jp/docs/perl/perldiag.pod

以下の通り。

 $ perl -e 'scalar sort {die($a) } (1,2); ' # scalar context
 $ perl -e 'sort {die($a) } (1,2); ' # void context
 $ perl -e 'my @x = sort {die($a) } (1,2); ' # list context
 2 at -e line 1.

dieしているのは、最後のものだけでした。

どうでも良いのですが、リスト操作系の関数でスカラコンテキストでは意味無さげな reverse はどうなんだろうと思ったら、こんな感じでした。

 print scalar reverse "dlrow ,", "olleH";    # Hello, world

どこで使うのかと小一時間悩みましたが、回文かどうかチェックするのが簡単でした(引用: http://kaibun21.jp/works/13200/)

 perl -CA -e 'print join("", @ARGV) eq reverse(@ARGV);' かいあたえ しつくしくつし えたあいか

もはや、記事名となんの関係もないけど、最後にまとめると、

リスト ... 最後の要素
配列 ... 要素数
sort ... 未定義
grep ... 要素数
map ... 要素数
reverse ... 引数の順番も中身もひっくり返す

て、感じでした。

Teng::Plugin::SearchBySQLAbstractMore 0.10 と 0.11 をリリース

https://metacpan.org/module/Teng::Plugin::SearchBySQLAbstractMore

0.10 は先日ご報告をうけた、search_by_sql_abstract_more メソッドの挙動が、search と違うよっていうバグの修正です(ちなみに、僕はこのバグに依存したコードを書いちゃってたので、修正が必要でした orz)。
0.11 はTengのバージョンが0.17に上がって、_execute メソッドじゃなくて、execute を使いなさいとwarningが出るようになったので、その修正です。
なんで、0.11 は、Teng 0.17 を要求します。別に上げても問題ないと思いますが、一応注意ということで。
(うちのプロダクション環境で、Teng 0.17を動かしてますが、特に問題はありません)。

Teng::Plugin::SearchBySQLAbstractMore 0.09 リリース

https://metacpan.org/module/Teng::Plugin::SearchBySQLAbstractMore


変更点は、create_sql_by_sql_abstract_more ていうのを生やしたのと、Teng::Plugin::SearchBySQLAbstractMore::Pager::Countの改良。


create_sql_by_sql_abstract_more は、そのまま、Tengのsearch形式の引数か、SQL::Abstract::Moreのselectの引数で、単にSQL生成するだけのものです。


Pager::Countは、改良したのですが、レコード多いと使えないので、MySQL使ってるなら、Pager::CountOrMySQLFoundRows か、Pager::MySQLFoundRowsを使ったほうが良いと思います。
(前者は、通常は COUNT(*) を使って、GROUP BY を使ってる時のみ、SQL_CALC_FOUND_ROWSを使います)
まぁ、SQL_CALC_FOUND_ROWS もあんまり評判は良くないですけどね。