Go言語の配列操作の使い難さを改善する


2023年 06月 15日

配列操作はプログラムの基本的な機能の一つですが,Go言語は他言語と比較してこの操作が使い難いと感じています.この記事では,Go言語の配列操作の使い難さを改善した話をします.

問題

この記事では,3種類の基本的な配列操作を取り上げます.

  • 配列末尾の要素を取得する
  • 配列末尾に要素を追加する
  • 配列のi番目の要素を削除する

C++, Python, MATLABでの操作を紹介したうえで,Go言語ではどのような操作になるか解説します.
そして,Go言語の配列操作の使い難さを改善する方法を紹介します.

C++

C++の配列操作プログラムと出力は,以下の通りです.

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> nums = {0,1,2,3,4}; // 配列の宣言
    for (auto num : nums) cout << num << " ";
    cout << endl;

    auto e = nums.back(); // 配列末尾の要素を取得する
    cout << e << endl;

    nums.push_back(8); // 配列末尾に要素を追加する
    for (auto num : nums) cout << num << " ";
    cout << endl;

    nums.erase(nums.begin() + 3); // 配列のi番目の要素を削除する
    for (auto num : nums) cout << num << " ";
    cout << endl;

    return 0;
}
0 1 2 3 4
4
0 1 2 3 4 8
0 1 2 4 8

C++は,コードが23行もあり,文字数も長いです.
配列末尾の要素を取得する操作は,汎用性がありません.
配列要素の削除は,配列名を2回書く必要があります.

Python

Pythonの配列操作プログラムと出力は,以下の通りです.

nums = list(range(5)) # 配列の宣言
print(nums)

e = nums[-1] # 配列末尾の要素を取得する
print(e)

nums.append(8) # 配列末尾に要素を追加する
print(nums)

nums.pop(3) # // 配列のi番目の要素を削除する
print(nums)
[0, 1, 2, 3, 4]
4
[0, 1, 2, 3, 4, 8]
[0, 1, 2, 4, 8]

Pythonは,C++よりもプログラムが簡潔です.
行数が短くなっており,push_back(8)append(8)のように操作の名前も短くなっています.
nums[-2]で配列の末尾から2番目の要素を取得できるなど,汎用性も向上しています.
配列名を2回以上書く操作も存在しません.

MATLAB

MATLABの配列操作プログラムと出力は,以下の通りです.

nums = 0 : 4; % 配列の宣言
disp(nums);

e = nums(end); % 配列末尾の要素を取得する
disp(e);

nums = [nums 8]; % 配列末尾に要素を追加する
disp(nums);
    
nums(4) = []; % // 配列のi番目の要素を削除する
disp(nums);
0 1 2 3 4
4
0 1 2 3 4 8
0 1 2 4 8

MATLABは,Pythonと行数は同じで,文字数がより削減されています.
配列操作で覚える必要のある英単語は,配列末尾を表すendのみです.

MATLABは,1:2:9で1から9までの奇数配列を用意できるなど,配列の宣言がシンプルで強力です.
nums(end-1)で配列の末尾から2番目の要素を取得できるなど,汎用性もあります.
配列末尾に要素を追加する操作は配列名を2回書く必要がありますが,配列名が短い場合は他言語より簡潔に記述できます.また,同じ記述で配列どうしの連結ができます.

MATLABはindexが1始まりなので,左から4番目の要素を削除したい時は,素直に数字の4を使います.
プログラムを機械的に日本語に翻訳する場合,自分はPythonよりMATLABが分かり易いと感じています.

  • MATLAB nums(4) = [];の機械的翻訳:配列numsの左から4番目の要素を[](空,削除)にする.
  • Python nums.pop(3)の機械的翻訳:配列numsについて,pop(空,削除)する,左から3番目の要素を.(一番左を0番目として考える)

MATLABは,自然な日本語「配列のi番目の要素を削除する」と記述プログラムが極めて近いです.

Go言語

Go言語の配列操作プログラムと出力は,以下の通りです.

package main

import "fmt"

func main() {
	nums := []int{0, 1, 2, 3, 4} // 配列の宣言
	fmt.Println(nums)

	e := nums[len(nums)-1] // 配列末尾の要素を取得する
	fmt.Println(e)

	nums = append(nums, 8) // 配列末尾に要素を追加する
	fmt.Println(nums)

	i := 4
	nums = append(nums[:i], nums[i+1:]...) // 配列のi番目の要素を削除する
	fmt.Println(nums)

	nums = nums[:i+copy(nums[i:], nums[i+1:])] // 配列のi番目の要素を削除する
	fmt.Println(nums)
}
[0 1 2 3 4]
4
[0 1 2 3 4 8]
[0 1 2 3 8]
[0 1 2 3]

Go言語は,配列操作時に配列名を2回以上書きます.他言語より1行の記述量が多いです.
配列末尾を表すものが存在しないので,[]の中で配列末尾のindexを計算する必要があります.
len(nums)で配列の長さを計算し,それを-1してindexを求めます.(Go言語はindexが0始まりです)

Go言語で配列のi番目の要素を削除する場合,代表的な方法が2種類あります.
16行目の方法は,appendの仕様と配列のサブ集合操作がわかれば,初見でも理解できそうな方法です.
ただし,配列名を3回も書く必要があり,もう1つの方法より実行速度が遅いです.
19行目の方法は,配列名を4回も書く必要があり,操作も難解です.
配列の長さを超えた要素を暗黙の処理で切り落とすテクニックを使っています.
実行速度は,こちらが優位にあります.

他言語と比較してGo言語の基本的な配列操作が使い難いと考えているのですが,いかがでしょうか.
コードの分かり易さを重視して変数名を長くすると,ひどい目に遭います.
例)塵肺症患者データから,指定された特殊な患者のデータを削除する

pneumonoultramicroscopicsilicovolcanoconiosisPatientData = pneumonoultramicroscopicsilicovolcanoconiosisPatientData[:specifiedSpecialPatientIndex+copy(pneumonoultramicroscopicsilicovolcanoconiosisPatientData[specifiedSpecialPatientIndex:], pneumonoultramicroscopicsilicovolcanoconiosisPatientData[specifiedSpecialPatientIndex+1:])]

pneumonoultramicroscopicsilicovolcanoconiosisは,辞書に載っている最も長い英単語です.
似た名前の変数が存在したとすると,変数が混ざっていた場合でも間違いを発見するのは困難です.
配列名を4回,削除要素のindexを表す変数名を3回書くプログラムは,改善が必須と考えます.

改善したGo言語

改善したGo言語の配列操作プログラムは,以下の通りです.(出力は同じです)

package main

import "fmt"

func main() {
	nums := []int{0, 1, 2, 3, 4} // 配列の宣言
	fmt.Println(nums)

	e := Back(&nums) // 配列末尾の要素を取得する
	fmt.Println(e)

	PushBack(&nums, 8) // 配列末尾に要素を追加する
	fmt.Println(nums)

	Erase(&nums, 4) // 配列のi番目の要素を削除する
	fmt.Println(nums)
}

func Back[T any](slice *[]T) T {
	a := *slice
	return a[len(a)-1]
}

func PushBack[T any](slice *[]T, v T) {
	*slice = append(*slice, v)
}

func Erase[T any](slice *[]T, i int) {
	a := *slice
	*slice = a[:i+copy(a[i:], a[i+1:])]
}

C++を参考に,3種類の関数を新しく実装しました.そして,main関数をシンプルに書き換えました.
Go 1.18からGenericsが導入されたので,配列操作の汎用的な関数が簡単に書けるようになりました.
関数の引数をポインタにしたことで,main関数では配列名を1回書くだけで良くなっています.

感想・終わりに

Go言語はシンプルさが特徴の言語です.他言語に慣れていると使い難いと感じる部分もありますが,日々のアップデートにより,プログラムの自作で改善可能な部分も増えています.
自分は,BackPushBackEraseのような汎用的に使える関数は別packageにまとめています.
配列と乱数を組み合わせた関数,DeepCopy関数,SubSet関数などもまとめており,便利に使っています.

プログラムが使い難いと感じた時は,創意工夫で乗り切りましょう.