時代遅れひとりFizzBuzz祭り Fortran 90〜95編(Fortranの配列は便利ですね)

時代遅れひとりFizzBuzz祭り、今回はFortran。前回のCOBOLと並んで「世界3大『古典だけど仕様拡張されつつ現在まで生き残ってきた』プログラミング言語」の1つである*1

ちなみに「世界3大『古典だけど(ry」はCOBOLFortranLispのことだ。このうち「Lisp」がどこからどこまでを指すかについては色々と揉めるところだが、他の2つと同様に標準規格が存在するCommon Lispあたりが適切――なんて風に門外漢は考えるのだが、実際のところどうなんだろうか?*2 ちなみにC言語はまだ若いので対象外。

脱線した。COBOLと同様にFortranも縁が無い言語だ。科学技術計算の分野、特に大量のデータを扱い且つ高速に計算しなくてはならない所では現役だという話を聞くけど、そもそもそんな分野に関わることがない。

とはいえFortranで書かれたプログラムの成果とは縁があって――気象予報の数値モデルはFortranで構築されているはずで、私は天気予報を頼りにしている。私に限らず多くの人々が天気予報に行動を左右されるし、今では人に限らず企業まで天気予報に左右されている面がある。

そう、実は日本はCOBOLFortranによる二極体制にあるのだ! そしてARM(組込み機器)とUnixサーバの普及*3に伴いC言語が台頭し*4、21世紀は混迷の世紀となるのだった :)

今からFortranでコードを書くとしたら、特にしがらみがないなら少なくともFortran 90ないし95をベースとした方が良いらしい。FORTRAN 77は古すぎる。Fortran 2003以降は処理系の対応度が問題なようだ*5。ということでG95 4.1.2を使ってF90〜95相当で書いてみた。

program fizzbuzz
  implicit none
  integer i

  do i = 1, 100
    if (mod(i, 15) == 0) then
      print '(A)', 'FizzBuzz'
    else if (mod(i, 3) == 0) then
      print '(A)', 'Fizz'
    else if (mod(i, 5) == 0) then
      print '(A)', 'Buzz'
    else
      print '(I0)', i
    end if
  end do
end program fizzbuzz

「implicit none」で暗黙の整数/定数の宣言を避けるのは、いまどきのFortranプログラミングでは必須らしい。暗黙の宣言の怖さはawkや「Option Explicit」の無いVBScriptで身に染みているので、納得できる話だ。

他言語メインの人としては剰余を演算子でなく関数modで求める所やprintの書式指定などが気になるポイントだ。ここではprintの書式指定を明示的に書くようにしている。

Fortranは仕様改定の度に近代化していて、例えばFortran 90以降にはC言語にはない内部副プログラムやモジュール機能が含まれている*6。内部副プログラムを使うと定義したサブルーチンや関数を呼び出せる範囲を限定しやすいので、Cプログラマとしては少し羨ましい。

program fizzbuzz
  implicit none
  integer i

  do i = 1, 100
    call pfizbuz(i)
  end do
contains
  subroutine pfizbuz(n)
    integer, intent(in) :: n

    character(4), parameter :: msg(2) = (/ 'Fizz', 'Buzz' /)
    integer fiz, buz

    fiz = int_from_logic(mod(n, 3) == 0)
    buz = int_from_logic(mod(n, 5) == 0) * 2

    select case (fiz + buz)
      case (0)
        print '(I0)', n
      case (1:2)
        print '(A)', msg(fiz + buz)
      case (3)
        print '(A)', msg(fiz) // msg(fiz)
    end select
  end subroutine pfizbuz

  ! convert from logical to integer 0 or 1 (programming language C)
  integer function int_from_logic(t)
    logical, intent(in) :: t

    if (t .eqv. .true.) then
      int_from_logic = 1
    else
      int_from_logic = 0
    end if
  end function int_from_logic
end program fizzbuzz

intentで引数の特性を指定できるとか、case構文で下限や上限を指定できるとか、文字列の連結ができるとか、意外と便利な機能が多い。ただ使いどころが限られるというか、嵌ったときは便利だけどそうでない場合はそれ程でもない機能が多い気がしないでもない(intentは別だけど……)。

配列の添字が(デフォルトでは)1から始まることや論理型が整数型と独立して存在する所は、Cプログラマとしては要注意事項だろう。

関数は配列や文字列を返すことができる。固定サイズだけでなく任意の大きさの配列/文字列も返せるが、宣言部でサイズを指定する必要があるので、例えば「引数に取った文字列と同じサイズで返す」ならOKだが「文字列をあれこれ組み立てた結果によってサイズが変動する」といったパターンでは使えない。

program fizzbuzz
  implicit none
  integer i

  do i = 1, 100
    print '(A)', trim(fizbuzstr(i))
  end do
contains
  function fizbuzstr(n)
    integer, intent(in) :: n
    character(8) fizbuzstr

    character(8) str
    integer idx

    str = ''
    if (mod(n, 3) == 0) then
      str = 'Fizz'
    end if
    if (mod(n, 5) == 0) then
      idx = len_trim(str) + 1
      str(idx:idx+4) = 'Buzz'
    end if
    if (len_trim(str) == 0) then
      write (str, '(I0)') n
    end if

    fizbuzstr = str
  end function fizbuzstr
end program fizzbuzz

ここでは固定サイズの文字列を返すようにしている。文字列 'Buzz' の連結が少し面倒なのだが、部分文字列を指定して上書きしている。

Fortranでデータ構造といったら配列だ。もちろんポインタを使ってリスト構造等を作ることもできるだろうけど、Fortranの配列はこんなことが簡単にできて便利だ。

  • 配列構成DO形反復で一気に初期化して数列を作成する。
  • 配列全体に代入や算術演算を適用する。
  • 配列同士で演算する。
  • 部分配列にアクセスする。

なので例えばこんなFizzBuzzも書ける。

program fizzbuzz
  implicit none
  integer i
  integer :: tmp(100) = (/ (i, i = 1, 100) /)

  tmp( 3:size(tmp): 3) = -1
  tmp( 5:size(tmp): 5) = -2
  tmp(15:size(tmp):15) = -3

  do i = 1, size(tmp)
    call pfizbuz(tmp(i))
  end do
contains
  subroutine pfizbuz(n)
    integer, intent(in) :: n

    select case (n)
      case (-1)
        print '(A)', 'Fizz'
      case (-2)
        print '(A)', 'Buzz'
      case (-3)
        print '(A)', 'FizzBuzz'
      case default
        print '(I0)', n
    end select
  end subroutine pfizbuz
end program fizzbuzz

1〜100を格納した数列の配列tmpを作成し、3・5・15の倍数が格納されているだろうインデックスにフラグ値代わりの値を一括代入している。あとは配列を舐めて出力用のサブルーチンを呼ぶだけ。

配列の使いやすさに関しては「さすが数値演算(行列演算)向け」といった所だろうか。

Fortranには他にも複素数や構造型、ポインタ、オプショナル引数といった気になる機能があるし、Fortran 2003以降ともなればオブジェクト指向プログラミング対応に始まるより多くの拡張や機能追加がなされている。追加されすぎて処理系の対応度が問題になるぐらいだ。

そこまで行かなくても、Fortran 90や95は結構近代的だと思う。少なくとも構造化プログラミングには十分だし、モジュール化絡みの言語機能はC言語よりも上だと思う。

ただ、幾ら近代的とはいえども「formula translation」という名前が示すように数値計算、科学技術計算が得意な言語な訳で、その目的外のことにFortranを使うとFortranの良さが薄れてしまうように思う。

例えばcase構文の下限・上限指定や配列演算の機能は扱うデータが数値なら非常に便利だが、数値以外のデータを扱う場合は威力が半減する。それに例えば配列の各要素に算術演算や組み込み関数を適用する場合は1発で書けるが、自分が定義したサブルーチンや関数を自動的に適用することはできない*7

そんな訳で、Fortranで何かしら(自分にとって)有用なツールが作れたらいいなと思っていたのだけど、今の所は数値計算絡みのツールは不要なので、暫くはFortranを使う機会は無さそうだ。

ところで少し気になったのだが、ループがforではなくdoなのはIBMの伝統なのだろうか? REXXのループもdoだった。他のIBM発祥の言語がどうなのか知らないので間違っているかもしれないけど。

*1:発案者は私。

*2:それともISLispか? 何しろCommon LispANSI規格なのに対してISLispはISOだから :)

*3:最近は誰も彼もクラウド大好きだし。

*4:FreeBSDLinuxは大部分がC言語で書かれている……え、いや確かにスクリプトの部分も多いけど。サーバアプリもC言語ベースのものが多いような。

*5:GFortranのサイトに「Fortran 2003〜2008の相当数の機能が実装されている」と書かれているが、裏を返せば未実装の機能があるということになる。

*6:正確に書くと、C言語は明示的な言語機能としてそれらの機能を持っていない。但しstaticな関数や変数を用いてファイル分割すれば似たようなことはできる。

*7:配列を引数に取るサブルーチンないし関数を自前で定義した場合は別として。