LoginSignup
5
3

More than 5 years have passed since last update.

GolangでGenerics的なコンテナライブラリの制作で苦労をしたが勉強になった

Last updated at Posted at 2018-12-06

ココ最近Goを勉強しはじめて
Go力(ごうりき) 低いですが ちょっとずつ勉強していきます

Genericsについて

GolangにはGenericsはありません
Genericsとは大雑把にいうと 色々な型での共通処理を1つのコードで行います

例えば、Listとかのコンテナライブラリが有名ですね
本来なら型ごとに

IntList ilist;
ilist.insert(1);

StringList slist;
slist.insert("hoge");

DoubleList dlist;
dlist.insert(1.0);



class IntList{
private:
    int *begin;
    int *end;
public:
    void insert(int);
...
;

class StringList{
private:
    string *begin;
    string *end;
public:
    void insert(string);
...
;


class DoubleList{
private:
    double *begin;
    double *end;
public:
    void insert(double);
...
};

のように 型ごとに関数を作る必要があるのが

List<int> ilist;
ilist.add(1);

List<string> slist;
slist.add("hoge");

List<double> dlist;
dlist.add(1.0);


template<class T>
class List<T>{
private:
    T *begin;
    T *end;
public:
    void insert(T);
...
};

と、複数型があっても 1つの関数書けばよくなり
開発工数もメンテナンス性もあがり とても便利

ところがGoには ジェネリクスがありません!!
Go2になればジェネリクスが導入されるという噂はありますが
現時点では存在しないので なんとかしたいところです

そういうのは interfaceを使えば出来そうなので 勉強ついでに 作ってみようと思いました

Contains

今回は Containsを作ろうと思います
与えられた配列内に特定の要素が存在すればTrue,しなければFalse を返します

IntVal作戦

パッと思いついた作戦です。
Goにはclassはありませんが interfaceと structがあります
継承はないけど ダックタイピングができます
ということで
ベースのInterfaceを作り、ダックタイピングしてみます

lib
type Traits interface {
    Val() Traits
}

type IntVal struct{val int}
func (u IntVal) Val() Traits {
    return (u)
}

type StringVal struct{val string}
func (u StringVal) Val() Traits {
    return (u);
}

func Contains(arr []Traits, val Traits) bool{
    for _, v := range arr{
        if v == val{
            return true
        }
    }
    return false
}

main
    va := []Traits  {IntVal{1},IntVal{2},IntVal{3},IntVal{4}}
    log.Printf( strconv.FormatBool( Contains( va, IntVal{1})) )
    log.Printf(strconv.FormatBool(Contains( va, IntVal{100})))

    vs := []Traits  {StringVal{"1"},StringVal{"2"},StringVal{"3"},StringVal{"4"}}
    log.Printf( strconv.FormatBool( Contains( vs, StringVal{"1"})) )


----
2018/12/02 03:10:15 true
2018/12/02 03:10:15 false
2018/12/02 03:10:15 true

IntVal、StringValという型?を作り interfaceで処理するという
最初に思いついたやりかたですが
不満ありますよね

Goには型変換コンストラクタがないので
va := []Traits {IntVal{1},IntVal{2},IntVal{3},IntVal{4}}
と いちいち 明示的に型変換が必要で面倒

interfaceで抽象化すればええやん?

更に抽象化をすすめる
実は IntValとかは作らなくてよかった(メタプロの癖)

lib
func Contains(arr []interface{}, val interface{}) bool{
    for _, v := range arr{
        if v == val{
            return true
        }
    }
    return false
}
main
    va := []int  {1, 2, 3, 4}
    log.Printf( strconv.FormatBool( Contains( va, 1)) )
    log.Printf(strconv.FormatBool(Contains( va, 100)))

interface最高! ジェネリクスなくてもなんとかなりそう

と思うだろ?
“cannot use va (type []int) as type []interface {} in argument to Contains”

ゔぁ〜〜〜!
popute.jpg

任意の型から interface{}への変換はできるが
配列から []interface{}への変換は ダメだそう

メモリマップの問題なのか いちど []interface{}配列へコピーする必要がある

main
    va := []int  {1, 2, 3, 4}

    list := make([]interface{}, 0)
    for _, v := range va {
        list = append(list, v)
    }

    log.Printf( strconv.FormatBool( Contains( list, 1)) )
    log.Printf(strconv.FormatBool(Contains( list, 100)))

    vs := []string  {"1", "2", "3", "4"}
    list = make([]interface{}, 0)
    for _, v := range vs {
        list = append(list, v)
    }

    log.Printf( strconv.FormatBool( Contains( list, "1")) )


2018/12/02 04:16:57 true
2018/12/02 04:16:57 false
2018/12/02 04:16:57 true

いちどコピーしなおせば動くけどめんどくさい・・・

配列コピーUtil

lib
func CopyInt(arr []int) []interface{} {
    list := make([]interface{}, 0)
    for _, v := range arr {
        list = append(list, v)
    }
    return list
}

func CopyString(arr []string) []interface{} {
    list := make([]interface{}, 0)
    for _, v := range arr {
        list = append(list, v)
    }
    return list
}

func Contains(arr []interface{}, val interface{}) bool{
    for _, v := range arr{
        if v == val{
            return true
        }
    }
    return false
}
main
    va := []int  {1, 2, 3, 4}
    log.Printf( strconv.FormatBool( Contains( CopyInt(va), 1)) )
    log.Printf(strconv.FormatBool(Contains( CopyInt(va), 100)))

    vs := []string  {"1", "2", "3", "4"}
    log.Printf( strconv.FormatBool( Contains( CopyString(vs), "1")) )

2018/12/02 04:16:57 true
2018/12/02 04:16:57 false
2018/12/02 04:16:57 true

Copyする関数をライブラリ化したよ。でも []interface{}が渡せないので型ごとにCopy関数作るよ

まー かなりましになったけど、色々と惜しいよ
型が増えるごとにCopy関数作らないとだめだし
Copy発生して 効率わるい
このへんが GenericsのないGoの限界なのか??

switchでCopyまとめる

配列自体を interface{}で受け取り それを型でSwitch Caseで処理分岐させる

lib

func Copy(arr interface{}) []interface{} {
    dest := make([]interface{}, 0)

    switch val := arr.(type) {
    case []interface{}:
    case []int:
        for _, v := range val {
            dest = append(dest, v)
        }
    case []string:
        for _, v := range val {
            dest = append(dest, v)
        }
    case int:
        dest = append(dest, val)
    case string:
        dest = append(dest, val)
    }

    return dest
}
main

    va := []int  {1, 2, 3, 4}
    log.Printf( strconv.FormatBool( Contains( Copy(va), 1)) )
    log.Printf(strconv.FormatBool(Contains( Copy(va), 100)))

    vs := []string  {"1", "2", "3", "4"}
    log.Printf( strconv.FormatBool( Contains( Copy(vs), "1")) )

2018/12/02 04:53:05 true
2018/12/02 04:53:05 false
2018/12/02 04:53:05 true

呼び出し側からは Copyメソッド1つで扱えるので便利だ
Switch CaseのdefaultでCopyしたいと思うが どうやらGoでは defaultでは foreachさせてもらえないので
型ごとに書くしか無い
つまり 型が増えると この関数を修正しなければならない

ContainsにCopyをまとめる

曖昧になってあまり好きではないが Containsに渡すものすら interface{}にしてしまう

lib
func Copy(arr interface{}) []interface{} {
    dest := make([]interface{}, 0)

    switch val := arr.(type) {
    case []interface{}:
    case []int:
        for _, v := range val {
            dest = append(dest, v)
        }
    case []string:
        for _, v := range val {
            dest = append(dest, v)
        }
    case int:
        dest = append(dest, val)
    case string:
        dest = append(dest, val)
    }

    return dest
}

func Contains(i interface{}, val interface{}) bool{
    arr := Copy(i)

    for _, v := range arr{
        if v == val{
            return true
        }
    }
    return false
}
main
    va := []int  {1, 2, 3, 4}
    log.Printf( strconv.FormatBool( Contains( va, 1)) )
    log.Printf(strconv.FormatBool(Contains( va, 100)))

    vs := []string  {"1", "2", "3", "4"}
    log.Printf( strconv.FormatBool( Contains( vs, "1")) )

2018/12/02 05:01:30 true
2018/12/02 05:01:30 false
2018/12/02 05:01:30 true

エラー処理(アサーションしたり) などを考えなければ かなりましになってきた
いまだにCopyにおいて 型が増えるとライブラリコード追加というダメなんだけど
これが ジェネリクスのないGolangの限界かな?

型チェック

本来ならGenericsを使っていれば厳密な型チェックができるため
Contain(int[], int) はOKであるが
Contain(int[],string) はコンパイル時にエラーさせられる

template<class T>
bool Contain(T[], T);

他の言語でも上記のように 2つの引数の型が厳密に定義できるが
Goでは 両方共interfaceでなんでも入ってしまうため

    va := []int  {1, 2, 3, 4}
    log.Printf( strconv.FormatBool( Contains( va, "1")) )

と型をミスしてしまうと、この場合 コンパイルエラーも実行時エラーもでず Falseになる
のでContainsにて型チェックを入れてみる
本来ならこういう場合には Assertを使い、デバッグビルド時には例外が発生するが本番コードには影響あたえないようにするが
GolangにはAssertがないため Panicで問答無用でおとす

このあたりGo開発者は
Assertが便利なのは認めるが お前らは正しく使わないだろ? エラー処理を正しく使わないだろ?
だからGoにはAssertは入れないぜ

とのことで、正しく使いたいのに困った。。

lib
func Contains(i interface{}, val interface{}) bool{
    arr := Copy(i)

    if(reflect.TypeOf(arr[0]) != reflect.TypeOf(val)){
        log.Printf("missmatch Type in Contains [%s] and [%s]",reflect.TypeOf(arr[0]) ,reflect.TypeOf(val) )
        panic(0)
    }

    for _, v := range arr{
        fmt.Println( reflect.TypeOf(arr), reflect.TypeOf(v), reflect.TypeOf(val))
        if v == val{
            return true
        }
    }
    return false
}

本来なら 引数の配列と 検索要素を比較して []intとint であればOK
という形にしたいが Goには配列の要素型を比較する方法がみつからず
また []interface{} になっており比較ができなかったので 1つ目の要素と比較しているが
意図通り 型間違えると panicをおこす

オチ

container/list 使いましょう

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3