2017/03/09


以下の記事は Java について触れていますが、Java を dis っている訳でもありませんし、冗長に見える例を意図的に使っています。

最近 Twitter で golang に Generics が無い事についてずいぶんと盛り上がったのですが、僕の意見をこのブログにも書いておこうと思います。

僕も煽り言葉で「golang に generics は要らない」と書いた事もありましたが、本心は「Generics 欲しいと思った事はあるけど無くても生きてこれた」というところです。

「golang の型システムは貧弱だ、Generics が無いから駄目だ」、そんな意見をたまに見ます。実際どんな場合に Generics が欲しいかというと、ある特定の意味を持った型を手続き処理に渡したい場合です。例えばその一つがコンテナです。Java の List<SomethingType> などがそれにあたります。この List の中身をぐるっと回して処理したい、または処理する関数を以降ほかの型でも使用したい、といった物です。golang に Generics が欲しいと言っておられる方の多くはこれが欲しいと言っておられるのだと思います(違っていたらごめんなさい)。例えば整数か浮動小数点か分からない数値をコンテナに持たせてその合計を出す処理を考えてみます。

import java.util.List;
import java.util.Arrays;

public class Foo {
    public static Integer sum(List<Integer> list) {
        int ret = 0;
        for (Integer i : list) {
            ret += i;
        }
        return ret;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(5,2,3,1,4);
        System.out.println(sum(list));
    }
}

Java だとこんなところでしょうか。golang だと container パッケージもありますが、slice で処理できる物は slice のまま扱いますね。さて、この Java のコードを再利用可能にしてみます。

import java.util.List;
import java.util.Arrays;

public class Foo {
    public static <T> T sum(List<T> list) {
        int ret = 0;
        for (T i : list) {
            ret += i; // コンパイルエラー
        }
        return ret;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(5,2,3,1,4);
        System.out.println(sum<Integer>(list));
    }
}

ここまでは想像できますよね。コメントにも書きましたが、Java もアドホック多相のまま演算子を処理するコードはコンパイル出来ないのです。そうなると「足し算する関数を持ったインタフェース」が必要になる訳です。java.lang.Number にもそんな物は定義されてないので、自分で意味づけする必要がある訳です。アドホック性を保ったまま Java で実装するとどうなるでしょう?

import java.util.List;
import java.util.Arrays;

interface MyNumber {
    MyNumber add(MyNumber rhs);
}

class MyInt implements MyNumber {
    private int x;
    MyInt(int n) {
        this.x = n;
    }
    public MyNumber add(MyNumber rhs) {
        return new MyInt(this.x + ((MyInt)rhs).x);
    }
    public int value() {
        return this.x;
    }
}

public class Foo {
    public static MyNumber sum(List<MyNumber> list) {
        MyNumber ret = list.get(0); 
        for (int i = 1; i < list.size(); i++) {
            ret = ret.add(list.get(i));
        }
        return ret;
    }

    public static void main(String[] args) {
        List<MyNumber> list = Arrays.asList(
            new MyInt(5),
            new MyInt(2),
            new MyInt(3),
            new MyInt(1),
            new MyInt(4)
        );
        System.out.println(((MyInt)sum(list)).value());
    }
}

おや、なんかめんどくさくなりましたね。Double 版も作りたくなったとしたらなんか大変そうだしもし Int と Double を足せる物を実装するとなったらもっと大変そうに見えませんか?golang でも同じ事をやってみましょう。

package main

import (
    "fmt"
)

type Numeric interface {
    Add(Numeric) Numeric
}

type Int int

func (i Int) Add(n Numeric) Numeric {
    switch t := n.(type) {
    case Int:
        return i + t
    }
    panic("unknown type")
}

func sum(list []Numeric) Numeric {
    ret := list[0]
    for i := 1; i < len(list); i++ {
        ret = ret.Add(list[i])
    }
    return ret
}

func main() {
    list := []Numeric {
        Int(5),
        Int(2),
        Int(3),
        Int(1),
        Int(4),
    }
    fmt.Println(sum(list))
}

Java も golang も意図的に冗長に書いているので、なんだそのコードはといったご意見もあるかと思います。

上記で Int と Double を足せる物を作る場合について触れましたが、これ golang でやってみたいと思います。

package main

import (
    "fmt"
)

type Numeric interface {
    Add(Numeric) Numeric
}

func sum(list []Numeric) Numeric {
    ret := list[0]
    for i := 1; i < len(list); i++ {
        ret = ret.Add(list[i])
    }
    return ret
}

type Int int

func (i Int) Add(n Numeric) Numeric {
    switch t := n.(type) {
    case Int:
        return i + t
    case Float:
        return i + Int(t)
    }
    panic("unknown type")
}

type Float float64

func (f Float) Add(n Numeric) Numeric {
    switch t := n.(type) {
    case Float:
        return f + t
    case Int:
        return f + Float(t)
    }
    panic("unknown type")
}

func main() {
    list := []Numeric {
        Float(5),
        Int(2),
        Int(3),
        Float(1),
        Int(4),
    }
    fmt.Println(sum(list))
}

そんなに複雑じゃないと思いませんか?上記でも書いた通り、Java もアドホック性を保ったままコンテナを使うには interface を作らないといけないのです。それは golang も同じ話なのです。

やりたい内容によっては Java も golang もフェアなのです。さらに言うなら Duck Type を使ってシグネチャさえ満たせば interface を引数に持った手続き処理に渡せるのは、Java にないメリットになり得ます。

僕は golang も Java も C++ も好きです。Generics が便利なのも知ってます。この記事も Java を dis っている訳ではないです。ただ言いたいのは「golang は型が貧弱だ、Generics を実装しないのは開発者の怠慢だ」といった意見に同調する人たちに「golang は Generics が無くても事足りてしまう事がある」という1例を見せたいだけなのです。もちろんリフレクションや Object のまま扱えばもっと短くできる事は知っています。また本当に Generics が欲しくなるケースもあるのは事実です。ただ、そんな荒さがししてるくらいなら、まず golang に触れてみたらいいんじゃないか、そう思う訳です。

追記

例えが偏り過ぎた事もあって反応頂いてるみたいです。

golang と Generics と吾 - Qiita
http://qiita.com/yuroyoro/items/6bf33f3cd4bb35469e0b
Java の Generics にもの思い - Qiita
http://qiita.com/t2y/items/139c6a38173d7750ddfc
みんなのGo言語【現場で使える実践テクニック】 みんなのGo言語【現場で使える実践テクニック】
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧 大輔, 鈴木健太, 稲葉貴洋
技術評論社 大型本 / ¥3,924 (2016年09月09日)
 
発送可能時間:

Posted at by



2017/02/27


元ネタはずいぶんと昔の記事なのだけど。

編集距離 (Levenshtein Distance) - naoyaのはてなダイアリー

■ 編集距離 (Levenshtein Distance) 昨日 最長共通部分列問題 (LCS) について触れました。ついでなので編集距離のアルゴリズムについても整理してみます。 編集距離 (レーベン...

http://d.hatena.ne.jp/naoya/20090329/1238307757

思い付きはまったく関係ない所から。

これを週末にやってました。naoya さんの記事にも書かれている通り、Levenshtein Distance の実装はとてつもなく簡単。

GitHub - mattn/go-lsd
https://github.com/mattn/go-lsd

ついでにこれを使った grep コマンド作ったら面白いんじゃないかと思って O(N^2) になる事覚悟の上で実装してみた。

実装にあたってまず Unicode のクラスを識別する物を作った。

GitHub - mattn/go-unicodeclass

Unicode class package

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

これは与えらえた rune が Unicode のどの部類に属するかを返すためのライブラリ。Vim のコードを一部参考にしました。もともとは bufio/scanner で与える区切り文字(デフォルトは改行)を Unicode のクラス区切りに出来ないかと思って実装しました。こんな風に使えます。

scan := bufio.NewScanner(strings.NewReader("本日は晴天なり"))
scan.Split(unicodeclass.SplitClass)
var got []string
for scan.Scan() {
    got = append(got, scan.Text())
}
// "本日", "は", "晴天", "なり"

まぁ結果的に言えば後で追加した unicodeclass.Split で十分事足りましたが。で、出来上がったのが lsdgrep。

GitHub - mattn/lsdgrep

lsdgrep Fuzzy Grep using Levenshtein String Distance

https://github.com/mattn/lsdgrep

これを使えば例えば

俺の名前は伊藤直哉だ
もしかしたら俺は伊藤直かもしれない
ちょっと待ってくれ佐藤直哉をお忘れではないか
こんばんわ佐藤B作です

こんなテキストに対して lsdgrep -d 3 伊藤直哉 を実行すると以下の様に「伊藤直哉」に近い単語がマッチする。しかも色付き!(ポイント高い)

lsdgrep

距離が3なのでマッチする単語も荒い。距離を1にするとそこそこ近い物がマッチする。

lsdgrep

いつもの事ながら研究材料なので実用的なものを作るつもりは無い。

Posted at by



2017/01/25


golang にはパッケージマネージャが無数にあります。

PackageManagementTools · golang/go Wiki · GitHub

Home Articles Blogs Books BoundingResourceUse cgo ChromeOS CodeReview CodeReviewComments CodeTools C...

https://github.com/golang/go/wiki/PackageManagementTools

僕もその一つの gom というのを開発している訳ですが、どれもこれも一長一短でなかなか全ての要望を応えられる物がないのが現状だったりします。ただし、開発者がやりたい事は

今 GOPATH にある、うまくビルドできるバージョンに依存したい

ただこれだけなのです。なんで golang はオフィシャルがこの手のツールを出してくれないんだろう、そう思っていた人も多いと思います。が、それは昨日までの話。

GitHub - golang/dep: Go dependency tool

See the help text for much more detailed usage instructions. Note that the manifest and lock file fo...

https://github.com/golang/dep

でたー!みんな待ってた。そしていつもながら他のツールと名前がバッティングしそうなこの名前の短さ!

使い方も簡単です。まずプロジェクトを作ったら

$ dep init

を実行します。すると lock.jsonmanifest.json Gopkg.tomlGopkg.lock が生成されます。この時点では中身はほぼ空っぽです。試しに以下のコードを書いてみます。

package main

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

func main() {
    println(runewidth.StringWidth("あ"))
}

そして以下を実行。

$ dep ensure

すると lock.json が更新されます。

{
    "memo": "e3dcff295c3782f2465033314eee2342535b0a792a61ebf41e0deeab5747a0e7",
    "projects": [
        {
            "name": "github.com/mattn/go-runewidth",
            "version": "v0.0.1",
            "revision": "d6bea18f789704b5f83375793155289da36a3c7f",
            "packages": [
                "."
            ]
        }
    ]
}

dep status を実行すると、go-runewidth の特定のバージョンで stick されているのが分かるかと思います。では試しに

PROJECT                        CONSTRAINT  VERSION  REVISION  LATEST  PKGS USED
github.com/mattn/go-runewidth  *           v0.0.1   d6bea18   v0.0.1  1  

試しに vendor ディレクトリを削除して、go-runewidth のリポジトリで別ブランチに切り替えておきます。そして再度 dep ensure を実行すると、ちゃんと stick したバージョンがよみがえる!

これや!ワイはこれが欲しかったんや!

今のところ、init と status と ensure と remove しかサブコマンドがありませんが、おそらく今後色々と増えてくるんじゃないかなーと思っています。チョー期待してます。

ちなみにいずれ go のサブコマンド(go dep)の様に取り込まれる可能性があるので慎重派の人はしばらくは遊ぶ程度にしておいた方が良さそう。

Posted at by