A Tour of Go の Exercise: Maps 解答例

はじめに

Go 言語基礎文法最速マスターにて出題されている演習課題Exercise: Maps(マップ)」の答えに辿り着くまでの手順と解答例の解説を記事にしてみました。wc.Test 関数、strings.Fields 関数の振る舞いなどについても簡単に説明しています。

課題

課題の目的は WordCount 関数に string 型の引数 s にある各単語と出現回数の map を返す機能を実装して、main 関数で呼び出している wc.Test 関数の Test suite(テストスイート)を通過させることです。

package main

import (
	"golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
	return map[string]int{"x": 1}
}

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

解答例

package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	x := map[string]int{}
	for _, word := range strings.Fields(s) {
		x[word]++
	}
	return x
}

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

解説

初めに、課題のソースコードを読み解きます。

3 〜 5 行目、golang.org/x/tour/wc パッケージをインポートしています。Test 関数が実装されているパッケージのことですね。

import (
	"golang.org/x/tour/wc"
)

7 〜 9 行目、WordCount 関数が定義されています。ここに wc.Test 関数のテストスイートを通過する処理を書きます。

func WordCount(s string) map[string]int {
	return map[string]int{"x": 1}
}

11 〜 13 行目、main 関数で wc.Test 関数が呼び出されていますね。

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

実装を進める前に、まずは wc.Test 関数の振る舞いについて理解する必要があります。

wc.Test 関数の ソースコード は下記になります。

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package wc // import "golang.org/x/tour/wc"

import "fmt"

// Test runs a test suite against f.
func Test(f func(string) map[string]int) {
	ok := true
	for _, c := range testCases {
		got := f(c.in)
		if len(c.want) != len(got) {
			ok = false
		} else {
			for k := range c.want {
				if c.want[k] != got[k] {
					ok = false
				}
			}
		}
		if !ok {
			fmt.Printf("FAIL\n f(%q) =\n  %#v\n want:\n  %#v",
				c.in, got, c.want)
			break
		}
		fmt.Printf("PASS\n f(%q) = \n  %#v\n", c.in, got)
	}
}

var testCases = []struct {
	in   string
	want map[string]int
}{
	{"I am learning Go!", map[string]int{
		"I": 1, "am": 1, "learning": 1, "Go!": 1,
	}},
	{"The quick brown fox jumped over the lazy dog.", map[string]int{
		"The": 1, "quick": 1, "brown": 1, "fox": 1, "jumped": 1,
		"over": 1, "the": 1, "lazy": 1, "dog.": 1,
	}},
	{"I ate a donut. Then I ate another donut.", map[string]int{
		"I": 2, "ate": 2, "a": 1, "donut.": 2, "Then": 1, "another": 1,
	}},
	{"A man a plan a canal panama.", map[string]int{
		"A": 1, "man": 1, "a": 2, "plan": 1, "canal": 1, "panama.": 1,
	}},
}

Test 関数は、引数の関数 f に変数 testCases を渡して正しく機能しているか検証していることがわかります。

検証は、変数 testCases の各要素を for-range で取り出し、要素内の in関数 f に渡して戻り値を要素の want と比較しています。

want と比較しているのはキーの数と文字列です、キーの数は len 組み込み関数で取得していますね。

処理フラグの変数 oktrue(成功)の場合は PASS と出力されます、false(失敗)の場合は FAIL と出力されます。つまり、WordCount 関数には wc.Test 関数の実行結果が全て PASS と出力されるように実装すれば良いことがわかりますね。

では、wc.Test 関数の正体がわかったところで実装を進めます。

課題の文章内にある「strings.Fields で、何かヒントを得ることができるはずです。」という記述が重要ポイントです。

strings.Fields 関数は、1 つ以上の連続する空白文字の各インスタンスの周囲で文字列 s を分割し、s の部分文字列のスライス、又は s に空白のみが含まれる場合は空のスライスを返すとあります。

func Fields(s string) []string

なるほどね。

つまり、引数 sstrings.Fields 関数で分割後、for-rangeで各要素を map[string]int 型の変数 x に格納して return するように実装すれば良いってことですね。

やることがわかったので、サクッとやってみます。

package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	x := map[string]int{}
	for _, word := range strings.Fields(s) {
		x[word]++
	}
	return x
}

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

for _, word_(アンダースコア)は、配列のインデックスが必要ない場合などにこのような書き方をします。for word のように書くと、word にはインデックスが格納されます。

for i, word のように書くと declared but not used エラーが発生します、未使用変数はアンダースコアで書くように注意しましょう。

また、x[word]++word(単語)の出現回数を++(インクリメント演算子)してカウントしています。Go に前置インクリメントはありません、書くと syntax error: unexpected ++, expecting エラーが発生するので注意しましょう。

実行結果は下記のようになります。

PASS
 f("I am learning Go!") = 
  map[string]int{"Go!":1, "I":1, "am":1, "learning":1}
PASS
 f("The quick brown fox jumped over the lazy dog.") = 
  map[string]int{"The":1, "brown":1, "dog.":1, "fox":1, "jumped":1, "lazy":1, "over":1, "quick":1, "the":1}
PASS
 f("I ate a donut. Then I ate another donut.") = 
  map[string]int{"I":2, "Then":1, "a":1, "another":1, "ate":2, "donut.":2}
PASS
 f("A man a plan a canal panama.") = 
  map[string]int{"A":1, "a":2, "canal":1, "man":1, "panama.":1, "plan":1}

大丈夫だ、問題ない。

以上です。

おわりに

Go の未使用変数に対するエラーや後置インクリメントのみにしている工夫はとても素晴らしいですね。

書く人によって品質の差が出やすい仕組みは少ない方が幸せになれるような気がします。