いかにしてプログラムを書くか

ある友人からこんなことを言われた。

「すでにあるプログラムを変更して自分のほしいプログラムにはできるけど、イチからブログラムが書けない。何とかしてくれ」

何ともならんというのが正直な感想である。ついでに本を読めとも言った。自分で言うのもなんだが、至極正論だと思う。

しかしこの記事ではちょっと正論を避けてみようと思う。つまり、書きたいプログラムを書くための技法をなんとか分かりやすく書くのである。果たしてそんなことができるのか。

手法としては、自分が普段プログラムを書く際に考えていることを言語化することにする。これで先述の友人の手掛かりになるかどうかは分からないが、多少の助けになればと願う。

なお、プログラミング言語はJavaとする。また、Javaの基礎的な知識はすでにあるものと仮定する。不安な読者は、適当な参考書などに目を通してから読むと良いだろう。

お題

ともかく書きたいプログラムがないと始まらない。もちろんなんでも良いのだが、手頃なものとして次のような課題を考える。

あるファイル price.txt が与えられる。このファイルの中には、一行に一つ、整数値が書かれている。これを読み取り、それを二倍したものを画面に出力せよ。ただし出力に際しては、入力ファイルの中身と対応する行に答えを書くものとする。

問題文は少々ややこしいが、要するにこういうことだ。まず price.txt という名前のファイルがある。その中身は、例えば

42
334
443
7

のようになっている。このファイルを読み込んで、次のように出力すれば良い。

84
668
886
14

極めて単純な問題である。さてどのように解いたものか。

問題文を読む

まずは問題文を理解することから始める。とにかくファイルを読み込まなければならない。そしてそれを処理する。最後に画面に出力するのだ。つまり、3段階の手順から解答が構成されることが分かる。

このように問題を分割することは極めて重要である。プログラムは、様々な要素が有機的に集まることによって構成されるためである。

実装する

さて実装を始めよう。どこからプログラムを書き始めるかは人によって千差万別であろうが、ここでは分かりやすいところから手をつけることにする。

プログラミングにおいて最も分かりやすいのは、いわゆる「おまじない」の部分である。どのようなプログラムを書くに当たっても、必ず書かねばならない部分だ。Javaでは以下のようになる。

import java.util.*;
import java.io.*;

public class TwiceMain {
    public static void main(String[] args)
    {
    }
}

Javaの規則に従って、この内容のファイルをTwiceMain.javaという名前で保存し、コンパイル・実行を行うと正常に動くことが分かる。

Javaにおいて、プログラムが実行され始めるのはmainメソッドである。すなわち、この部分は必ず書かねばならない。ついでにいくつかimportを行っている。私は普段Javaを使わないのでよく知らないが、とりあえずこう書いておくと良いらしい。

ここで白状すれば、私は上のプログラムをコピペした。なぜなら覚えていないからである。覚える必要もないと思っている。別に今は試験を行っているわけではない。プログラムを書く上でコピペは極めて重要なテクニックである。Webなどからも積極的に行えば良いと思う ――ただし、意味を理解せずコピペするのは良いとは言えない。

さて、脳死で書ける部分が終わったところで、次に何をしようか。先程、プログラムを3つの部分に分けた。とりあえずこの構造を反映させよう。各々の段階を行う処理に一つずつメソッドを割り当てる。例えば次のようにする。

  1. ファイルを読み込む部分はreadFileメソッド。
  2. データを処理する部分はprocessDataメソッド。
  3. 画面に出力する部分はoutputDataメソッド。

これらのメソッドはmainの中で呼ばれる。これを上のプログラムに反映させる。

import java.util.*;
import java.io.*;

public class TwiceMain {
    public static void main(String[] args)
    {
        readFile();
        processData();
        outputData();
    }
}

ね、簡単でしょ?

もちろんこのプログラムはまだ動かない。readFile以下のメソッドはまったく実装されていないからである。しかし、いま自分が書こうとしているプログラムの大筋を理解するには重要なプロセスである。

さて順番に中身を作っていこう。まずはreadFileメソッドである。このメソッドではファイルを読み込む。より具体的には次のような処理をすることになる。

  1. 読み込みたいファイルを開く。
  2. ファイルを読み込むための「読み取り機」(スキャナ)を作る。
  3. ファイルからスキャナを使って整数値を順に読み込む。読み込んだ整数値は適当なコンテナに格納する。
  4. 格納したコンテナを返す。

このあたりのプロセスをどのように構成するかは、プログラミング言語によってもかなり異なる。いろいろな文献を調べながら書くことになるだろう。もちろん手近な参考書などで確認しても良いが、一つの手段として「リファレンス」というものがある。例えばJavaだと「java ArrayList」などで検索して上位にヒットするページがそれである。公式から出されている情報としてこれが一番正確であるから、積極的に活用したい。ただし慣れるまでは使いにくい上に、いわゆる「逆引き」的には使いにくい。例えば「とにかくファイルを開きたいんだけど……」のようなときには使いにくい。

そこで、以下のようにすることを推奨する。まずやりたいことをGoogleなどの検索窓に突っ込む。ここでは「java ファイル 開く」とでも入れればよいだろう。すると(メジャーな言語であれば)適当な解説記事がみつかるはずである。個人的にはQiitaなどの記事が質が高くて良いと感じているが、この際何でも良い。いくつかのサイトを巡回し、ファイルの扱い方を知ると同時に、ファイルを操作する際に関係するクラスやメソッドの名前を認識する。これらの名前でリファレンスを引くのである。一般に、解説記事では各々のメソッドについて詳しい解説(引数の型や戻り値、詳細なメソッドの挙動など)が与えられることは稀であるから、それらの情報をリファレンスで得ると考えれば良い。要するに「いいとこ取り」をするのである。

さて、その結果readFileメソッドを次のように書けば良いことが分かる。

import java.util.*;
import java.io.*;

public class TwiceMain {
    private static ArrayList<String> readFile()
    {
        try {
            // 1. ファイルを開く。
            File inputFile = new File("price.txt");
            // 2. スキャナを作る。
            Scanner sc = new Scanner(inputFile);
            // 3. 読み込んだ整数値を書くのするためのコンテナ(ArrayList)を作る。
            ArrayList<Integer> arrayList = new ArrayList<Integer>();
            // 3. ファイルからスキャナを使って整数値を順に読み込む。
            while (sc.hasNext()) arrayList.add(sc.nextInt());
            // 4. 格納したコンテナを返す。
            return arrayList;
        }
        // もしファイルが見つからなかった場合はここに飛んでくる。
        // 「例外」(Exception)というシステムによる。
        catch (FileNotFoundException ex) {
            // "File not found"というエラーメッセージを吐く。
            System.out.println("File not found");
            // プログラムを終了する。
            System.exit(0);
            return null;
        }
    }

    public static void main(String[] args)
    {
        readFile();
        processData();
        outputData();
    }
}

詳細なコメントを付けたため、先述のリファレンスなどを参照すれば何をしているかは分かると思う。上で構成したことをそのまま書いただけである。Java特有の規則(ArrayListintを入れられないために Integerを入れる、など)はあるものの、ある程度直感的なコードになっていると思う。

次へ進もう。とは言っても、あとは似たような作業を続けるだけである。 processDataメソッドを実装する。得られた整数値を二倍して返すメソッドである。以下のようになる。

import java.util.*;
import java.io.*;

public class TwiceMain {
    private static ArrayList<Integer> readFile()
    {
        try {
            // 1. ファイルを開く。
            File inputFile = new File("price.txt");
            // 2. スキャナを作る。
            Scanner sc = new Scanner(inputFile);
            // 3. 読み込んだ整数値を書くのするためのコンテナ(ArrayList)を作る。
            ArrayList<Integer> arrayList = new ArrayList<Integer>();
            // 3. ファイルからスキャナを使って整数値を順に読み込む。
            while (sc.hasNext()) arrayList.add(sc.nextInt());
            // 4. 格納したコンテナを返す。
            return arrayList;
        }
        // もしファイルが見つからなかった場合はここに飛んでくる。
        // 「例外」(Exception)というシステムによる。
        catch (FileNotFoundException ex) {
            // "File not found"というエラーメッセージを吐く。
            System.out.println("File not found");
            // プログラムを終了する。
            System.exit(0);
            return null;
        }
    }

    private static void processData(ArrayList<Integer> arrayList)
    {
        // arrayListの全ての要素について繰り返す。
        for (int i = 0; i < arrayList.size(); i++) {
            // arrayListの当該要素を取得する。
            Integer src = arrayList.get(i);
            // 二倍したものを当該要素の代わりに突っ込む。
            arrayList.set(i, src * 2);
        }
    }

    public static void main(String[] args)
    {
        ArrayList<Integer> data = readFile();
        processData(data);
        outputData();
    }
}

processDataメソッドが実装されたのと同時に、mainメソッドも変更されていることに着目してほしい。 readFileメソッドから得られたデータをprocessDataメソッドに渡す必要があるためである。始めにmainメソッド内に書いたものは単にイメージを固定するためのものであるから、プログラムを具体的に書いていくなかで変更されることももちろんありえる。臨機応変に対処する。

最後にoutputDataメソッドを実装する。次のようになる。

import java.util.*;
import java.io.*;

public class TwiceMain {
    private static ArrayList<Integer> readFile()
    {
        try {
            // 1. ファイルを開く。
            File inputFile = new File("price.txt");
            // 2. スキャナを作る。
            Scanner sc = new Scanner(inputFile);
            // 3. 読み込んだ整数値を書くのするためのコンテナ(ArrayList)を作る。
            ArrayList<Integer> arrayList = new ArrayList<Integer>();
            // 3. ファイルからスキャナを使って整数値を順に読み込む。
            while (sc.hasNext()) arrayList.add(sc.nextInt());
            // 4. 格納したコンテナを返す。
            return arrayList;
        }
        // もしファイルが見つからなかった場合はここに飛んでくる。
        // 「例外」(Exception)というシステムによる。
        catch (FileNotFoundException ex) {
            // "File not found"というエラーメッセージを吐く。
            System.out.println("File not found");
            // プログラムを終了する。
            System.exit(0);
            return null;
        }
    }

    private static void processData(ArrayList<Integer> arrayList)
    {
        // arrayListの全ての要素について繰り返す。
        for (int i = 0; i < arrayList.size(); i++) {
            // arrayListの当該要素を取得する。
            Integer src = arrayList.get(i);
            // 二倍したものを当該要素の代わりに突っ込む。
            arrayList.set(i, src * 2);
        }
    }

    private static void outputData(ArrayList<Integer> arrayList)
    {
        // arrayListの全ての要素について繰り返す。
        for (int i = 0; i < arrayList.size(); i++) {
            // arrayListの当該要素を取得する。
            Integer src = arrayList.get(i);
            // 当該要素を出力し改行する。
            System.out.println(src);
        }
    }

    public static void main(String[] args)
    {
        ArrayList<Integer> data = readFile();
        processData(data);
        outputData(data);
    }
}

これで完成である。ね、簡単でしょ?

完成させて

一見簡単なお題を完成させるにも、そこそこの思考と、そこそこのコード量が要求される。イチからプログラムを作るにはある程度仕方のないことである。

競技プログラミングのようなアルゴリズムとして難しいものを除けば、プログラミング自体は決して難解な営みではない。出来る・出来ないは慣れによるところが大きいだろう。結局「やったもんがち」である。楽しみながら長時間やるのが上達への近道ではないだろうか。

終わりに

上のソースコードはMIT Licenseとします。それ以外の文書はCC0 1.0です。

質問がある場合はTwitter(@ushitora_anqou)か Mastodon(@[email protected])まで。より手軽にはこの記事のコメント欄でも結構です。暇なときに返信します。

コメント

  1. torikori - ひろみみち より:

    Python で解いてみた. Java 言語の厳格さがかえって私のような初心者には難しいのかもしれない.
    with open(“price.txt”) as f:
    print(“\n”.join(map(lambda x: str(2*int(x)), f)))

  2. torikori - ひろみみち より:

    s/かえって私のような初心者には難しいのかもしれない/かえって難しくさせているのかもしれない. 少なくとも初心者の私にはそう感じさせられた./

    • 艮 鮟鱇 より:

      ひろみさんが初心者かどうかはさておき、Javaに対する考察に関してはそのとおりだと思います。そういう意味で、Javaは初心者向きではないとも考えられます。