LoginSignup
3
1

More than 5 years have passed since last update.

足し算プログラムをいくつかの言語で作ってみる

Last updated at Posted at 2018-11-11

はじめに

 当初は、オブジェクト指向プログラミングと、その特徴についてまとめていたのですが、どうもうまく出来なくて、断念してしまいました。

 それで、その代わりに簡単な課題をつくって、それを具体的にプログラミングして、比較していくのが良いかな、と思って、まずは簡単に足し算プログラムから始めようと思ったことがきっかけです。

 言語としては、手続き指向型言語としてPascal (標準Pascal)、オブジェクト指向型言語として Java、関数型言語として Haskell を選びました。

 この言語チョイスは完全に趣味です。うまく言えないのですが、シングルパラダイムで、静的な型を持つ言語で、シンタックスが長くなりがちで、ちょっとこう「いかつい」感じのする言語なところが好きです。

 足し算といっても、ただ一行で計算するのではなく、ユーザーからの入力を受け付ける感じで、次のように機能を定義します。

1. 最初に「数を入力して下さい。空行で終了です。」と表示する
2. 「>」を表示して入力待ちになる。数字を入れて改行すると、それが記録される
3. 再度「>」を表示して入力待ちになる。何も文字を入れずに、改行するまで繰り返す。
4. 空改行されたら、それまでに入力されたすべての数の合計を足し算した結果を表示する。「合計はxxxです」と表示する。
5. なお、数字以外の文字を含んだ入力は無視する

本当はもうちょっと複雑なサンプルの方が、各言語の個性が出てくるとは思うのですが、まずは第一歩ということで。

実際の動作イメージはこんな感じです。

数を入力して下さい。空行で終了です。
>10
>20
>4
>
合計は34です。

この記事は、何か一つプログラミング言語を学んだことがあり、条件分岐やループ処理などが理解出来るくらいの初学者が読むことを想定しています。

Pascalでの実装

ここでは標準Pascalで考えてみます。標準Pascalは機能も少なく、また、分割コンパイルも出来ないのですが、プログラミングの学習、特にアルゴリズムの学習の基礎を学ぶのには、現在でも適しているように思います。

まず、メインルーチンを次のように実装することを考えます。


program CalcSumApp;

type
  ItemPtr = ^Item;
  Item = 
    record
      next: ItemPtr;
      text: ShortString;
    end;

// 各種関数の定義

var
  lines : ItemPtr;
  sum : Integer;

begin
  writeln('数を入力して下さい。空行で終了です。');
  lines := readLines();
  sum := sumOfLines(lines);
  writeln('合計は' + toStr(sum) + 'です。');
  disposeAll(lines);
end.

最初に、ItemPtrというポインタを定義しています。実は、標準Pascal(ISO 7185 Pascal)では、可変長の配列を利用することが出来ないので、ここではこのようにItemという数値を一つだけ格納出来るNodeを定義し、そのポインタという形で入力された数値を格納するようにしました。

メインルーチン(begin以下の部分)は、次のようになります。

1行目でメッセージを表示し、2行目で数値を(複数行)受け付けます。
そして、3行目で、入力した数値の合計を計算し、4行目で結果を表示しています。toStrは、整数を文字列に変換する関数ですが、これも標準pascalにはないので、プログラム内で実装しています。
5行目は、メモリの解放です。ポインタは終わった段階で自分で解放しなくてはいけません。

さて、次は入力部分のreadLinesの実装を考えてみます。


function readLines(): ItemPtr;
var 
  result: ItemPtr;
  prevItemPtr: ItemPtr;
  currItemPtr: ItemPtr;
  line: ShortString;
begin
  result := nil;
  prevItemPtr := nil;
  while true do
  begin
    write('>');
    readln(line);
    if line <> '' then 
    begin
      if prevItemPtr = nil then
      begin
        new(prevItemPtr);
        prevItemPtr^.text := line;
        prevItemPtr^.next := nil;
        result := prevItemPtr;
      end
      else 
      begin
        new(currItemPtr);
        currItemPtr^.text := line;
        currItemPtr^.next := nil;
        prevItemPtr^.next := currItemPtr;
        prevItemPtr := currItemPtr;
      end;
    end   
    else
      break;
  end;
  readLines := result;
end;

全体をwhileループでくくり、条件(空改行入力)されるまで繰り返す形式にします。

readln(line)が、文字入力を受け取っている部分です。

初回だけは特別で、入力した値をprevItemPtrに保持します。new(prevItemPtr)は、ヒープメモリに領域を確保する命令で、入力された数値をそこに格納し、結果(result)に保存します。
2回目以後は、入力した値をcurrItemPtrに保持します。そして、入力された数値を格納し、それ一つ前のprevItemPtrから参照させます。最後に、currItemPtrを新しいprevItemPtrとして更新します。

次は、足し算を計算しているsumOfLines関数です。

function sumOfLines(lines: ItemPtr): Integer;
var
  a : Integer;
  v : Integer;
  err: Boolean;
begin
  a := 0;
  while lines <> nil do
  begin
    err := false;
    v := toInt(lines^.text, err);
    if not err then
      a := a + v;
    lines := lines^.next;
  end;
  sumOfLines := a;
end;

これは、linesを一行一行確認しながら、文字列を整数に変換し、足し算しています。文字列を整数に変換するtoInt関数も、標準Pascalにはないので、プログラム内で定義しています。また、数値以外の文字列が来た時は、エラーにならないよう、第二引数のerrで判定するようにしています。(そのため、この第二引数は、参照呼び出しです)

プログラム全体では、次のようになります。

program CalcSumApp;

type
  ItemPtr = ^Item;
  Item = 
    record
      next: ItemPtr;
      text: ShortString;
    end;

function toInt(str: ShortString; var err: Boolean): Integer;
var
  i : Integer;
  n : Integer;
  r : Integer;
begin
  r := 0;
  for i := 1 to ord(str[0]) do
  begin
    n := Ord(str[i]);
    if (n < Ord('0')) or (n > Ord('9')) then 
      begin
        err := true;
        break;
      end;
    r := r * 10 + n - Ord('0');
  end;
  toInt := r;
end;

function toStr(n: Integer): ShortString;
var
  m : Integer;
  s : ShortString;
begin
  s := '';
  while n > 0 do
  begin
    m := n mod 10;
    n := n div 10;
    s := chr(m + ord('0')) + s;
  end;
  if s = '' then
    s := '0';
  toStr := s;
end;

procedure disposeAll(ptr: ItemPtr);
begin
  if ptr <> nil then
  begin
    disposeAll(ptr^.next);
  end;
end;

function readLines(): ItemPtr;
var 
  result: ItemPtr;
  prevItemPtr: ItemPtr;
  currItemPtr: ItemPtr;
  line: ShortString;
begin
  result := nil;
  prevItemPtr := nil;
  while true do
  begin
    write('>');
    readln(line);
    if line <> '' then 
    begin
      if prevItemPtr = nil then
      begin
        new(prevItemPtr);
        prevItemPtr^.text := line;
        prevItemPtr^.next := nil;
        result := prevItemPtr;
      end
      else 
      begin
        new(currItemPtr);
        currItemPtr^.text := line;
        currItemPtr^.next := nil;
        prevItemPtr^.next := currItemPtr;
        prevItemPtr := currItemPtr;
      end;
    end   
    else
      break;
  end;
  readLines := result;
end;

function sumOfLines(lines: ItemPtr): Integer;
var
  a : Integer;
  v : Integer;
  err: Boolean;
begin
  a := 0;
  while lines <> nil do
  begin
    err := false;
    v := toInt(lines^.text, err);
    if not err then
      a := a + v;
    lines := lines^.next;
  end;
  sumOfLines := a;
end;

var
  lines : ItemPtr;
  sum : Integer;

begin
  writeln('数を入力して下さい。空行で終了です。');
  lines := readLines();
  sum := sumOfLines(lines);
  writeln('合計は' + toStr(sum) + 'です。');
  disposeAll(lines);
end.

Javaでの実装

Javaでは、二つのクラス、CalcSumAppクラスとCalcSumクラスで実装してみます。まずは、アプリケーション全体のCalcSumAppクラスです。


public class CalcSumApp {
  public static void main(String args[]) {
    CalcSum cs = new CalcSum();
    CalcSumApp app = new CalcSumApp();
    app.start(cs);
  } 

  public void start(CalcSum cs) {
    System.out.println("数を入力して下さい。空行で終了です。");
    cs.readLines();
    int sum = cs.getSum();
    System.out.println("合計は" + String.valueOf(sum) + "です。");
  }
}

main関数内で、CalcSumAppクラスとcalcSumクラスのインスタンスを作成して、 startメソッドを呼んでいます。

startメソッドの内容は、Pascalのメインルーチンとほぼ同じことをしていますが、直接データを扱うことはせず、CalcSumクラスのオブジェクトを操作しているだけです。

次に、CalcSumクラスは次のようになります。

class CalcSum {
  List<String> list;

  CalcSum () {
    list = new ArrayList<String>();
  }

  public void readLines() {
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    String line;
    try {
      do {
        System.out.print('>');
        line = input.readLine();
        if ("".equals(line)) {
          break;
        }
        list.add(line);
      } while (true);
    } catch (IOException e) {

    }
  }

  public int getSum() { 
    int sum = 0;
    for (String s : list) {
      try {
        sum += Integer.valueOf(s).intValue();
      } catch (NumberFormatException e) {

      }
    }
    return sum;
  }
}

 コンソールから入力を受け付けるreadLinesメソッドと、総和を計算するgetSumメソッドが定義されています。中身は、pascalのときのreadLines関数、sumOfLines関数とほぼおなじです。ただし、入力された数値データ(list)は、CalcSumクラスのインスタンス変数として保持しています。

 プログラム全体では、次のようになります。Pascalと比べると、toIntやtoStrのような変換処理や、メモリ解放のdisposeAllといった処理がないだけすっきりしていますし、また、listがCalcSumの中に収まっているのがいい感じです。(private にした方がもっと良いかもしれませんが)

import java.util.Scanner;
import java.util.List;
import java.util.ArrayList;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.NumberFormatException;

class CalcSum {
  List<String> list;

  CalcSum () {
    list = new ArrayList<String>();
  }

  public void readLines() {
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    String line;
    try {
      do {
        System.out.print('>');
        line = input.readLine();
        if ("".equals(line)) {
          break;
        }
        list.add(line);
      } while (true);
    } catch (IOException e) {

    }
  }

  public int getSum() {
    int sum = 0;
    for (String s : list) {
      try {
        sum += Integer.valueOf(s).intValue();
      } catch (NumberFormatException e) {

      }
    }
    return sum;
  }
}

public class CalcSumApp {
  public static void main(String args[]) {
    CalcSum cs = new CalcSum();
    CalcSumApp app = new CalcSumApp();
    app.start(cs);
  } 

  public void start(CalcSum cs) {
    System.out.println("数を入力して下さい。空行で終了です。");
    cs.readLines();
    int sum = cs.getSum();
    System.out.println("合計は" + String.valueOf(sum) + "です。");
  }
}

この例では簡単なので、CalcSumを不変オブジェクトにする実装も出来ると思うのですが、一般にオブジェクト指向プログラミングでは、不変に出来るオブジェクトは限られていると思うので、不変オブジェクト化した実装については触れませんでした。

Haskellでの実装

Haskellでも、メインルーチンはだいたい同じような感じで、次のようになりました。

  main :: IO ()
  main = do
    hSetBuffering stdout NoBuffering
    putStrLn "数を入力して下さい。空行で終了です。"
    lines <- readLines getLines
    list <- sequence lines
    let s = sum list
    putStrLn $ "合計は" ++ (show s) ++ "です。"

hSetBuffering stdout NoBufferingの部分は、この記事のコメントでご指摘いただいたところで、これを入れないと、バッファリングにより入力と出力が期待した順序になることが保証されないということでした。( @igrep さん、ありがとうございます)

getLinesは、文字列を無限に受け取るリストで、次のように定義しています。


  getLines :: [IO String]
  getLines = do
    repeat $ putStr ">" >> getLine

repeatは、無限リストを作り出します。つまり、

repeat x = [x x x x .... x ... ]

という感じです。
このような無限リストが扱えるのはHakellの遅延処理の特徴で、これを使ってみたくこのようにしてみました。

readLinesは、空行が入力されるまでの部分を切り出す処理で、次のようになります。


  readLines :: [IO String] -> IO [IO Int]
  readLines [] = return []
  readLines (l:ls) = do
    v <- l :: IO String
    if v == ""
      then return []
    else do
      ll <- readLines ls
      case readMaybe v of 
        Just x -> return $ (return x):ll
        Nothing -> return ll

 配列の第一要素が空文字なら、それで処理を終了。そうでなければ、
第一要素を数値に変換(readMaybe)して、変換できれば、それに残りを変換したもの(再帰処理させたもの)を足して返し、変換出来なければ、残りを変換したもの(再帰処理させたもの)だけを返しています。

  list <- sequence lines 

ここはちょっと分かりにくですが、IOの配列を、配列のIOに変換して、その中身を取り出しています。つまりlistは、ただの数値の配列になります。

ここは元々は

list <- mapM (>>= return) lines

としてたのですが、コメントで指摘されたように、squenceで一発で解決出来るので、変更しました。

そして、合計を計算するのは、次で行っています。

   let s = sum list

(正格評価の話は、私の理解が間違っていたので割愛します)

 プログラム全体では次のようになります。モナドの変換がちょっと面倒なのと、Javaと違って変数linesを直接扱っていますが、do構文を使うことでpascalっぽい感じで記述することが出来ています。

module CalcSumApp where

  import Text.Read
  import System.IO

  getLines :: [IO String]
  getLines = do
    repeat $ putStr ">" >> getLine

  readLines :: [IO String] -> IO [IO Int]
  readLines [] = return []
  readLines (l:ls) = do
    v <- l :: IO String
    if v == ""
      then return []
    else do
      ll <- readLines ls
      case readMaybe v of 
        Just x -> return $ (return x):ll
        Nothing -> return ll

  main :: IO ()
  main = do
    hSetBuffering stdout NoBuffering
    putStrLn "数を入力して下さい。空行で終了です。"
    lines <- readLines getLines
    list <- sequence lines
    let s = sum list
    putStrLn $ "合計は" ++ (show s) ++ "です。"

おまけ Haskell その2

Haskellでも、オブジェクト指向っぽく、データタイプを作ってカプセル化(っぽく)してみました。実用的ではないと思いますが、、。

メインルーチンは


  main :: IO ()
  main = do
    hSetBuffering stdout NoBuffering
    putStrLn "数を入力して下さい。空行で終了です。"
    calcSum <- newCalcSum   -- calcSum = new CalcSum()
    inputStrings calcSum    -- calcSum.inputString()
    sum <- getSum calcSum   -- sum = calcSum.getSum()
    putStrLn $ "合計は" ++ (show sum) ++ "です。"

こうなっていて、Javaに近くなっていると思います。
コード全体では、


module CalcSumApp2 where
  import Text.Read
  import Control.Monad
  import Data.IORef
  import Data.Maybe
  import System.IO

  data CalcSum = CalcSum { getStrings :: IORef [String] }

  newCalcSum :: IO CalcSum
  newCalcSum = do
    ref <- newIORef []
    return $ CalcSum ref

  inputStrings :: CalcSum -> IO ()
  inputStrings calcSum = do
    let truncateStrings :: [IO String] -> IO [String]
        truncateStrings [] = return []
        truncateStrings (x:xs) = do
        s <- x
        if s == "" 
          then 
            return []
          else do
            l <- truncateStrings xs
            return $ (:l) $! s            -- strict evaluation of s corresponding to `return (s:l)`

    list <- truncateStrings $ repeat $ putStr ">" >> getLine
    writeIORef (getStrings calcSum) list  -- calcSums.strings = list

  getSum :: CalcSum -> IO Int
  getSum calcSum = do
    list <- readIORef (getStrings calcSum) :: IO [String] -- list = calcSum.strings 
    let nums = catMaybes $ readMaybe <$> list :: [Int] 
    return $ sum nums

  main :: IO ()
  main = do
    hSetBuffering stdout NoBuffering
    putStrLn "数を入力して下さい。空行で終了です。"
    calcSum <- newCalcSum   -- calcSum = new CalcSum()
    inputStrings calcSum    -- calcSum.inputString()
    sum <- getSum calcSum   -- sum = calcSum.getSum()
    putStrLn $ "合計は" ++ (show sum) ++ "です。"

ここで言いたかったのは、オブジェクト(的なもの)をIORefにしたことです。そもそもオブジェクトはIOとは関係ないものだと思うのですが、自分の中ではオブジェクトは、アプリケーションとは別に、ヒープメモリに領域を確保して読み書きするもの(DBみたいなイメージ)なので、IORefにするとしっくり来るように思いました。

オブジェクト指向言語のオブジェクトは、IORef的なもの(共有されて、変更可能なもの)以外に、共有されないオブジェクトや、不変オブジェクト、不変でかつ副作用もない(IO入出力がないなど)オブジェクト、副作用の大きなオブジェクト(外部サーバーとやりとりする処理をラップしたオブジェクトとか)など、いろいろなものがあると思うのですが、そのあたりをうまくまとめた考察が出来なかったので、ここではこのサンプルを提示するだけにとどめておきます。

感想

標準Pascalは機能が少なくて、toInttoStr を自分で実装しなくてはいけなかったり、あるいは、メモリの解放を自分でやる必要があるのですが、こうした(面倒な)処理を書いていると、「あ〜、プログラミングしているなあ」という気持ちになれるのが好きです。
 なんていうか、オートマ車じゃなくてマニュアル車をいじっている感じがするというか。最近では、Goとかがこういう感じなんですかね。

 Javaは、いろいろ批判されることはあるものの、やっぱり使いやすくて良い言語だと思います。オブジェクト指向プログラミング言語は、Rubyのようにメタプログラミング出来たり、JavaScriptのように動的にオブジェクトを作れたりする方が向いているような気もしますが、でも、静的にクラスが決まる安心感はいいですね。自分はJava 1.5くらいが好きで、あまり他のパラダイムが組み込まれて複雑になってしまうのは残念です。

Haskellは、まだまだ勉強することがいっぱいで、苦戦していますが、動作した時はすごく嬉しいです。関数がたくさんあって理解するのも覚えるのも大変なのですが、使い所が分かるとおおっという感じがするのと、静的に解決している感じがよいですね。型に注意するようになるのも良い気がします。

おわりに

この簡単な例では、どの実装でもあまり変わらないのですが、それでも各言語の特徴はいくつか出ているのではないかな、と思いました。

 最近のプログラミング言語は、マルチパラダイムなものが増えていますが、だからこそ、シングルパラダイムの言語の基本を学ぶのもの大事だと思っています。

間違った説明や、不十分な点など、ありましたら、ぜひともコメントお願いします。

 

3
1
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1