はじめに
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
組み込み関数で取得していますね。
処理フラグの変数 ok
が true
(成功)の場合は PASS と出力されます、false
(失敗)の場合は FAIL と出力されます。つまり、WordCount
関数には wc.Test
関数の実行結果が全て PASS と出力されるように実装すれば良いことがわかりますね。
では、wc.Test
関数の正体がわかったところで実装を進めます。
課題の文章内にある「strings.Fields で、何かヒントを得ることができるはずです。」という記述が重要ポイントです。
strings.Fields
関数は、1 つ以上の連続する空白文字の各インスタンスの周囲で文字列 s
を分割し、s
の部分文字列のスライス、又は s
に空白のみが含まれる場合は空のスライスを返すとあります。
func Fields(s string) []string
なるほどね。
つまり、引数 s
を strings.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 の未使用変数に対するエラーや後置インクリメントのみにしている工夫はとても素晴らしいですね。
書く人によって品質の差が出やすい仕組みは少ない方が幸せになれるような気がします。