ようへいの日々精進XP

よかろうもん

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 日 (金)

ジョギング

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

日課

  • こっちもお休み

一応, 整形外科に行く

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

今日のトゥウィート

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

アルゴリズムとプログラミング 「第 5 章 関数」の予習とまとめ

これは

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

1. 関数

序説

  • 頻繁に使うコードは関数 (function) として 1 つのコードにまとめると再利用が可能
  • 構造化言語において関数は構造化を実現する上で重要な役割を果たす
  • プログラミング言語によっては, 関数はサブルーチン, プロシージャと呼ばれることもある

1.1 プログラムコードの関数

以下, 冗長なコードの例.

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

int main()
{
  int i;

  for (i = 0; i < 10; i++)
    printf ("%d", i);
  printf ("\n");
  
  for (i = 0; i < 10; i++)
    printf ("%d", i);
  printf ("\n");

  for (i = 0; i < 10; i++)
    printf ("%d", i);
  printf ("\n");

  return 0;
}

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

root@0be431eebb77:/work# gcc ex5-1.c -o ex5-1 -g3
root@0be431eebb77:/work# ./ex5-1
0123456789
0123456789
0123456789

以下は, このコードから 0 〜 9 の数値を表示する print_numbers を関数に切り出した場合のコード.

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

void print_numbers (void)
{
  int i;

  for (i = 0; i < 10; i++)
    printf ("%d", i);
  printf ("\n");    
}

int main ()
{
  print_numbers ();  
  print_numbers ();  
  print_numbers ();  
  
  return 0;
}

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

root@0be431eebb77:/work# gcc ex5-2.c -o ex5-2 -g3
root@0be431eebb77:/work# ./ex5-2
0123456789
0123456789
0123456789
  • 結果はどちらのコードも同じになるがプログラムコードの見通しは関数化したプログラムコードの方が俄然良い
  • main も関数の 1 つであり, これはコードの初めに呼ばれる特殊な関数である
  • main 関数よりも後に関数を記述する場合, 関数プロトタイプ (function prototype = 関数の宣言) が必要になる
  • 関数の宣言には, 関数数の戻り値の型, 関数の引数の型, 関数の引数の数等の情報を記述する

以下は, main 関数より前に print_numbers 関数の関数プロトタイプが記述されている.

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

void print_numbers (void);

int main ()
{
  print_numbers ();  
  print_numbers ();  
  print_numbers ();  
  
  return 0;
}

void print_numbers (void)
{
  int i;

  for (i = 0; i < 10; i++)
    printf ("%d", i);
  printf ("\n");    
}

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

root@0be431eebb77:/work# gcc ex5-3.c -o ex5-3 -g3
root@0be431eebb77:/work# ./ex5-3
0123456789
0123456789
0123456789
  • main 関数よりも前に関数を記述する場合, 関数プロトタイプの宣言は省略可能...だが, バグ防止等の観点から関数プロトタイプの世間を強く推奨している書籍が多い
  • ライブラリに含まれる様々な関数にも関数プロトタイプに関する情報があり, 通常, これらはヘッダファイルに記述されている

例えば, sqrt 関数 (平方根を求める関数) の関数プロトタイプ (double sqrt (double);) であり, math.h というヘッダファイルに関数プロトタイプの記述があるので, sqrt 関数を利用する場合には, 以下のように math.h をインクルードして利用する.

...
#include <stdio.h>
#include <math.h>
...

1.2 関数とスコープ

  • 変数にはスコープ (scope) と呼ばれる属性がある
  • 変数のスコープとは, 変数が有効であるコードの範囲のことである
  • スコープの範囲の違いによって, グローバル変数 (global variable) とローカル変数 (local variable) がある
  • グローバル変数では, コードの全ての範囲で変数が有効である
  • ローカル変数では, ローカル変数が宣言されたブロック内の範囲だけで変数が有効である

以下のコードでは, 関数 f と関数 g の中で同じ変数名 i が使われているが, それぞれの関数でスコープが異なる.

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

void g (void)
{
  int i;
  for (i = 0; i < 3; i++) {
    printf ("a");
  }
}

void f (void)
{
  int i;
  for (i = 0; i < 5; i++) {
    g ();
  }
}

int main ()
{
  f ();
  return 0;
}

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

root@0be431eebb77:/work# gcc ex5-4.c -o ex5-4 -g3
root@0be431eebb77:/work# ./ex5-4
aaaaaaaaaaaaaaa

同じ変数名のグローバル変数とローカル変数を宣言することも可能であるが, ローカル変数が宣言されているブロック内では, グローバル変数よりもローカル変数の方が優先される.

2. 引数, 戻り値, 値渡し, 参照渡し

2.1 引数と戻り値

  • 引数 (parameter) は, 実引数 (argument, actual argument) と仮引数 (parameter, formal parameter) の 2 つがある
  • 実引数とは関数にわたす値のこと
  • 関数の中で引数の値を受け取る変数は仮引数という
  • 戻り値 (return value) とは関数が返す値

以下のコードは三角形の面積 ( area) を求める関数の例.

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

float triangle (float base, float height)
{
  float c;
  c = (base * height) / 2.000F;
  return c;
}

int main ()
{
  float t;
  t = triangle (3.00, 4.00);
  printf ("triangle = %f\n", t);
  t = triangle (5.00, 6.00);
  printf ("triangle = %f\n", t);
  
  return 0;
}

尚, 三角形の面積は以下の公式で求められる.

$$ area = \frac{1}{2} \times base \times height $$

上記のコードにおいて, 底辺 ( base) と高さ ( height) の 2 つの float 型の引数を持ち, この引数に値が渡されると関数 (triangle) が三角形の面積を計算する. この関数の戻り値として float 型の面積の値が関数から返される. 3.00, 4.00 及び 5.00 と 6.00 が実引数であり, 関数で宣言されている base と height が仮引数となる.

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

root@0be431eebb77:/work# gcc ex5-5.c -o ex5-5 -g3
root@0be431eebb77:/work# ./ex5-5
triangle = 6.000000
triangle = 15.000000

2.2 "値渡し"と"参照渡し"

  • 引数の渡し型には, 値渡し (pass by value) と 参照渡し (pass by reference) と呼ばれるものがある
  • 値渡し は変数のコピーが作成されて値が渡される方法で, 関数内で変数の値が変更されても元の変数の値は変わらない
  • 参照渡し は, 変数そのものを渡す為, 関数内で変数の値の変更は, 呼び出した側の変数にも影響して値が変わる

以下のコードは 値渡し参照渡し の場合では, 変数 a の出力する値が異なる例.

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

void add_pass_by_value (int i)
{
  i = i + 1;
}

void add_pass_by_reference (int *i)
{
  *i = *i + 1;
}

int main()
{
  int a;
  a = 10;
  add_pass_by_value (a);
  printf ("%d\n", a);

  a = 10;
  add_pass_by_reference (&a);
  printf ("%d\n", a);
  
  return 0;
}

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

root@0be431eebb77:/work# gcc ex5-6.c -o ex5-6 -g3
root@0be431eebb77:/work# ./ex5-6
10
11

C 言語では変数ポインタを取得出来る為, 変数へのポインタを渡すことで 参照渡し と同じ効果がある アドレス渡し が可能であるとのこと...ポインタの話がサラッと出てきているので辛い...ポインタについては別で勉強する必要があるな.

3. 再帰関数

再帰呼び出し (recursion call) とは, 関数や手続き等が, 自分自身を呼び出して実行することで, アルゴリズムの中には, 再帰的にプログラムを記述することによって効果的な処理を出来るものがある. 以下, 再帰プログラミングの例として用いられる階乗 (factorial) の計算式で, 1 から  n までの自然数の総乗である. 階乗は  n! で表現され, 以下のような式で定義される. (尚,  0!=1 と定義されている.)

$$ n!=\prod_{k=1}^n k=n\times\left(n-1\right)\times\left(n-2\right)\cdots\times3\times2\times1 $$

実際の階乗の計算例は以下の通り. $n$ の値が大きくなるに従って急激に数が増加する.

1! = 1
2! = 2 x 1 = 2
3! = 3 x 2 x 1 = 6
4! = 4 x 3 x 2 x 1 = 24
..
..
10! = 10 x 9 x 8 x 7 x 6 x 5 x 4 x 3 x 2 x 1 = 3,628,800

この階乗を計算をコードに落とし込むと以下のような感じで factorial 関数を再帰で呼び出して計算結果を返す. factorial 関数は, factorial 関数内で  n - 1 という引数で呼び出すようになっており,  n 0 になった時点で 1 を返し, その時点で factorial 関数の呼び出しを止める.

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

int factorial (int n)
{
  if (n == 0) {
    return 1;
  } else {
    return n * factorial (n - 1);
  }
}

int main()
{
  int i;
  i = 5;
  printf ("%d! = %d\n", i, factorial (i));
  
  return 0;
}

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

root@0be431eebb77:/work# gcc ex5-7.c -o ex5-7 -g3
root@0be431eebb77:/work# ./ex5-7
5! = 120

これを gdb でステップ実行してみると以下のような感じになる.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /work/ex5-7

Breakpoint 1, main () at ex5-7.c:16
16        i = 5;
(gdb) s
17        printf ("%d! = %d\n", i, factorial (i));
(gdb) s
factorial (n=5) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n -1);
(gdb) s
factorial (n=4) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n -1);
(gdb) s
factorial (n=3) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n -1);
(gdb) s
factorial (n=2) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n -1);
(gdb) s
factorial (n=1) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n -1);
(gdb) s
factorial (n=0) at ex5-7.c:6
6         if (n == 0) {
(gdb) s
7           return 1;

return n * factorial (n -1); が 5 回繰り返された後に return 1; が返っていることが解る.

いきなり, ポインタとは...

本教材ではポインタがサラッとしか触れられていないのが辛い. とりあえず, 本教材に書かれている内容を抜粋する.

  • ポインタは変数等のオブジェクトを指すもの
  • * 間接演算子 (indirection operator) は, ポンタを介して値に間接的に参照し, ポインタが指し示すアドレスに格納されている値を参照する演算子である
  • & アドレス演算子 (address-of operator) は, オペランドのアドレスを与える, 変数のアドレスを取得する演算子である

これだけではピンと来ないので...自分で調べるしかない.

演習問題

問 5.1

以下のコードは, どのような値を出力するか答えなさい.

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

float trapezoid (float a, float b, float h)
{
  float c;
  c = ((a + b) * h) / 2.000F;
  return c;
}

int main ()
{
  float t;
  t = trapezoid (3.00, 4.00, 5.00);
  printf ("trapezoid = %f\n", t);
  t = trapezoid (5.00, 6.00, 7.00);
  printf ("trapezoid = %f\n", t);
 
  return 0;
}

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

root@0be431eebb77:/work# gcc q5-1.c -o q5-1 -g3
root@0be431eebb77:/work# ./q5-1
trapezoid = 17.500000
trapezoid = 38.500000

これは台形の面積を求めるコードだった. 尚, C 言語では, 関数に設定出来る引数の数の上限はコンパイラに依存している. C90 規格では 31 個, C99 規格では 127 個まで利用出来る.

問 5.2

以下のコードは, どのような値を出力するか答えなさい.

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

struct student
{
  int id;
  char grade;
  float average;
};
typedef struct student STUDENT_TYPE;

STUDENT_TYPE initialize_student_record (STUDENT_TYPE s)
{
  s. id++;
  s. grade = 'x';
  s. average = 0.0;
  return s;
}

int main ()
{
  STUDENT_TYPE student;

  student. id = 20;
  student. grade = 'a';
  student. average = 300.000;
  printf ("%d %c %f\n", student. id, student. grade, student. average);
  student = initialize_student_record (student);
  printf ("%d %c %f\n", student. id, student. grade, student. average);

  return 0;
}

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

root@0be431eebb77:/work# gcc q5-2.c -o q5-2 -g3
root@0be431eebb77:/work# ./q5-2
20 a 300.000000
21 x 0.000000

問 5.3

以下の再帰関数のコードは, どのような値を出力するか答えなさい.

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

int fibonacci (int n)
{
  if (n == 0) {
    return 0;
  } else if (n == 1) {
    return 1;
  } else {
    return (fibonacci (n - 1) + fibonacci (n - 2));
  }
}

int main ()
{
  int i;
  i = 10;
  printf ("fibonacci (%d) = %d\n", i, fibonacci (i));
}

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

root@0be431eebb77:/work# gcc q5-3.c -o q5-3 -g3
root@0be431eebb77:/work# ./q5-3
fibonacci (10) = 55

関数名から判るとおり, これはフィボナッチ数を計算するプログラムである. このプログラムをちょっと弄って 0 〜 11 まで数のフィボナッチ数列を計算させてみる.

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

int fibonacci (int n)
{
  if (n == 0) {
    return 0;
  } else if (n == 1) {
    return 1;
  } else {
    return (fibonacci (n - 1) + fibonacci (n - 2));
  }
}

int main ()
{
  int i;
  for (i = 0; i < 11; i++) {
    printf ("fibonacci (%d) = %d\n", i, fibonacci (i));
  }
}

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

root@0be431eebb77:/work# gcc q5-3a.c -o q5-3a -g3
root@0be431eebb77:/work# ./q5-3a
fibonacci (0) = 0
fibonacci (1) = 1
fibonacci (2) = 1
fibonacci (3) = 2
fibonacci (4) = 3
fibonacci (5) = 5
fibonacci (6) = 8
fibonacci (7) = 13
fibonacci (8) = 21
fibonacci (9) = 34
fibonacci (10) = 55

問 5.4

以下の再帰関数のコードは, どのような値を出力するか答えなさい.

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

void foo (int n)
{
  if (n < 15) {
    foo (n + 1);
    printf ("%d ", n);
  }
}

int main ()
{
  foo (0);
  return 0;
}

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

root@0be431eebb77:/work# gcc q5-4.c -o q5-4 -g3
root@0be431eebb77:/work# ./q5-4
14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

上記のように 14 から 0 の数値が出力される.

問 5.5

コード ex5-7.c の階乗を計算する再帰関数のコードで  i = 5 を変更し,  i = -1 として負の整数を引数にして関数を呼び出した場合, どのような問題があるか考えなさい.

以下, ex5-7c のコード.

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

int factorial (int n)
{
  if (n == 0) {
    return 1;
  } else {
    return n * factorial (n - 1);
  }
}

int main()
{
  int i;
  i = 5;
  printf ("%d! = %d\n", i, factorial (i));
  
  return 0;
}

とりあえず, ex5-7.c の実行結果.

root@0be431eebb77:/work# ./ex5-7
5! = 120

上記のコードを以下のように書き換えてみる.

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

int factorial (int n)
{
  if (n == 0) {
    return 1;
  } else {
    return n * factorial (n - 1);
  }
}

int main()
{
  int i;
  i = -1;
  printf ("%d! = %d\n", i, factorial (i));
  
  return 0;
}

これを実行する.

root@0be431eebb77:/work# ./q5-5
Segmentation fault

あちゃー. 一応, gdb で確認.

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /work/q5-5

Breakpoint 1, main () at q5-5.c:16
16        i = -1;
(gdb) s
17        printf ("%d! = %d\n", i, factorial (i));
(gdb) s
factorial (n=-1) at q5-5.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n - 1);
(gdb) s
factorial (n=-2) at q5-5.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n - 1);
(gdb) s
factorial (n=-3) at q5-5.c:6
6         if (n == 0) {
(gdb) s
9           return n * factorial (n - 1);
(gdb) s
factorial (n=-4) at q5-5.c:6
6         if (n == 0) {
(gdb)

上記のように factorial (n=-1), factorial (n=-2), factorial (n=-3) となる為, n == 0 の条件に合致しなくなり, 無限ループが発生して最終的には Segmentation fault となる.

以上

  • C 言語の関数の雰囲気が判ってきた
  • いきなりポインタとか構造体とか出てきてビビった...ちゃんと勉強する必要があるな

2018 年 11 月 01 日 (木)

ジョギング

  • 山王公園往復
  • 懸垂 x 5
  • 懸垂頑張りすぎたかも...右の胸のあたりに変な痛み...汗

JAWS-UG 福岡もくもく会

  • 今日は Cognito の User Pool をイジった (管理者権限でユーザーの登録等を行う CLI ツールを Go 言語で作ることにした)
  • gRPC とか GraphQL の話が出てきたので, これはやっぱり勉強せねばと思った次第

とんとん

久しぶりに美人居酒屋とんとん. 鶏のタタキと湯豆腐を食べた.

2018 年 10 月 31 日 (水)

ジョギング

  • 自宅から天神交差点まで (40 分弱)
  • 懸垂 x 5

日課

  • おやすみ

気づいたら 10 月も終わった... 今日のトゥウィート

あっという間に福岡マラソンだ.

ちょっと忘れそうになっていたけど, 鹿児島マラソンにも当選した. 入金は 11/15 まで. 忘れずに.