📖

プログラミング初学者の大学生が動かないコードを動かすには?

2022/12/25に公開

はじめに

年の瀬ですね。何かと話題の mast Advent Calendar 2022 も 21 日目を迎えました[1]
20 日目の記事は AMY🌮 さんの「University of Wisconsin-Madisonってどこ!」でした。続く 23 日目の記事はしょあさんの「プログラミング課題不正回答を防ぐ手法の提言」です[2]

さて、mast というのは筑波大学[3]情報学群情報メディア創成学類[4]の略称です。
筑波大学の情報系は入学して 1 年間は殆ど情報系の実習を行わないのですが、2 年次になると春先から唐突に C 言語を書かされる授業が乱発[5]し、その結果「インターネットの大海原から拾ってきたコードを継ぎ接ぎで合わせてみたら大量のエラーが出てきたよ〜〜たすけて〜〜」みたいな人が跡を絶ちません。

本稿ではそうした状況を回避するべく、プログラミングの初学者でも取り敢えず動かないコードを動かせるようにすることに重点を置いて、そのためのプラクティスをいくつかご紹介したいと思います。

「動かない」とは?

「動かない」とは非常に多義な言葉で、コンパイルが通らない、ランタイムエラーになる、想定通りの挙動をしない等、有象無象の現象を表すようです。1 つずつ確認してみましょう。

1. コンパイル/ランタイムエラー

コンパイルエラーは、C, Go, Java, C#, TypeScript, TeX 等のコンパイルやトランスパイルが要求される言語に見られるエラーです。冒頭の「大量のエラーメッセージが出てきた」パターンは大抵これに該当する気がします。
一方のランタイムエラーは、コンパイル時には検出できなかったエラーを指します。両者を比較すると、一般に以下の特徴を有します。

  • コンパイルエラー
    コンパイル時に発生するため、当然ながら機械的に検出可能なエラー。この場合はエラーメッセージを丁寧に読むことで解決できることが多い。例)構文エラー、型エラー、未割り当て変数の利用
  • ランタイムエラー
    実行時(runtime)に発生するエラー。機械的な検出が難しいエラーであるため、コンパイルエラーよりも厄介。例)配列の境界外アクセス、メモリ割り当て失敗、スタックオーバーフロー

2. 想定通りの挙動をしない

エラーを見ると気が萎えるのですが、まだエラーとして報告してくれているだけマシで、実際は「取り敢えず動いてはいるけど想定外の挙動をする」パターンが最も厄介です。

エラーの治し方

そうした現象に遭遇した場合に、どのような手法を用いればいいのでしょうか。我々が取るべき処方箋を考えてみます。

1. エラーメッセージを読む

大抵のエラーの原因はエラーメッセージに書いてあります。人間は大量の英文を見ると徐に目を背けてしまう傾向があるのですが、要点をつまみ出すだけでも結構な情報が取得できます。英文解釈におけるディスコースマーカーと一緒ですね。

試しに、誤りだらけの sample.c をコンパイルしてみます。

sample.c
int main(int argc, char *argv[]) {
  int count = 0;
  for (i = 0; i < 10; i++) {
    func(++count);
  }
}
int func(int no) {
  printf("%d", no);
  return 0;
}
$ gcc sample.c
sample.c:3:8: error: use of undeclared identifier 'i'
  for (i = 0; i < 10; i++) {
       ^
sample.c:4:5: error: implicit declaration of function 'func' is invalid in C99
    func(++count);
    ^
sample.c:9:3: error: implicitly declaring library function 'printf' with type 'int (const char *, ...)'
  printf("%d", no);
  ^
sample.c:9:3: note: include the header <stdio.h> or explicitly provide a declaration for 'printf'
5 errors generated.

なんか大量の英文が出てきた、うげー……でもよくよく見てみると

sample.c:3:8: error: use of undeclared identifier 'i'

冒頭にエラーが発生した行番号/列番号が記述されていますね、後続するエラー文を読んでみると「error: use of undeclared identifier 'i'」、すなわち未宣言の識別子(変数等)である i を使用していることを指摘するメッセージのようです。ということは int i 的な宣言を関数の先頭に追加すれば良いだけです。なんだ簡単ではありませんか。
――といった具合に、最近のコンパイラ[6]はとても優秀なので、エラーの位置や内容まで丁寧に教えてくれます。基本的にはこの内容に従って修正するのみです。

2. エラーメッセージを検索する

慣れるとエラーメッセージの適当な箇所だけを引っ張り出して要因を特定できるのですが、最初のうちはなかなか難易度が高いかと思います。また、読んでもさっぱり解らん!という場合もあります。こうした場合は取り敢えず脳死でググってみるのも手です。

言語にもよりますが、大体5―6 割のエラーは日本語文献が見当たり、残り 2 割程度も何らかの英語文献がヒットすると思います。それでも解らなければ仕様書を読みましょう。

信頼できるドキュメントにあたろう

脳死でググると言えどもテクはあります。特にドキュメントの情報源(ソース)は重要で、適当なドキュメントを読んで実装すると思わぬ落とし穴にハマるケースも多いです。

最近ではテック……や侍……に代表されるオウンドメディアが検索結果の上位に表示される場合が多く、これらは主に自社が運営するスクール等に呼び込むこと(あるいは単にアフィカス)を目的としています。したがって SEO[7] 対策は上手いのですが、中身は薄く、読むだけ時間の無駄……というケースも多いので注意が必要です。uBlackList 等の拡張機能で非表示にすることをお勧めします。

3. printf を入れてみる

デバッグの王道です。以下のような事象を確認するために、ソースコードの随所に printf 関数を入れて動作検証を行います。

  • プログラムが動作しているか
    何も表示されない場合はそもそも根本が間違っている(例:実行ファイルが違った、出力がリダイレクトされていて動作していないように見えていた)可能性があります[8]
  • 変数の状態
    例)変数が所望の値になっていない、ポインタが指すアドレスが間違っている、NULL になっている
  • 該当箇所が実行されているか
    例)関数の処理が途中で打ち切られている、条件分岐に誤りがある
  • 無限ループに陥っていないか
    例)再帰関数の終了条件をミスっている

printf を挿入する箇所としては、冒頭から処理を追いながら少しずつ後ろにずらしていく、人力二分探索をする、エラーが起きそうな箇所を重点的に攻める等の方法があります。IDE[9] を導入している場合は、デバッガを使用するとより効率的に確認作業を行えます。

https://twitter.com/noimi_kyopro/status/1222005958655660033
二分探索は様々な場面で活用されている

4. 小規模なテストケースで試してみる

ソートのアルゴリズム課題に対して、初っ端から 1 万行の入力を突っ込んでいませんか? まずは入力数を 10 のオーダー程度に絞り、上手く作動するかを検証すると良いです。

特定の入力に対してのみエラーが発生する(エッジケースコーナーケースと呼ばれる)場合もあります。ある入力行に対してのみエラーが発生する場合は、問題のない入力を少しずつ削っていき、「該当のエラーに遭遇する最小のケース」を作成して検証することが望ましいです。
また最近では、テストコードを先行して書く開発手法であるところのテスト駆動開発(TDD)が有効とされています。

5. 別の言語で実装してみる

そもそもビジネスロジックやアルゴリズムが間違っている場合も想定されます。そうした場合に備えて、該当箇所を一度、別言語で実装して誤りがないかを確認し、その上で目的の言語に移植するという手段もあります。
この令和の時代であれば C よりも高水準な言語は山程存在しており、大抵の言語には連結リストもスタックもヒープも GC[10] も弱参照も標準で実装されています。

人に聞くとき

パソコンカタカタしてそうな友人や知人に聞いてみることで、効果的なアドバイスが得られるケースは多いです。
ただし、相手は人間なので執拗に聞きまくると当然辟易されますし、そもそも人間は寝不足、機嫌、アルコール等の様々な要因から適当な返答をすることも多い生物です。そんな適当な人間から有用な回答を引き出すための方法を考えてみましょう。

Before: やってはいけない!質問の仕方

  • コードだけ投げない
    なにもわからない
  • 「これって動くかな?」
    人間はインタプリタではありません。
  • 「動きません」
    冒頭に述べた通り、「動く」は非常に多義な言葉で様々な解釈を持ちます。言葉の意味をエスパーすることは難しいので、もう少し詳細に報告してくれるとありがたいです。
  • Syntax Error なコードを「エラーの原因が分からないと」投げない
    エラーメッセージに全部書いてあるやん

After: 正しい聞き方

的確な回答を得るためには、以下の項目を共有しましょう。

  • 背景
    どのようなプログラムを作成しているか
  • ソースコード
    エラーを再現可能なソースコード。スクリーンショットよりもテキストで直接(すなわちコピペで実行できる状態)で送ることが望ましい
  • 入力・出力
    実行コマンド、入力ファイル、エラーメッセージ、出力、スクリーンショット等
  • 動作環境
    OS、CPU アーキテクチャ、コンパイラ/ブラウザ/IDEの種類・バージョン 等

エラー調査を引き受けた人がどのように解決を試みるかといえば、まずエラーメッセージを読み、手元の環境でバグが再現するかを試します。しかしながら環境が変わればコードは動いてしまうことも多いため、出来るだけエラーが発生した環境を再現出来るような情報を伝えてあげることが重要です。

バグを生みにくいコードを生むには?

バグの要因として案外多いのが ij を間違えた等の凡ミスです。未然に防げるバグは未然に防ぐべく、意識すべき点をいくつかご紹介します。

解りやすい変数名・関数名を使う/命名規則を守る

a, b, c 等の変数名は何を意図しているかが解らないので避けましょう。それであればローマ字で namae, ninzu の方がまだマシです。英語で name, personCount であればなお望ましいです。

また、言語毎に定められた命名規則スタイルガイドを遵守し、表記揺れを減らすことも重要です。命名規則としては、以下のような項目が定められることが多いです。

  • 複合語の表記方法
    例)get_name(スネークケース), getName(キャメルケース), GetName(アッパーキャメルケース、パスカルケース)、get-name(ケバブケース)
  • プレフィックスの有無
    例)プライベート変数には _ を付ける、ハンガリアン記法
  • 使用する語彙
    例)取得する関数は get から始める

言語にもよりますが、関数は多くの場合 動詞 + 名詞 で命名することが多いです。
例えばある人の名前を取得/設定/更新する場合は getPersonName, setPersonName, updatePersonName と命名します。また、更新できるかなどの bool 値を返す関数は、isEnable, hasQualification, existsStudent 等の三単現の動詞を先頭に使用します。

命名に使用する語彙としては、以下の記事等が参考になります。

https://qiita.com/KeithYokoma/items/2193cf79ba76563e3db6#コレクションの操作に関するメソッド

フォーマットする

コードは積極的にフォーマット(整形)しましょう。
見た目が実行結果に直接影響を与えることはありません[11]が、インデントの数や中括弧の配置等が統一されていると読みやすく、保守もしやすいコードに繋がります。近年では Prettier のようなコードフォーマッタを用いることで、保存時等に自動フォーマットの恩恵を享受することができます。

うわっ、私のコード、汚すぎ…?

駄目コード
void func ( int a,  int b)  {
   int x;
  int y =  1;
  for(x=0;x<10;x++){y++;printf("%d %d",x,  y);} 
}
改善例
void func (int a, int b) {
  int x;
  int y = 1;
  for(x = 0; x < 10; x++) {
    printf("%d %d", x, ++y);
  } 
}

ループを使う

反復処理はループや関数にまとめて、できるだけ同じ処理を複数回書かないようにするのが鉄則です。ソフトウェア工学の世界では DRY 原則や OAOO (Once And Only Once) 原則と呼ばれます。
同じ処理をひたすら羅列したソースコードを意外に多く見掛けましたが、類似するコードを大量に書くとミスを誘発するほかタイピング量も増えて良いことはありません。

ダメコード
func(0, "a", "test")
func(1, "b", "test")
func(2, "c", "test")

→ 対処法:リスト(または配列)を作り、enumerate で回す

改善例
data = ["a", "b", "c"]
for i, v in enumerate(data):
    func(i, v, "test")

変数を使い回さない/スコープを意識する

変数は可能な限り定数として宣言し、スコープ(有効範囲)は狭く取りましょう。変数が意図せぬところで書き換わっており、思わぬバグに繋がった……というのはバグフィックスにおけるにおける頻出パターンです。特にグローバル変数を乱用すると、グローバル空間が汚染されて阿鼻叫喚になるので避けましょう。

例えば JavaScript であれば、ES6 から letconst が導入されています。

if (条件) {
  let temp = 100;
  const tax = 0.08;
  var total = temp * tax;
  tax = 0.10; // TypeError: invalid assignment. 定数は書き換え不可
}
console.log(temp); // ReferenceError. 参照できない
console.log(total); // 108. 参照できてしまう

早期リターンを使う

条件分岐が複雑に入り組むとネストが深くなり、コードの可読性が下がります。こうした際には処理が終わった段階で積極的に return 文を挿入することで、以降のインデントを 1 個減らすことができます。

int func(string? text) {
  if (text == null) {
    Console.Writeline("Error!");
  }
  else {
    // 長い処理
  }
}
int func(string? text) {
  if (text == null) {
    Console.Writeline("Error!");
    return;
  }
  // 長い処理
}

コメントを書く

積極的にコメントを書く習慣を付けましょう。Pydoc や Javadoc 等のツールを用いると、コメントを書きながらドキュメンテーションも同時に行うことができます。IDE での入力支援にも表示されるなど便利です。

高階関数を使う

引数や戻り値に関数を取る関数を高階関数といいます。高階関数を用いると、本来は複数行を割く必要がある処理をワンライナー(1 行)で記述できる場合があります。ただし、あまり込み入ったコードを書くと逆に可読性を下げるのでやりすぎは禁物です。

let array0 = [];
for (let i = 0; i < 4; i++) {
  array.push(i);
}

const array1 = [...Array(4)].map((_, i) => i);

Console.log(array0); // [0, 1, 2, 3]
Console.log(array1); // [0, 1, 2, 3]

その他にも、三項演算子 a ? b : c や短絡評価 || などを上手く活用すると、if 文等を使用せずにコードを簡潔に記述できます。

Git を使う

バージョン管理システムである Git を活用しましょう。Git は、ファイルの過去の変更履歴を管理したり、複数人で同一のソースコードを共同編集したりする際に有用なソフトウェアです。これを用いることで、例えばある箇所からプログラムが作動しなくなった場合に、元のソースコードとは分離された環境(ブランチ)で、正常に作動していた地点(コミット)まで戻って別途開発を行うことが可能となります。

https://github.com/git/git

慣れるまでコマンドラインの操作は難しく感じるかもしれませんが、現在では様々なエディタで GUI から操作可能な Git 連携の拡張機能が用意されています。

車輪の再発明を避ける

車輪の再発明とは「広く確立された車輪を、あえて一から再発明する」こと、すなわち「既に定石の手法が存在するにも関わらず、一からオレオレなものを自作してしまう」事象を指します。議論の余地もあるのですが、一般には車輪の再発明は避けるべきです[12]

初学者は無敵状態に陥りやすく[13]、往々にして独自のアルゴリズムを考案してしまったり、ライブラリを使わずにフルスクラッチで実装を始めてしまったりすることがあります。それはそれで面白く勉強にもなるのですが、思わぬバグの温床にも繋がるのもまた事実です。また、フレームワークやライブラリの学習コストを忌避した結果、車輪の再発明に走ってしまう現象も多く見られます。

むすびにかえて

いかがでしたか? 情報学群に来たということは少なからず情報分野に興味を持ってきたのに、蓋を開けてみればひたすら黒い画面にカタカタする作業(あるいは数学)で萎えるよね〜という気持ちは十分に理解できます。何事も気持ちは大事なので、とりあえず興味を持つために以下のようなところから始めてみるのは手だと思います。

言語を変える

授業で扱ったから Emacs で C89 を書かなければならないと思い込んでいませんか!? 世の中にはもっとモダンな言語と開発環境が溢れています。JavaScript や Processing はグラフィックスにも強い軽量言語で、グラフィックスや UI、ものづくりが好きなメ創の人々には打って付けの言語だと思います。

労働と結び付ける

大学生を対象とするコーディングの求人はそれなりにあります。多くの場合はインターンや業務委託の形態を取るようです。お金を外発的動機づけとするのはあまり良い方法ではないですが、賃金が発生する以上は真面目に取り組むことになるので、プログラムに慣れるには手っ取り早い気もします。加えて、一般的なアルバイトに比べると賃金が高いことが多いです。

休息を取る

良いコードは良い睡眠から! 休息は大切です。

折角の冬休み、どうぞ良い年末年始[14]をお過ごしください!

脚注
  1. 投稿遅れてすみません ↩︎

  2. 22日目の記事は投稿されていません ↩︎

  3. 名門 ↩︎

  4. Media Arts, Science and Technology の略らしい ↩︎

  5. 今年一の鬼門と称される GC12701 プログラミング ↩︎

  6. 手元の環境が macOS なので、gcc で出てくるコンパイラは clang ↩︎

  7. 検索エンジン最適化。検索時に上位に表示されるように Googlebot をハックする ↩︎

  8. 別のソースコードをコンパイルしていた、といった勘違いをしている人は案外見掛けました ↩︎

  9. integrated development environment. 統合開発環境 ↩︎

  10. Garbage Collection ↩︎

  11. Python くん…… ↩︎

  12. ただし、未踏の竹迫 PM も「筋の良い『車輪の再発明』は大歓迎です」と言及されているように、必ずしも悪いものではないと思います ↩︎

  13. ダニング・クルーガー効果 ↩︎

  14. マナー講師「三が日に Pull Request を投げるのは失礼に当たります。」 ↩︎

Discussion