ゼロから学ぶGo言語プログラミング(10) A Tour of Go 40~48

f:id:belbo:20140616112930j:plain

A Tour of Goの続き。 mapの途中から。

40.Mutating Maps

m := make(map[string]int)

という風に、mをstringがキーでintが値のmapとして初期化したとして、

m["foo"] = 100 //挿入・更新
v := m["foo"] //取得
delete(m, "foo") //削除

という操作が出来る。

キーの確認は

v, ok := m["foo"] //取得

という風に、取得時の書き方に2番めの値を付けて真偽を受け取る。

辞書的なデータ構造の扱いは、非常に多用しそうなので、自然と書けるようになっておきたいです。

41.Exercise: Maps

mapを使った課題。

ヒント通り、strings.Fieldsのリファレンスを見ると、与えられた文字列に含まれる単語を、空白文字で分割したsliceを返す関数だった。

切り出したsliceを受けて、後はforでmapに単語単位でカウントしていけば、テストは全てパスできた。

package main

import (
    "code.google.com/p/go-tour/wc"
    "strings"
)

func WordCount(s string) map[string]int {
    fs := strings.Fields(s)
    ret := make(map[string]int)
    for i := 0; i < len(fs); i++ {
        ret[fs[i]] = ret[fs[i]] + 1
    }
    return ret
}

func main() {
    wc.Test(WordCount)
}

日本語利用者としては、strings.Fields()の挙動が気になったので、日本語を与えた場合も試してみた。

package main

import (
    "code.google.com/p/go-tour/wc"
    "strings"
    "fmt"
)

func WordCount(s string) map[string]int {
    st := "私はGo言語を学習している者です。"
    fs := strings.Fields(st)
    ret := make(map[string]int)
    for i := 0; i < len(fs); i++ {
        ret[fs[i]] = ret[fs[i]] + 1
    }
    fmt.Printf("%s", ret)
    return ret
}

func main() {
    wc.Test(WordCount)
}

結果は、単語単位でsliceされず、人連なりの文字列が単独の要素になったsliceを得られただけだった。 UCAや漢字コードなどを元に、単語の自動分かちを期待したが、stringsという文字列処理の基本的パッケージらしいものがカバーすべきとは思わないので、当然かも知れない。

ちなみに、strings.Fields()は、全角の空白文字であっても分割対象にしている。 基本がUnicodeベースになっている恩恵で、楽で嬉しい。

42.Function values

関数の値渡しをサポートしている、という話。 これは少し前、関数を引数に取る画像生成の課題があったため、予測していた。

43.Function closures

クロージャは、JavasScriptでしっかり踏み込まずにやり過ごしてきた概念。 せっかくなのでGoできちんと学ぼう。

サンプルコードと、ツアーからもリンクされていたWikipediaの解説を見ながら進めます。

まずサンプルコード。

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

adder()がクロージャを実現している関数。 戻り値としてintを引数に取る関数を使うことが宣言されている。

adder()の中では変数sumが0で初期化されている。 さらにadder()は、returnで無名関数を返している。 adder()から返す無名関数は、adder()が持つ変数sumに引数xを加算し、変数sumを返すもの。

main()の中で変数posとnegは、"adder()"で初期化されている。 つまりadder()の戻り値である無名関数で初期化されていることになる。 この2つの変数それぞれをforループの中でintを渡しながら呼び出すと、次の結果が出力される。

0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

関数adderがもつsumの値は、0で初期化されるだけで、後は無名関数で加算されるだけ。 なのでposの出力は 0,1,2,3,4... となっていく筈。 しかし実際にはsumの値を保持しており、0,1,3,6,10... となっている。 また、negの値はposの場合と異なる結果を返しており、posとnegそれぞれが独立したsumを保持していることになる。

これがGoのクロージャのようである。

漠然とした印象では、オブジェクト指向におけるインスタンスの挙動のように見える。 これが関数のスコープで実現できるメリットは、まだいまいち気付けていない。 クロージャオブジェクト指向の関係を探すと、MDNの以下の記述があった。

感じた類似性そのものは誤りでないらしい。 そしてここにあるように

メソッドを 1 つだけ持つオブジェクトを使いたくなるような状況ならば、どんな時でもクロージャを使う事ができます。 という点も、returnを使ってクロージャが実装されている点から、理解できた気がする。

44.Exercise: Fibonacci closure

早速クロージャを使った課題。 フィボナッチ数をクロージャで返そう、というもの。

初期のコードはこれ。

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

fibonacci()は関数を返すようになっている。 ここで返す関数が、クロージャということだろう。 Wikipediaを見ると、こういったクロージャの外側のスコープをエンクロージャと呼ぶらしい。

エンクロージャクロージャを実現するためのスコープをまず用意し、クロージャを無名関数として実装してエンクロージャから返すことで、あたかもオブジェクト指向インスタンスのように、返された無名関数ごとに状況を引き継ぐ、独立した環境が手に入る、という理解を現時点ではしておく。

最終的に課題はこう書いた。

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
    a := 0
    b := 1
    return func() int {
        fib := a + b
        a = b
        b = fib
        return fib
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

期待通りの結果にはなるが、これがクロージャの用途として適しているのか…。 扱っている関数がクロージャかどうか、注意が必要そうだという事は理解できたし、シンプルにインスタンスっぽいものを持ちたい場合、確かに楽な状況もありそう。

クロージャが活躍する状況は大いにあるのだろうから、もっと積極的に使って慣れておきたい。

45.Switch

ifやforからしばらく経ったここにきて、制御構造switch。

pythonにはswitchが無く、欲しいなあと思っていたけど、breakを忘れてバグに繋がりやすいというのが理由らしい。 goではどうなんだろう。

switch部分だけ抜き出すと、こうなっている。

    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.", os)
    }

重要なのは以下の解説。

Goでのswitchは、 case の最後で自動的にbreakします。 もし、brakeせずに通したい場合は、 fallthrough 文をcaseの最後に記述します。

つまりバグの温床になりやすいbreakを自動化し、むしろ意図的な特殊な制御の方をfallthroughという文で実現したらしい。 非常に明確で素晴らしい仕様だと思う。

46.Switch evaluation order

caseは上から順に評価、一致で停止、全てのcaseで一致しなければdefault。 うん明確。

Goのswitchは、評価に式が書けるらしい。 あっちにfallthroughは無いけれど、FileMakerのCase()に似ている。

47.Switch with no condition

switchの条件文を省略することで、caseが比較すべき対象が無くなり、caseの式の真偽で評価される。 確かに条件の多いifを連ねるなら、こちらの方が見通しが良さそう。

48.Advanced Exercise: Complex cube roots

complexに関するAdvancedな課題。

complex型は、複素数だったらしい。 しかし複素数虚数の素養が、せいぜい概念程度の理解であり、この課題そのものを大きく誤解してしまいそう。 ということで、ひとまずここはスキップする。


次はメソッドから。