解いて終わりにしない、プログラミング初学者にオススメしたい練習問題を使った基礎トレ。


Progate、ドットインストールを終えた後の勉強として、書籍などで基礎固めをするのは鉄板の流れになっています。
そして多くの初学者用書籍には練習問題がついています。

この練習問題を「解いて、解答例を見て、終わり」にせず、きちんと筋肉に変えていくために行なっていることをまとめました。

環境

macOS 10.14.1 (Mojave)
Homebrew 2.2.5
Ruby on Rails 5.2.4.1

背景

プログラミング初学者にとって、練習問題は大事な勉強方法だと思っています。

なぜなら、プログラミングに限らず「ものごとが上達すること」を考えたときに、初学者が上達するなるには以下が重要だと言われているからです。

  • 基本的な動作なんども反復 する
  • 情報を 自分の言葉 でアウトプットする
  • 解きっぱなしにせず、 アウトプットを検証する

練習問題は実務に入っていない未経験者が、自分の頭で考えながらコードを繰り返し検証するのに良い教材になります。
■参考
上達の法則

いつ勉強してたの?っていうぐらいたいして全然勉強せずに東大に入ってしまう人は、どのように受験勉強をしている (していた) のですか?

練習問題

もちろん書籍に書いてる練習問題でもOKですが、練習問題はいくらでもネットに落ちているので便利ですね。
自分の学習している言語ではなくても、答えを考えればいいので「プログラミング 練習問題」「プログラミング 初心者 練習」とかで検索しても結構色々出てきます。

以下は僕が実際に練習している問題ページです。

プログラミングスレまとめ

PRACTICE PYTHON

解き方(方針)

ただ解くだけで終わらないためには、以下2つの記事が方針の参考になります。
どちらも好きな記事で、それこそ何度も繰り返し読んでいます。

効果的なコーディング練習問題の解かせ方

きみたちは今まで何のためにRailsでMVCパターンを勉強してきたのか

上記の上達に関する考え方や、練習問題の記事から考えるに、練習問題を解く方針として以下をプロセスに組み込む必要があると分かります。

■問題の選び方
- ややこしい操作や使用頻度の低いメソッドではなく、頻出する動作を中心に問題を選ぶ
- 検証ができるように、できればディスカッションをするスレなどがあるサイトを選ぶ

上記のようなサイトは条件に当てはまるかと思います。
その他Qiitaなどに投稿されている問題を自分で解いてみるのも良いかと。

■問題の解き方
- 手動入力や目視確認は極力排除する
- 関数の結果を純粋なオブジェクトとして表現する
- 処理を出来るだけ共通化する

解き方(Rubyでの詳細例)

今回はこちらの問題を使用します。

# http://vipprog.net/wiki/exercise.html#x9ab58f6
# Hello World!を5回表示させてください。
# print等は1回の使用にとどめてみてください。
# 可能ならコマンドラインから入力を受け取って、n回表示するように改造してください。

とりあえずの回答が以下。
コンソールで受け取った数字に対してHello Worldを繰り返して表示しました。普通にやったらここまでで学習が終わってしまいます。実際サイトを見る限りだと、このレベルの解答が解答例として載っている。


num = gets.to_i

num.times do |n|
  print "Hello World! ", n+1, "\n"
end

問題1)入力が手動になっており煩雑
改善1)手動入力を関数に変換
このままだと、毎回手入力する必要があるため、変更を加える。関数にしておいて、数字を指定できるようにしてみましょう。


def repeat_hello(n)
  n.times do |n|
    print "Hello World! ", n+1, "\n"
  end
end

repeat_hello(5)

問題2)テストの実施や、コンソール以外での使用がしにくい
改善2)関数の出力をテスト化
関数にはなったものの、出力がprintのため、このままだとコンソール以外で使いにくくテストも実施しづらいです。そこで以下の変更を行います。

  • ロジック本体とコンソール出力用に分解
  • minitestを追加

require 'minitest/autorun'

def repeat_hello(n)
# ロジック本体
  rows = []
  n.times do |n|
    rows.push("Hello World! #{n + 1}")
  end

  rows
end

def main
  # コンソール出力用
  rows = repeat_hello(5)
  puts rows
end

class RepeatHello < Minitest::Test
  def test_repeat_hello
    assert_equal ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4', 'Hello World! 5'], repeat_hello(5)
  end
end

問題3)テストの冗長化が気になる
改善3)テストパターンのテストデータ化
1つだから良いものの、複数の引数のテストを試したいとなった時に文が長いのが気になってきます。
安全にチェックするために、テストのパターンが複数試せた方が良いので、以下の修正を行います。


require 'minitest/autorun'

def repeat_hello(n)
  # ロジック本体
  rows = []
  n.times do |n|
    rows.push("Hello World! #{n + 1}")
  end

  rows
end

def main
  # コンソール出力用
  rows = repeat_hello(5)
  puts rows
end

class RepeatHello < Minitest::Test

  def test_repeat_hello

 テストデータの作成
    test_data = [
      [],
      ['Hello World! 1'],
      ['Hello World! 1', 'Hello World! 2'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4', 'Hello World! 5']
      ]

# テストの繰り返し実施
    test_data.each_with_index do |expected, id|
      assert_equal expected, repeat_hello(id)
    end
  end
end

改善4)ロジック本体のリファクタリング
ここまできたら、テストで結果を手軽に試せるので、あとはロジック本体のリファクタリングに入れると。
今回の例ではわざわざややこしくしてるけど、例えば先に1.2.3.4.5のような配列を作ってからpushするような書き方はできるかな、とか、色々試して楽しめます。


def repeat_hello(n)
  # ロジック本体 => 変更
  list = []
  n.times do |i|
    list.push(i + 1)
  end

  rows = []
  for l in list
    rows.push("Hello World! #{l}")
  end

  rows
end

まとめ

早く新しいことを覚えたいと、つい流してしまいがちな練習問題。
ですが、新しいことを覚えるときに大切な要素が詰まっていますので、毎日1題ずつでも継続していきましょう!

Qiita始めたばかりですので、改善のためにもぜひコメントくださいませ。

追記

@scivola さんにコメントいただきました。ありがとうございます。
ロジック本体を以下のように修正し、テストを問題なく通過することを確認しました。

def repeat_hello(n)
  # ロジック本体
  (1..n).map{ |i| "Hello World! #{i}" }
end