ようへいの日々精進XP

よかろうもん

2018 年 11 月 08 日 (木)

ジョギング

  • 山王公園往復
  • 引き続き, 胸の痛みが治まってきた感じ...
  • やっと 11 日のスタートラインに立てる気がしてきた

日課

  • お休み
  • 完全に痛みが引いたら再開する

43 回目の誕生日

  • まずは元気にこの世に命を授けてくれた両親に感謝
  • 自分勝手な自分をいつも支えてくれる奥さんに感謝
  • リモートで支えてくれるチームの皆さんに感謝

Facebook でもたくさんのおめでとうごコメントを頂いて, 本当に有難うございました. コメントを返しながら, 誕生日で懐かしい人達との再開を与えてくれたり, それらの人たちに対して感謝をする日なんだろうなあと.

夕飯は奥さんが気を利かせて新型 MacBook Air をサプライズで買ってきてくれると思っていたが, それは流石に無かったけど, 尾ノ上の特上寿司をテイクアウトしてくれて細やかな誕生祭を催してくれた. ありがとう . 尾ノ上のお寿司, 本当に美味しかった.

www.apple.com

ということで, 今年一年, 家族のみんなが元気に過ごせますように.

アルゴリズムとプログラミング 「第 6 章 配列の仕組み」の予習とまとめ

これは

放送大学教養学部の「アルゴリズムとプログラミング」という授業で使われる「アルゴリズムとプログラミング」という教材書籍を自分なりにまとめたものです.

1. 配列の仕組み

  • 同じ型の変数データを多数利用する場合, 変数の宣言や取扱が煩雑になる
  • このような問題を解決するする方法の一つとして配列 (array) がある
  • 配列の要素を指定する為の通し番号は添字 (index) と呼ばれる

以下は配列と添字の関係を示したもの.

添字 0 1 2 3 4 5 6 7 8 9
30 20 10 25 15
  • 添字には整数値が使われ, その値は 0 や 1 からスタートするが, -1 や -2 のような負の値の添字を使うことが出来るプログラミング言語もある
  • 連想配列と呼ばれる, 整数以外の文字列等の添字を使うことが出来るプログラミング言語もある
  • 配列の要素には同じデータ型しか使えないが, 異なるデータ型を混ぜて使える配列も存在する

以下は C 言語での配列の宣言例.

int a[10];

この例では 10 個の整数型の様子を持つ a という名前の配列を宣言している.

以下, 配列を使わずに 5 つの整数の平均値を求めるコード.

/* code: ex6-1.c */
#include <stdio.h>

int main ()
{
  int a, b, c, d, e;
  int sum, avg;
  
  a = 30;
  b = 20;
  c = 10;
  d = 25;
  e = 15;
  sum = a + b + c + d + e;
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

上記のコードについて, 配列を使うと以下のようになる.

/* code: ex6-2.c */
#include <stdio.h>

int main ()
{
  int a[10];
  int i, sum, avg;
  
  a[0] = 30;
  a[1] = 20;
  a[2] = 10;
  a[3] = 25;
  a[4] = 15;
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

このコードでは, for ループにより変数 i が増加する. この変数は配列の添字となっており, 配列の各要素が加算されて 5 つの数の合計が求められる.

root@0be431eebb77:/work# gcc ex6-2.c -o ex6-2 -g3
root@0be431eebb77:/work# ./ex6-2
20

以下は ex6-2.c と同じ動作をするコードである.

/* code: ex6-3.c */
#include <stdio.h>

int main ()
{
  int a[10] = { 30, 20, 10, 25, 15 };
  int i, sum, avg;
  
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc ex6-3.c -o ex6-3 -g3
root@0be431eebb77:/work# ./ex6-3
20

このコードでは配列の初期化を配列の宣言文内で行っている. 配列の要素は波括弧 {} 内に列挙する. 要素数が宣言した配列の数より少ない場合は, 列挙した要素以外は初期化されない. 列挙した要素が多い場合は, 多くのコンパイラでは警告が出る. 例えば, 以下のように宣言した数よりも要素の数が多い場合.

/* code: ex6-3a.c */
#include <stdio.h>

int main ()
{
  int a[3] = { 30, 20, 10, 25, 15 };
  int i, sum, avg;
  
  sum = 0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5;
  printf ("%d\n", avg);
  
  return 0;
}

以下のようにコンパイラ時エラーが発生することを確認した.

# gcc ex6-3a.c -o ex6-3a -g3
ex6-3a.c: In function 'main':
ex6-3a.c:6:28: warning: excess elements in array initializer
   int a[3] = { 30, 20, 10, 25, 15 };
                            ^~
ex6-3a.c:6:28: note: (near initialization for 'a')
ex6-3a.c:6:32: warning: excess elements in array initializer
   int a[3] = { 30, 20, 10, 25, 15 };
                                ^~
ex6-3a.c:6:32: note: (near initialization for 'a')

以下のコードは, 要素数 10 の配列の中に 0 〜 99 まで範囲の乱数を入れていくコードである.

/* code: ex6-4.c */
#include <stdio.h>
#include <stdlib.h>

#define ARRAY_SIZE 10

int main ()
{
  int a[ARRAY_SIZE];
  int i, sum, avg;

  for (i = 0; i < ARRAY_SIZE; i++) {
    a[i] = rand () % 100;
  }

  for (i = 0; i < ARRAY_SIZE; i++) {
    printf ("%03d ", a[i]);
  }
  printf ("\n");

  return 0;
}

これをコンパイルして実行する. 以下のように 0 パディングされた 100 以下のランダムな整数が 10 個出力される.

root@0be431eebb77:/work# gcc ex6-4.c -o ex6-4 -g3
root@0be431eebb77:/work# ./ex6-4
083 086 077 015 093 035 086 092 049 021

上記の例では #define を利用して, 配列の要素数 ARRAY_SIZE を定義している. 配列の要素数を変更したい場合には, ARRAY_SIZE を変更するだけで良い. rand 関数は, 0 以上, RAND_MAX で定義された値以下の疑似乱数整数を返す. % 記号はモジュロ演算で, 剰余の計算をする.

2. 多次元配列

多くのプログラミング言語では, 多次元配列を利用することが出来る. 以下は 2 次元配列を図示したものである.

C0 C1 C2 C3
R0 a[0][0] a[0][1] a[0][2] a[0][3]
R1 a[1][0] a[1][1] a[1][2] a[1][3]
R2 a[2][0] a[2][1] a[2][2] a[2][3]

以下のコードは, 上記の 2 次元配列表が示すような整数型の 2 次元の配列を作成し, 値を代入した後に二重の for ループによって配列内の値を出力する.

/* code: ex6-5.c */
#include <stdio.h>

int main ()
{
  int i, j;
  int a[3][4] = {
    {0, 10, 20, 30},
    {40, 50, 60, 70},
    {80, 90, 100, 110}
  };

  for (i = 0; i < 3; i++) {
    for (j = 0; j < 4; j++) {
      printf ("array[%d][%d] = %3d\n", i, j, a[i][j]);
    }
  }

  return 0;
}

これをコンパイラして実行してみる.

root@0be431eebb77:/work# gcc ex6-5.c -o ex6-5 -g3
root@0be431eebb77:/work# ./ex6-5
array[0][0] =   0
array[0][1] =  10
array[0][2] =  20
array[0][3] =  30
array[1][0] =  40
array[1][1] =  50
array[1][2] =  60
array[1][3] =  70
array[2][0] =  80
array[2][1] =  90
array[2][2] = 100
array[2][3] = 110

以下は, 3 次元配列のコード例である.

/* code: ex6-6.c */
#include <stdio.h>

int main ()
{
  int i, j, k;
  int a[2][3][4] = {
    {{0, 1, 2, 3},
     {4, 5, 6, 7},
     {8, 9, 10, 11}},
    {{0, 10, 20, 30},
     {40, 50, 60, 70},
     {80, 90, 100, 110}}
  };

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        printf ("array[%d][%d][%d] = %3d\n", i, j, k, a[i][j][k]);
      }
    }
  }

  return 0;
}

これをコンパイラして実行してみる.

root@0be431eebb77:/work# gcc ex6-6.c -o ex6-6 -g3
root@0be431eebb77:/work# ./ex6-6
array[0][0][0] =   0
array[0][0][1] =   1
array[0][0][2] =   2
array[0][0][3] =   3
array[0][1][0] =   4
array[0][1][1] =   5
array[0][1][2] =   6
array[0][1][3] =   7
array[0][2][0] =   8
array[0][2][1] =   9
array[0][2][2] =  10
array[0][2][3] =  11
array[1][0][0] =   0
array[1][0][1] =  10
array[1][0][2] =  20
array[1][0][3] =  30
array[1][1][0] =  40
array[1][1][1] =  50
array[1][1][2] =  60
array[1][1][3] =  70
array[1][2][0] =  80
array[1][2][1] =  90
array[1][2][2] = 100
array[1][2][3] = 110

C 言語の場合, 要素数を宣言する [] を増やすことによって, 配列の次元を増やすことが出来る. 多次元配列は 1 次元配列に変換することが可能である. 例えば, 3 次元配列 a の各次元が X, Y, Z であり, 0 以上の値となる添字を i, j, k とすると, 1 次元配列 b は以下の式で変換することが出来る.

b[X × Y × i + Y × j + k] = a[i][j][k]

一般的に多次元の配列は要素数が大きくなりやすい為, 配列に使用出来るメモリの上限に注意する必要がある. データを格納する為の配列のメモリは, スタック領域と呼ばれる場所に確保される. スタック領域で利用出来るメモリサイズは OS やコンパイラの制限を受ける.

3. 文字列

  • 文字列 (string) は一連の文字 (character) から出来ている
  • C 言語では標準で文字列型を持っておらず, 文字列は配列を利用して作ることが出来る

以下のコードは OUJ という文字列を出力するコードである.

/* code: ex6-7.c */
#include <stdio.h>

int main ()
{
  char s[4];
  s[0] = 'O';
  s[1] = 'U';
  s[2] = 'J';
  s[3] = '\0';
  printf ("%s\n", s);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-7.c -o ex6-7 -g3
root@0be431eebb77:/work# ./ex6-7
OUJ

このコードは文字型の要素数が 4 の配列 s を宣言し, その配列の先頭から順番に O, U, J の文字を配列に代入している. 最後には \0 という文字列の終端を表す為の特別な文字 (ヌル文字) を配列に代入している. 長さ n の文字列を格納する為には, ヌル文字も必要になる為, n + 1 個の要素数を持つ配列を用意する必要がある.

C 言語では, 文字列のような配列に対しては代入演算子を利用することが出来ない為, ある配列から別の配列への要素を代入する場合, 以下のコードのように, ループ文等を用いて配列の要素を 1 つずつコピーしていく必要がある.

/* code: ex6-8.c */
#include <stdio.h>

void string_copy (char *target, char *source) // void string_copy (char target[], char source[]) と記述出来る
{
  int i;
  i = 0;
  while (source[i] != '\0') {
    target[i] = source[i];
    i++;
  }
  target[i] = '\0';
}

int main ()
{
  char s[20] = "University";
  char t[20];
  
  string_copy (t, s);
  printf ("%s\n", t);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-8.c -o ex6-8 -g3
root@0be431eebb77:/work# ./ex6-8
University

このコードの string_copy 関数は, for ループを用いて, 配列 source の各要素からヌル文字以外を配列 target の各要素にコピーしていく. 最後にヌル文字を追加して文字列をコピーする. この関数を呼び出す時には, コピー先に十分なメモリが確保されていなければならない.

以下の表のように, C 言語のライブラリには文字列を扱う関数が多数含まれている. これらの関数は string.h に含まれる為, 利用する場合には #include <string.h> を宣言する必要がある.

関数 英語での意味 関数の説明
strcpy copy a string 文字列をコピー
strcat concatenate two strings 2 つの文字列を連結
strcmp compare two strings 2 つの文字列を比較
strncmp compare part of two strings 2 つの文字列を文字数を指定して比較
strchr locate character in strings 文字列の先頭から文字を探索
strstr locate a substrings 文字列から文字列を探索
strlen calculate the length of string 文字列の長さを求める

以下のコードは, 文字列をコピーする関数 strcpy 関数を利用した例で ex6-8.c と同様の結果となる.

/* code: ex6-9.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[20] = "University";
  char t[20];
  
  strcpy (t, s);
  printf ("%s\n", t);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-9.c -o ex6-9 -g3
root@0be431eebb77:/work# ./ex6-9
University

関数を使うことで, ex6-8.c よりもだいぶんコードの記述量が減っている. 特に理由が無い限りは関数を使うべきなのかな.

以下のコードは strcmp 関数を利用したコードである.

/* code: ex6-10.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s0[] = "aaaaa";
  char s1[] = "bbbbb";
  char s2[] = "aaaaaaa";
  
  int i;
  printf ("strcmp(str1, str2)\n");
  i = strcmp (s0, s0);
  printf ("[%s] [%s] (%d)\n", s0, s0, i);

  i = strcmp (s0, s1);
  printf ("[%s] [%s] (%d)\n", s0, s1, i);

  i = strcmp (s1, s0);
  printf ("[%s] [%s] (%d)\n", s1, s0, i);

  i = strcmp (s0, s2);
  printf ("[%s] [%s] (%d)\n", s0, s2, i);
  
  return 0;
}

これをコンパイルして実行する.

root@0be431eebb77:/work# gcc ex6-10.c -o ex6-10 -g3
root@0be431eebb77:/work# ./ex6-10
strcmp(str1, str2)
[aaaaa] [aaaaa] (0)
[aaaaa] [bbbbb] (-1)
[bbbbb] [aaaaa] (1)
[aaaaa] [aaaaaaa] (-97)

strcmp 関数は 2 つの文字列 s1 と s2 を比較する. strcmp 関数は以下の書式で定義されている. const は定数である為, 書き換え不可能な文字列であり, 関数は文字列を関数内では変更しないことを意味している.

int strcmp(const char *s1, const char *s2)

この関数は s1 が s2 に比べて小さい場合, 等しい場合, 大きい場合にそれぞれ, 以下のような整数値を返す.

  • 小さい場合... ゼロよりも小さい整数
  • 等しい場合... ゼロと等しい整数
  • 大きい場合... ゼロより大きい整数

演習問題

問 6.1

コード 6.2 では, 整数型の配列が宣言されている. これを浮動小数点型の配列に変更したプログラムを作成しなさい.

/* code: q6-1.c */
#include <stdio.h>

int main ()
{
  float a[10];
  int i;
  float sum, avg;
  
  a[0] = 30;
  a[1] = 20;
  a[2] = 10;
  a[3] = 25;
  a[4] = 15;
  sum = 0.0;
  for (i = 0; i < 5; i++) {
    sum += a[i];
  }
  avg = sum / 5.00;
  printf ("%f\n", avg);
  
  return 0;
}

このコードをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-1.c -o q6-1 -g3
root@0be431eebb77:/work# ./q6-1
20.000000
root@0be431eebb77:/work#

ポイントは変数の宣言に float を利用している点. また, 平均値を出力する printf 関数において, %d\n ではなく %f\n としている点.

問 6.2

コード 6.5 を参考にして, 九九表 (掛け算表) を表示するプログラムを作成しなさい. コードでは, 九九表の値を一度, 2 次元配列に代入してから, 配列内の値を出力しなさい.

/* code: q6-2.c */
#include <stdio.h>
#define TABLE 9

int main ()
{
  int i, j;
  int a[TABLE][TABLE]; 

  for (i = 0; i < TABLE; i++) {
    for (j = 0; j < TABLE; j++) {
      a[i][j] = (i + 1) * (j + 1);
    }
  }

  for (i = 0; i < TABLE; i++) {
    for (j = 0; j < TABLE; j++) {
      printf ("%02d ", a[i][j]);
    }
    printf ("\n");
  }

  return 0;
}

このコードをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-2.c -o q6-2 -g3
root@0be431eebb77:/work# ./q6-2
01 02 03 04 05 06 07 08 09
02 04 06 08 10 12 14 16 18
03 06 09 12 15 18 21 24 27
04 08 12 16 20 24 28 32 36
05 10 15 20 25 30 35 40 45
06 12 18 24 30 36 42 48 54
07 14 21 28 35 42 49 56 63
08 16 24 32 40 48 56 64 72
09 18 27 36 45 54 63 72 81

問 6.3

コード 6.6 を変更にして, 3 次元配列に 1 以上 100 以下の乱数を代入するプログラムを作成しなさい. コードでは, 乱数の値を一度, 3 次元配列に代入してから, 配列内の値を出力しなさい.

/* code: q6-3.c */
#include <stdio.h>
#include <stdlib.h>

int main ()
{
  int i, j, k;
  int a[2][3][4]; 

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        a[i][j][k] = (rand () % 100) + 1;
      }
    }
  }

  for (i = 0; i < 2; i++) {
    for (j = 0; j < 3; j++) {
      for (k = 0; k < 4; k++) {
        printf ("%03d ", a[i][j][k]);
      }
      printf ("\n");
    }
    printf ("\n");
  }
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-3.c -o q6-3 -g3
root@0be431eebb77:/work# ./q6-3
084 087 078 016
094 036 087 093
050 022 063 028

091 060 064 027
041 027 073 037
012 069 068 030

問 6.4

コード 6.10 を変更にして, 文字列の最初の 3 文字のみを比較するようにしなさい. 文字列の比較には, srtncmp 関数を用いること.

/* code: q6-4.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s0[] = "aaaaa";
  char s1[] = "bbbbb";
  char s2[] = "aaaaaaa";
  
  int i;
  printf ("strncmp(str1, str2)\n");
  i = strncmp (s0, s0, 3);
  printf ("[%s] [%s] (%d)\n", s0, s0, i);

  i = strncmp (s0, s1, 3);
  printf ("[%s] [%s] (%d)\n", s0, s1, i);

  i = strncmp (s1, s0, 3);
  printf ("[%s] [%s] (%d)\n", s1, s0, i);

  i = strncmp (s0, s2, 3);
  printf ("[%s] [%s] (%d)\n", s0, s2, i);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-4.c -o q6-4 -g3
root@0be431eebb77:/work# ./q6-4
strncmp(str1, str2)
[aaaaa] [aaaaa] (0)
[aaaaa] [bbbbb] (-1)
[bbbbb] [aaaaa] (1)
[aaaaa] [aaaaaaa] (0)

strncmp を man してみる.

$ man strncmp
...
     #include <string.h>

     int
     strcmp(const char *s1, const char *s2);

     int
     strncmp(const char *s1, const char *s2, size_t n);
...

strncmp は第三引数に比較する文字数を指定する.

問 6.5

strlen 関数を用いて文字列 "abcdefg" の長さを表示するプログラムを作成しなさい.

/* code: q6-5.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[] = "abcdefg";
  
  int i;
  i = strlen (s);
  printf ("[%s] (%d)\n", s, i);
  
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-5.c -o q6-5 -g3
root@0be431eebb77:/work# ./q6-5
[abcdefg] (7)

入力した文字列の長さを返すように改変してみる.

/* code: q6-5a.c */
#include <stdio.h>
#include <string.h>

int main ()
{
  char s[10];

  printf ("Enter Words: ");
  scanf ("%s", &s);

  int i;
  i = strlen (s);
  printf ("[%s] (%d)\n", s, i);

  return 0;
}

実行してみる.

root@0be431eebb77:/work# ./q6-5a
Enter Words: foobar
[foobar] (6)

問 6.6

配列はデータの集まりを処理する為に重要である. 配列と一緒に使うと便利なものとして構造体 (structure) がある. 構造体を用いると複数のデータ型を 1 つにまとめて扱うことが出来る. (1) 構造体の配列, (2) 構造体のポインタ配列を使ったプログラムを作成しなさい.

chapter 06 (Algorithms and Programming 2016) より引用.

/* code: q6-6.c   (v1.16.00) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define MAX 10
 
struct student
{
  int id;
  char grade;
  char name[128];
};
typedef struct student STUDENT_TYPE;
 
/* ------------------------------------------- */
int main ()
{
  STUDENT_TYPE db1[MAX];
  STUDENT_TYPE *db2[MAX];
  int i;
 
  printf ("database1\n");
  for (i = 0; i < MAX; i++) {
    db1[i].id = 100 + i;
    db1[i].grade = 'a' + rand () % 5;
    strcpy (db1[i].name, "John Doe");
    printf ("%d %c %s\n", db1[i].id, db1[i].grade, db1[i].name);
  }
 
  printf ("\n");
  printf ("database2\n");
  for (i = 0; i < MAX; i++) {
    db2[i] = malloc (sizeof (STUDENT_TYPE));
    db2[i]->id = 200 + i;
    db2[i]->grade = 'a' + rand () % 5;
    strcpy (db2[i]->name, "John Doe");
    printf ("%d %c %s\t\t", db2[i]->id, db2[i]->grade, db2[i]->name);
    printf ("%d %c %s\n", (*db2[i]).id, (*db2[i]).grade, (*db2[i]).name);
  }
  for (i = 0; i < MAX; i++) {
    free (db2[i]);
  }
 
 
  return 0;
}

これをコンパイルして実行してみる.

root@0be431eebb77:/work# gcc q6-6.c -o q6-6 -g3
root@0be431eebb77:/work# ./q6-6
database1
100 d John Doe
101 b John Doe
102 c John Doe
103 a John Doe
104 d John Doe
105 a John Doe
106 b John Doe
107 c John Doe
108 e John Doe
109 b John Doe

database2
200 c John Doe          200 c John Doe
201 c John Doe          201 c John Doe
202 a John Doe          202 a John Doe
203 e John Doe          203 e John Doe
204 d John Doe          204 d John Doe
205 b John Doe          205 b John Doe
206 a John Doe          206 a John Doe
207 b John Doe          207 b John Doe
208 c John Doe          208 c John Doe
209 b John Doe          209 b John Doe

ふむ. 現時点で, このコードを理解するのは難しい. ごめんなさい. 以下, 印刷教材より引用.

  • 構造体のメンバ参照に使われているドット演算子とアロー演算子に注意すること
  • db2[i]->id(*db2[i]).id は同義である
  • 構造体のポインタ配列では, malloc と free が利用されている

以上

  • 配列の理解が深まった (これまではふわっとしか理解していなかった)
  • 文字列の生成に配列を利用する必要があるというのは意外だった
  • 関数のドキュメントは man 関数名 でいける
  • ポインタとか構造体とかがサラッと出てくるので焦る...

2018 年 11 月 07 日 (水)

ジョギング

  • 山王公園往復
  • 少しだけ胸の痛みが治まってきた感じ

日課

  • お休み

引き続き, AppSync

  • API Gateway + Lambda (Golang) で実装した HTTP エンドポイントをデータソースにして AppSync を試してみた
  • 本当に試しただけなので, リゾルマッピングの実装については試行錯誤が続く

2018 年 11 月 06 日 (火)

ジョギング

  • 山王公園往復
  • いつもよりもペースは抑え気味
  • やっぱり右胸は痛い...福岡マラソン走れっかな...

日課

  • お休み

GraphQL と AppSync

ギョームにて GraphQL について, Hello World レベルから調査する. AWS だと AppSync を利用すると, サクッと GraphQL の環境を用意出来ることが解った. AppSync はデータソースとして DynamoDB や Amazon Elasticsearch Service, Lambda を利用出来る. スキーマを定義して, スキーマとデータソースはリゾルマッピングを書くことで関連付けることが出来ることがだいたい理解出来た.

www.slideshare.net

しかし, この仕組を導入するメリットがあるのかどうかについては, GraphQL 自体がどのようなものなのかの理解をより深める必要があるなと思った.

2018 年 11 月 05 日 (月)

ジョギング

  • 右胸の痛みが治まらず...おやすみ

日課

  • お休み

右胸の痛みに耐えかねて...

  • 改めて病院に (別の病院に行った)
  • レントゲンは異常無し, 触診等を行った上で肋軟骨損傷の可能性があるとのこと
  • どうしてもマラソンを走りたければ, テーピングで肋骨を固定して走れば少しは楽になるのではとのこと

夕飯

  • 近所の鳥鶏研究所にて
  • 焼き鳥も然ることながら, 〆で食べた塩ラーメンが最&高だった

AWS SDK for Ruby を利用した CLI ツールのサンプル的なものを実装検討した

tl;dr

AWS SDK for Ruby を使ってコマンドラインツールを作る時にテストまで含めた雛形みたいなのがあったら楽だよなーと思いつつ, ソフトバンク日本シリーズを制したり, 楽しそうな JAWS FESTA の様子が SNS のタイムラインに流れてくるのを羨ましく横目に見つつ, もくもくサンプル的な何かを作ってみました.

作ったもの

github.com

これ.

EC2 インスタンス ID 一覧, S3 バケット名一覧を返すだけのあくまでもサンプル的なものとなります.

$ bundle exec sample-cli --help
Commands:
  sample-cli buckets         # list up bucket name.
  sample-cli help [COMMAND]  # Describe available commands or one specific command
  sample-cli input WORD      # input words print.
  sample-cli instances       # list up instance ids.
  sample-cli version         # version print.

環境変数AWS_PROFILE と AWS_REGION を定義して利用することを想定しています.

$ export AWS_PROFILE=xxxxxxxxxxxxxxxxxxx
$ export AWS_REGION=ap-northeast-1
$ bundle exec sample-cli instances
i-xxxxxxxxxxxxxxxx1
i-xxxxxxxxxxxxxxxx2
i-xxxxxxxxxxxxxxxx3
i-xxxxxxxxxxxxxxxx4
i-xxxxxxxxxxxxxxxx5
$ bundle exec sample-cli buckets
bucket-a
bucket-b
bucket-c
bucket-d
bucket-e
...

で, コマンドラインツールを作る上でいろいろと検討した内容を以下の通り書いていきたいと思います. 誤りや認識不足がありおかしなことが書かれているかと思いますので, コメント等で指摘いただければ幸いでございます.

やっぱり Thor

そーなんです

コマンドラインツールを作る際に引数の処理等を良しなに面倒を見てくれる erikhuda/thor がとても便利だと思います. 例えば, 上述の例で実行されている instancesbuckets のサブコマンドをメソッドとして記述することで, 簡単にサブコマンドを追加することが出来ます.

以下, 実装例です.

module SampleCli
  class CLI < Thor
    desc 'version', 'version print.'
    def version
      puts SampleCli::VERSION
    end

    desc 'input WORD', 'input words print.'
    def input(word = nil)
      puts 'Please input `word`' if word.nil?
      puts word
    end

    desc 'instances', 'list up instance ids.'
    def instances
      ec2 = SampleCli::Ec2.new
      puts ec2.instances
    end

    desc 'buckets', 'list up bucket name.'
    def buckets
      s3 = SampleCli::S3.new
      puts s3.buckets
    end
  end
end

Thor クラスを継承して, 上記のように実装しておくと, 以下のような感じでサブコマンド化してくれます.

$ bundle exec sample-cli --help
Commands:
  sample-cli buckets         # list up bucket name.
  sample-cli help [COMMAND]  # Describe available commands or one specific command
  sample-cli input WORD      # input words print.
  sample-cli instances       # list up instance ids.
  sample-cli version         # version print.

これだけでも十分にテンションが上がります.

あとは...

各サブコマンドで呼ばれる処理を実装していくことで, それなりにコマンドラインツールが数分で出来上がってしまいます.

コードをどーやって分割するか

あくまでも

好みの問題になってしまうかもしれませんが, 今回は以下のようにファイルを分割してみました. よく利用するツールのフォルダ構成を程よく参考させて頂きました.

$ tree -L 3 .
.
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── exe
│   └── sample-cli
├── lib
│   ├── sample_cli
│   │   ├── cli.rb
│   │   ├── client.rb
│   │   ├── ec2.rb
│   │   ├── s3.rb
│   │   ├── stub
│   │   ├── stub.rb
│   │   └── version.rb
│   └── sample_cli.rb
├── sample-cli.gemspec
├── spec
│   ├── default_spec.rb
│   ├── ec2_spec.rb
│   ├── s3_spec.rb
│   └── spec_helper.rb
└── vendor
    └── bundle
        └── ruby
  • lib/sample_cli/cli.rb には個々のサブコマンドを実装 (ここを見れば, このコマンドにはどんなサブコマンドがあるか解る)
  • lib/sample_cli/client.rb には個々のサブコマンドで利用する処理の初期化処理 (主に AWS SDK for Ruby の認証処理とか) を実装
  • lib/sample_cli/ec2.rb にはサブコマンド instances で利用する処理を実装
  • lib/sample_cli/s3.rb にはサブコマンド buckets で利用する処理を実装
  • lib/sample_cli/stub.rb には stub ディレクトリ以下のスタブを読み込む処理を実装 (テストについては後述)
  • lib/sample_cli/stub/*.rb には instancesbuckets の処理内容をテストする際に利用するスタブを定義 (テストについは後述)
  • lib/sample_cli/sample_cli.rb には, 上記の各ファイルを require する処理を実装

という感じです.

ですので...

今後, S3 オブジェクトの一覧を取得するサブコマンドを追加する場合には, lib/sample_cli/cli.rb に objects という名前のメソッドを追加して, lib/sample_cli/s3.rb に実際の処理を追加していく感じを想定しています.

# lib/sample_cli/cli.rb に以下を追加
...
desc 'objects', 'list up S3 objects'
option :bucket, type: :string, aliases: '-b', desc: 'S3 バケットを指定する.'
def objects
  s3 = SampleCli::S3.new
  puts s3.objects(option[:bucket])
end
...
  
# lib/sample_cli/s3.rb に以下を追加
def objects(bucket)
  puts list_objects(bucket)
end

private

def list_objects(bucket)
  objects = []
  options = { bucket: bucket }
  loop do
    res = s3.list_objects_v2(options)
    objects << res.contents.map(&:key)
    options[:continuation_token] = res.next_continuation_token
    break unless options[:continuation_token]
  end
  objects
end

尚, stub まわりの処理については, awspec を参考にさせて頂きました. 有難うございました.

テスト

コマンドラインの実行をどのようにテストするか (1)

Thor のテストが参考になることを最近知りました ありがとうございます. コマンド出力を以下の capture というメソッドで取得して, その出力をパースして評価しているようです.

# https://github.com/erikhuda/thor/blob/master/spec/helper.rb#L51-L62
...
  def capture(stream)
    begin
      stream = stream.to_s
      eval "$#{stream} = StringIO.new"
      yield
      result = eval("$#{stream}").string
    ensure
      eval("$#{stream} = #{stream.upcase}")
    end

    result
  end
...

このメソッドは, eval で文字列をコマンドとして実行されるようになっていて, 一見, 何をやっているか良く解りませんでしたが, よく見ると以下のような処理になっています.

$stdout = StringIO.new  # 標準出力の出力先を StringIO クラスに変更
yield                    # ブロックで渡された処理を実行する
output = $stdout.string # 標準入力を変数 output に代入
$stdout = STDOUT        # 標準出力の出力先を STDOUT に戻す (デフォルトが STDOUT なので)

これをそのまま spec_helper.rb に押し込んでおいて, テストには以下のように実装しました.

describe 'sample_cli check subcommand objects' do
  it 'have object keys by cli' do
    output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) }
    expect(output).to match('foo\nbar\nbaz/key\n')
  end
end

このテストの場合, 先述のオブジェクト一覧を取得する objects サブコマンドの正常系テストを想定しています.

sample-cli objects --bucket=foo

これを実行すると以下のようにテストがパスします.

$ bundle exec rake spec:s3
...

sample_cli check subcommand buckets
  have bucket names
  have bucket names by cli

sample_cli check subcommand objects
  have object keys
  have object keys by cli

Finished in 0.11143 seconds (files took 3.24 seconds to load)
4 examples, 0 failures

コマンドラインの実行をどのようにテストするか (2)

これは, Docker を利用して仮想的な AWS 環境, Ruby の実行環境を用意し, 実際にコマンドを AWS 環境に対して実行する方法を検討しました. これについては, 別の記事でまとめたいと思います.

スタブ

2 つのアプローチ

AWS 環境に対するテストを行う場合, 実際の AWS リソースを叩くというのは出来るだけ避けたいところです. もちろん, テスト用の環境を AWS 上に用意して実際にリソースを叩くことでより精度の高いテスト結果を得られる可能性があると思いますが, うっかり変更してはいけない環境を操作してしまう事故の可能性も 0 ではありませんし, 利用料が発生することも有り得ます. これらを解決する方法として, 以下の 2 点のアプローチが取れると考えています.

  1. Docker で仮想的な AWS 環境を用意する (いつか別の記事で書く予定です)
  2. スタブを利用する

1 については, 前節の「コマンドラインの実行をどのようにテストするか (2)」でも触れていますが, localstack や moto 等のローカル環境に AWS 環境を実行するツールを Docker 上で起動して, その環境に対して実際のコマンドを発行して結果を解析してテストを行います.

AWS SDK for Ruby におけるスタブ

本節 (今回) は 2 のスタブを利用したテストについて検討しています. スタブを利用する場合, AWS SDK for Ruby ではクライアントを初期化する際に以下のように指定することで, スタブ化されたデータを返すようになります.

require 'aws-sdk'

s3 = Aws::S3::Client.new(stub_responses: true)
...

詳細は以下のドキュメントに記載されています.

docs.aws.amazon.com

このドキュメントによると, スタブ化した場合, 特にデータを用意しない場合には, 以下のようなデータを返却するとのことです.

  • リストは空の配列
  • マップは空のハッシュ
  • 数値は 0
  • 日付は now

例えば, S3 バケットの一覧を取得する list_buckets というメソッドをスタブ化した場合には以下のような結果が返ってきます.

# スタブデータを用意しない場合
irb(main):001:0> require 'aws-sdk-s3'
=> true
irb(main):002:0> s3 = Aws::S3::Client.new(stub_responses: true)
=> #<Aws::S3::Client>
irb(main):003:0> s3.list_buckets
=> #<struct Aws::S3::Types::ListBucketsOutput buckets=[], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">>
irb(main):004:0> s3.list_buckets.buckets
=> []

# スタブデータを用意した場合 (foo と bar というバケット名を返されることを想定する)
irb(main):005:0> bucket_data = s3.stub_data(:list_buckets, :buckets => [{name:'foo'}, {name:'bar'}])
=> #<struct Aws::S3::Types::ListBucketsOutput buckets=[#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">>
irb(main):006:0> s3.stub_responses(:list_buckets, bucket_data)
=> [{:data=>#<struct Aws::S3::Types::ListBucketsOutput buckets=[#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">>}]
irb(main):007:0> s3.list_buckets.buckets
=> [#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>]
irb(main):008:0> s3.list_buckets.buckets.map(&:name)
=> ["foo", "bar"]

Rspec からスタブを利用する

今回は awspec の実装をそのまま参考にさせて頂いて, 以下のようにスタブが利用されるように実装を行いました.

  • spec/spec_helper.rbstub_responses: true を追加
# spec/spec_helper.rb
require 'rspec'
require 'sample_cli'

Aws.config.update(stub_responses: true)
...
  • スタブデータは lib/stub/${対象 AWS サービス}.rb を追加 (以下は S3 の list_bucketslist_object_v2 メソッドのスタブデータ)
# lib/stub/s3.rb
Aws.config[:s3] = {
  stub_responses: {
    list_buckets: {
      buckets: [
        {
          name: 'foo'
        },
        {
          name: 'bar'
        }
      ]
    },
    list_objects_v2: {
      contents: [
        {
          key: 'foo'
        },
        {
          key: 'bar'
        },
        {
          key: 'baz/key'
        }
      ]
    }
  }
}
  • スタブデータを各テストに読み込む為の小さなクラスメソッドを用意
# lib/stub.rb
module SampleCli
  class Stub
    def self.load(type)
      require File.dirname(__FILE__) + '/stub/' + type
    end
  end
end
  • テストは以下のように実装
require 'spec_helper'
SampleCli::Stub.load 's3'

describe 'sample_cli check subcommand buckets' do
  it 'have bucket names' do
    expect(SampleCli::S3.new.buckets).to eq(%w(foo bar))
  end

  it 'have bucket names by cli' do
    output = capture(:stdout) { SampleCli::CLI.start(%w{buckets}) }
    expect(output).to match('foo\nbar\n')
  end
end

describe 'sample_cli check subcommand objects' do
  it 'have object keys' do
    expect(SampleCli::S3.new.objects('foo')).to match(%w(foo bar baz/key))
  end

  it 'have object keys by cli' do
    output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) }
    expect(output).to match('foo\nbar\nbaz/key\n')
  end
end

上記の通り, テストはサブコマンドを実行した場合 (have bucket names by clihave object keys by cli) と, サブコマンドから呼ばれるメソッド (have bucket nameshave object keys) に対してテストを行っています. これを以下のように実行してテストします.

$ bundle exec rake spec:s3
...

sample_cli check subcommand buckets
  have bucket names
  have bucket names by cli

sample_cli check subcommand objects
  have object keys
  have object keys by cli

Finished in 0.11505 seconds (files took 3.01 seconds to load)
4 examples, 0 failures

いい感じです.

rubocop 警察

せっかくなので, rubocop でコーディングスタイルに準拠しているかもチェックしています. 例えば, 以下のようなコードは警察の取締対象となります.

  it "have object keys by cli" do
    output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) }
    expect(output).to match("foo\nbar\nbaz/key\n")
  end

容疑は Prefer single-quoted strings when you don't need string interpolation or special symbols. です. 実際の取締の様子です.

$ bundle exec rake spec:rubocop
Running RuboCop...
Inspecting 18 files
.....C............

Offenses:

spec/s3_spec.rb:20:6: C: Prefer single-quoted strings when you don't need string interpolation or special symbols.
  it "have object keys by cli" do
     ^^^^^^^^^^^^^^^^^^^^^^^^^

18 files inspected, 1 offense detected
RuboCop failed!

釈放される為にはダブルクウォートをシングルクォートに書き換えることで釈放されます.

  it 'have object keys by cli' do
    output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) }
    expect(output).to match('foo\nbar\nbaz/key\n')
  end

釈放の様子です.

$ bundle exec rake spec:rubocop
Running RuboCop...
Inspecting 18 files
..................

18 files inspected, no offenses detected

いい感じですね.

Travis CI

こちらもせっかくなので Travis CI でテストを走らせます. .travis.yml は以下の通りです.

sudo: false
language: ruby
rvm:
  - 2.5.1
before_install: gem install bundler -v 1.16.2

テスト結果は以下の通りです.

f:id:inokara:20181105082953p:plain

以上

AWS SDK for Ruby を利用した CLI ツールのサンプル的なものを実装検討してみました. 素人のたわごとになりますので, いろいろとツッコミどころがあるとは思いますが, これをテンプレートとしてより良いオレオレツールが作れるようになると嬉しいなあ. あと, 先人が書かれたコードを読むというのは本当に勉強になりました.

2018 年 11 月 04 日 (日)

ジョギング

  • 山王公園往復
  • 右の胸が痛み, 状況は変わらず...むしろ走った後に痛みひどくなる...困った

日課

  • お休み

博多デート

  • 奥さんと歩いて博多駅界隈に買い物に出かける
  • お昼すぎからさくっと歩いて博多駅界隈に行けるというのは, この近隣に住んでいるメリットなのかもしれない

右肺の痛み

  • 肺なのか, 肋骨なのか解らないけど痛みが続いている...
  • 痛み止めと湿布はもらって真面目に服用したり貼っているけど状況は良くならず...辛い

2018 年 11 月 03 日 (土)

ジョギング

  • おやすみ
  • 引き続き, 右の胸が痛い...

日課

  • こっちもお休み

足のケア

  • ケケのマッサージをうける
  • ジョギングも休んでいるのでだいぶん披露が抜けてきた感
  • 博多駅まで歩いて行ったりして良い運動になった

2018 年 11 月 02 日 (金)

ジョギング

  • おやすみ
  • 右の胸が痛い...

日課

  • こっちもお休み

一応, 整形外科に行く

  • 右の胸のあたりの痛みがひどくなったので近所の整形外科へ
  • 一応, 骨には異常無し

今日のトゥウィート

本当に申し訳ございません...