当記事では、自作したじゃんけんゲームを題材にしてFortranの文法や使用するときの注意点などを確認していきます。Windows 10 Home 64bit版環境で検証しています。
ソースコードは以下よりご確認ください。
rps-like/Fortran at master · tomomoss/rps-like
じゃんけんゲームとは
じゃんけんゲームはコンソール上で動作するCUIゲームです。
基本的なルールは既存のじゃんけんそのものです。じゃんけんで勝つごとに1点取得し、対戦相手よりも先に5点先取することで勝者となります。
対戦内容はログファイルに書き出して、後から読み返せるようにします。
Fortranとは
Fortranは 世界初の高級言語 です。1954年に開発が始まり、3年後の1957年にコンパイラが完成しました。同時期に誕生したプログラミング言語としてはALGOL(1958年)・LISP(1958年)・COBOL(1959年)・PL/I(1964年)などがあります。なお、時折「化石」などと形容されるC言語ですら1972年に作られたことを考えると、その古さがよく分かるかと思います。
60年以上も前に作られたプログラミング言語ではありますが、今日に至るまでに何度もバージョンアップが続けられてきました。なかでも Fortran 77からFortran 90へのバージョンアップが分水嶺 となっています。このバージョンアップで、ソースコードの書き方が従来の固定形式から自由形式へと大胆に改良されました。最新版となるのはFortran 2018であり、当記事に掲載しているコードもFortran 2018に準拠しています。
現在では主要なプログラミング言語とは言えなくなりましたが、それでもシミュレーター業界・大学および研究機関・スーパーコンピューターなどの分野ではいまだに現役で活躍しているらしいです。これはFortranが数値計算に適した言語として設計されたことが関係しているように思われます。
文法や仕様に目を向けると、さすがは世界初の高級言語というだけあってか、後発の言語ではみられない独特な機能や記法が散見されます。とはいえ、やっていること自体は他言語と変わらない――いや、むしろ最近の言語よりもできることが少ないのでFortran特有の要素にさえ慣れてしまえば学習や習得は容易なほうでしょう。
ただ、書きやすい言語であるとは到底言えませんし、他のプログラミング言語と比べると需要もかなり小さいと推測します。そのため、今からわざわざFortranを学ぶ価値があるかは怪しいところです。
開発環境構築手順
Fortranでプログラムを組むにはコンパイラが必要です。Fortranコンパイラは無料のものから有料のものまで色々あるようですが、今回のじゃんけんゲーム開発にあたっては「GFortran(あるいはGNU Fortran)」という無料のコンパイラを利用しました。
GFortranは「MinGW(Minimalist GNU for Windows)」というアプリを使って導入します。どうも、MinGWとは色々なプログラミング言語の開発環境を構築するためのアプリのようです。元々はUNIXやLinuxで使われていたプログラム開発ツール群「GNUツールチェーン」をWindowsでも使えるように移植した「Cygwin」というアプリから派生したのがMinGWであるようです。
仔細はともかく、ここからはMinGWを使ってFortranの開発環境を構築する手順を説明します。なお、以下手順はWindows 10 Home 64bit版でのみ動作が確認できている手順となります。
MinGWのインストーラーをダウンロード
まずはMinGWのインストーラーをダウンロードしましょう。
MinGWのサイトから、インストーラーのダウンロードページに行きます。
Download File List - MinGW - Minimalist GNU for Windows - OSDN
ダウンロードページのページ下部に「Download Package list」という見出しとともに、10個近くのリンクが貼ってあることが確認できるかと思います。これらがMinGWに関連するパッケージ群となります。このなかにMinGWのインストーラーがあるのですが少し深い階層にあるので、ここは手っ取り早くダウンロードする手順を採ります。
先の「Download Package list」という見出しのすぐ上に、Windowsのロゴと「mingw-get-setup」が書かれた水色のボタンがあるかと思います。これをクリックしてMinGWのインストーラーをダウンロードしてください。
MinGWをインストール
MinGWのインストーラーがダウンロードできましたら、インストーラーを起動してMinGWをインストールしてください。
まずはインストーラーのウィンドウ下部にある「Install」をクリックしてください。
次の画面ではMinGWをインストールするパスを聞かれますので任意の場所を指定してください。指定後、ウィンドウ下部の「Continue」をクリックしてください。
MinGWに必要なファイルのダウンロードとインストールが始まりますので待ちます。平均的な速度の有線ならば1分もかからずに終わると思います。処理が終わりましたら、ウィンドウ下部の「Continue」をクリックしてください。インストーラーを閉じ、MinGWが起動します。
なお、インストール場所は標準でC:\MinGW\libexec\mingw-get\guimain.exeです。
Fortranの開発環境を準備
MinGWを介してFortranの開発環境を準備します。
まずはウィンドウ上部メニューから「Installation」を選び、そのなかの「Update Catalogue」を選択します。これでMinGWを介して準備できる開発環境の一覧が最新のものになります。
更新が終わりましたら、ウィンドウ左部にある項目一覧から「Basic Setup」を選択し、ウィンドウ上部にある項目一覧から「mingw32-gcc-fortran-bin」を右クリックし、右クリックメニューのなかから「Mark forInstallation」を選択してください。「mingw32-gcc-fortran-bin」の左にあるチェックボックスに矢印記号が付いたことを確認してください。
その後、ウィンドウ上部メニューの「Installation」から「Apply Changes」を選択してください。ウィンドウが表示されますので「Apply」を選択してください。これでFortranでの開発に必要な諸ファイルがダウンロード・インストールされます。
処理が完了しましたらMinGWを閉じてもらってかまいません。
PATHを通す
MinGWのインストールディレクトリ(標準設定ではC:\MinGW)直下の「bin」というディレクトリに「gfortran.exe」という実行ファイルが配置されていることを確認してください。これがFortranのコンパイラです。
この「bin」ディレクトリにPATHを通してください。理由はよく分かりませんがPATHを通さないと正常に動作しません。
開発環境が準備できたかの確認
Fortranの開発環境が準備できたかの確認を行います。適当なシェル――Command PromptかPowerShellを開き、gfortran.exeを走らせて動作確認をします。
以下コマンドを打ち、Fortranのバージョン情報が表示されることを確認してください。
PS C:\Users\tomomoss> gfortran -v
Using built-in specs.
COLLECT_GCC=C:\MinGW\bin\gfortran.exe
COLLECT_LTO_WRAPPER=c:/mingw/bin/../libexec/gcc/mingw32/9.2.0/lto-wrapper.exe
Target: mingw32
Configured with: ../src/gcc-9.2.0/configure --build=x86_64-pc-linux-gnu --host=mingw32 --target=mingw32 --disable-win32-registry --with-arch=i586 --with-tune=generic --enable-static --enable-shared --enable-threads --enable-languages=c,c++,objc,obj-c++,fortran,ada --with-dwarf2 --disable-sjlj-exceptions --enable-version-specific-runtime-libs --enable-libgomp --disable-libvtv --with-libiconv-prefix=/mingw --with-libintl-prefix=/mingw --enable-libstdcxx-debug --disable-build-format-warnings --prefix=/mingw --with-gmp=/mingw --with-mpfr=/mingw --with-mpc=/mingw --with-isl=/mingw --enable-nls --with-pkgversion='MinGW.org GCC Build-2'
Thread model: win32
gcc version 9.2.0 (MinGW.org GCC Build-2)
せっかくなので正常にコンパイルできるかも確認しておきましょう。以下コードが正常にコンパイルされ、生成された実行ファイルも正常に動作するか確認してください。
program test
print '(a)', 'Hello world.'
end program
PS C:\Users\tomomoss> gfortran .\test.f90 -o test
PS C:\Users\tomomoss> .\test.exe
Hello world.
ここまでできたらパーフェクトです。お疲れさまでした。
注意したい仕様
基本的な文法などはGitHubに上げているソースコードを読んでいただくか、あるいは自前で調べていただくとして――ここからは、GitHubに上げているソースコードからは読み取れない仕様や初めてFortranを触る人に向けての注意事項を列挙します。
Fortranの動かし方
Fortranソースファイルの拡張子には「.f90」を使うのが 無難 です。何故「無難」などという言い回しをするかというとFortranには公式的な拡張子というものがなく、慣習的に.f90が使われているためです。噂ではコンパイラによって使える拡張子に違いがあるようですが、何はともあれ、当記事にて紹介した開発環境では.f90で問題なく動作します。
今回の開発環境――GFortranのコマンドを軽く紹介します。
コンパイルするコマンドは以下の通りです。これでコンパイルすると「a.exe」という実行ファイルが生成されます。このファイル名は固定です。
PS C:\Users\tomomoss> gfortran コンパイルするファイルのパス
特定のファイル名を生成したいときは-oオプションを使用します。
PS C:\Users\tomomoss> gfortran -o ファイル名 コンパイルするファイルパス
また、モジュールを利用する場合は モジュールを格納したファイルを先にコンパイルしてから、program文が記述されたファイルをコンパイルする ようにしてください。たとえば、メインとなるソースファイル「main.f90」と、モジュールとなるソースファイル「mod_a.f90」「mod_b.f90」がある場合は次の順番でコンパイルします。
まずはモジュールだけをコンパイルします。 モジュールだけをコンパイルするときは-oオプションは不要 です。
PS C:\Users\tomomoss> gfortran .\mod_a.f90 .\mod_b.f90
するとコンパイルされたモジュールファイル(.modファイル)が生成されます。上記の場合は「mod_a.mod」と「mod_b.mod」が生成されます。
その後、メインとなるソースファイルと、メインとなるソースファイルから参照しているモジュールファイルをまとめてコンパイルします。このときは-oオプションを使って生成される実行ファイル名を指定しておいたほうが良いでしょう。
PS C:\Users\tomomoss> gfortran -o main .\main.f90 .\mod_a.f90 .\mod_b.f90
これで「main.exe」が生成されます。
1行あたり132文字の制限
Fortranでは 1行あたり132文字まで しか記述することができません。133文字以上書くとコンパイルエラーになります。
ただ、ここで注意したいのは 何をもって文字数として数えるのか ということです。ここがややこしいところで、Fortranでは独自の法則に基づいて1行あたりの文字数が計算されることを覚えておいてください。
1行あたりの文字数は次のような計算で求められます。まずは以下コードをご覧ください。
! インデントは半角空白2つにしています。
program main
print '(a)', 'Hello world.'
end program
このコードにおける1行あたりの最大文字数は3行目の27文字です。何故27文字なのでしょうか。
Fortranの1行あたりの文字数カウント処理には 文法的に不要な部分は文字数として数えられない という性質があります。たとえば、上記コードではprint文の直後やカンマ記号の直後に半角空白を挿れています。この半角空白は文法的には必須ではなく、コードの可読性を上げるためのものです。このような文法的に必須ではない文字はカウント対象外として無視されるのです。
ただ、何故か インデントを構成する半角空白はカウント対象 となっています。注意してください。
以下2つのソースコードは、それぞれ実際に書かれたソースコードとコンパイラ側で認識されていると思われるソースコードの形になります。
! こちらは実際のソースコードです。
program main
print '(a)', 'Hello world.'
end program
! こちらは、おそらくコンパイラが認識しているソースコードです。
! 微妙に半角空白などが詰められていることが分かります。
program main
print'(a)','Hello world.'
end program
1行あたりの字数制限の対策
1行あたり132文字までしか記述できないという制約への対策には2種類あります。
1つは、アンド記号を使って1文を複数行に分けて書く方法です。
program main
print &
'(a)', &
'Hello world...?'
end program
このアンド記号は行末に付けるだけでも機能するのですが、どこが複数行になっているのかを把握しやすくするために改行された行の先頭に付けてもかまいません。
program main
print &
& '(a)', &
& 'Hello world...?'
end program
そして、もう1つの方法が、文字数制限を適用せずにコンパイルするというものです。コンパイル時に-ffree-line-length-noneオプションを付けることで文字数制限が解除されます。
大文字・小文字は区別しない
キーワードや識別子の大文字・小文字は区別しません。
program文とエントリポイント
Fortranには面白い仕様がいくつかあるのですが、その1つが 主となるプログラム部分はprogram文のブロック内に記述しなければならない ことです。
program文には3つの書き方があります。どの書き方でも処理内容に違いは出ないはずです。「プログラム名」の部分には任意の識別子をあててください。一般的には「main」が使われるようです。
! 最も丁寧な書き方です。
! 色々な解説記事では、この書き方が推奨されていました。
program プログラム名
! ※色々な処理
end program プログラム名
! ちょっと省略した書き方です。
! じゃんけんゲームで採用している書き方です。
program プログラム名
! ※色々な処理
end program
! さらに省略した書き方です。
program プログラム名
! ※色々な処理
end
なお、 プログラム名に使用した識別子は他のところでは使用できません 。使用したプログラムをコンパイルすると次のようなエラーが発生します。
program main
implicit none
integer :: main
print '(a)', 'Hello world.'
end program
PS C:\Users\tomomoss> gfortran .\test.f90 -o test
.\test.f90:3:17:
3 | integer :: main
| 1
Error: Symbol 'main' at (1) cannot have a type
そして、Fortranのエントリポイントは program文の先頭 になっています。
モジュール
Fortranにもモジュール機能が実装されています。
モジュールはmodule文を使って定義します。
module モジュール名
! ※色々な処理
end module
モジュールを利用するにはuse文を使用します。use文は program文・module文のブロック先頭でのみ 記述することができます。implicit noneを宣言する場合はuse文の直後に置いてください。
program main
use モジュール名
implicit none
end program
コメント
感嘆符記号から行末までがコメントの範囲となります。ブロックコメント(複数行コメント)は実装されていません。
! ここがコメントです。
変数宣言はブロック先頭
C言語の古い規格では、変数宣言はブロック先頭でないと駄目でした。
Fortranでは新しいバージョンでも ブロック先頭でないと駄目 です。変数宣言よりも前に置けるのはimplicit noneやuse文などに限られています。
変数の初期値はリテラルのみ
変数の初期値に指定できるのはリテラルだけです。関数からの戻り値を初期値として利用することはできません。
implicit noneと変数の暗黙的型宣言
implicit noneはprogram文の直後に配置することができる命令文です。
この命令文の効果は2つあります。1つは変数の宣言を強制化させることです。これはVB系言語に見られるOption Explicitと同様の効果ですね。そして、もう1つが 変数の暗黙的型宣言 を行わないようにする効果です。
変数の暗黙的型宣言とは、宣言されていない変数が使用されるときに 変数名に応じてデータ型が暗黙的に定まる という大変面白い仕様です。具体的には、変数名がiからnで始まるときは整数型、それ以外であれば実数型の変数と見なされます。
連結演算子
文字列同士をくっつけるには二重スラッシュ記号を使います。
program main
character(1) foo_1
character(1) foo_2
foo_1 = 'a'
foo_2 = 'b'
print '(a)', foo_1 // foo_2
end program
数値と文字列の連結
Fortranでは 数値と文字列を直接連結させることはできません 。連結させたいならば数値を文字列に変換して、文字列同士でくっつける必要があります。
JavaScriptでたとえるならば、以下のような処理は書けないということです。
console.log(1 + '足す' + 1 + 'は?');
配列の操作
Fortranで配列を取り扱うときは少し注意していください。
まず、Fortranには固定長配列と可変長配列の2つが用意されています。可変長配列の記述方法はかなり奇妙なものになっています。
program main
implicit none
! 長さ3の整数型固定長配列です。
integer :: array_1(3)
! 整数型可変長配列です。
integer, allocatable :: array_2(:)
end program
インデックス値が「1」から始まることに注意してください 。「0」ではありません。
program main
implicit none
integer :: array(3)
array(1) = 1
array(2) = 22
array(3) = 333
end program
また、複数の要素をまとめて選択することもできます。
program main
implicit none
integer :: array(3)
! 第1要素から第3要素に値を代入しています。
array(1:3) = 1
end program
可変長文字列型配列
C言語などと同様に――Fortranは変数に文字列を代入するときは、原則としてあらかじめ代入する文字列の長さを指定しておく必要がある言語の1つです。
program main
implicit none
! あらかじめ文字列の長さを決めておく必要があります。
character(128) :: string_variable
end program
ただ、可変長文字列という機能も実装されていますので、そちらを利用することもできます。可変長配列と似た記述になっていますので混同しないように注意してください。
program main
implicit none
! これが可変長文字列配列です。
character(:), allocatable :: string_variable
! これは可変長配列です。
integer, allocatable :: array(:)
end program
サブルーチンと関数
Fortranではサブルーチンと関数は別のものとして定義されています。
サブルーチンは戻り値を返さない処理塊のことです。call文を使って呼び出します。
対して、関数は戻り値を返す処理塊のことです。call文を使って呼び出すことはできず、変数への代入やサブルーチン・関数の引数部分にのみ使用することができます。
program main
implicit none
call sample_subroutine(sample_function())
contains
!!
!! 引数に指定された文字列を標準出力します。
!!
subroutine sample_subroutine(message)
character(*) :: message
print '(a)', message
end subroutine
!!
!! 特定の文字列を返します。
!!
function sample_function() result(message)
character(:), allocatable :: message
message = 'def'
end function
end program
引数と戻り値の再定義
Fortranでは、全く奇妙なことに、 サブルーチンと関数の引数・戻り値はそれぞれの定義ブロック内の先頭で変数のように宣言しておく必要があります 。これは私の知るかぎりFortranでしか見られない仕様であり、大変興味深いものです。
引数は原則参照渡し
サブルーチン・関数への引数は、原則として参照渡し になります。
値渡しをするには、サブルーチン・関数を呼び出すときに引数を小括弧で囲む必要があります。
program main
implicit none
integer :: foo = 1
! これは参照渡しです。
call sample_subroutine(foo)
! これは値渡しです。
call sample_subroutine((foo))
contains
!!
!! 受け取った値をインクリメントします。
!!
subroutine sample_subroutine(foo)
integer :: foo
foo = foo + 1
end subroutine
end program
ただ、コメント欄で頂きましたご指摘によりますと、この小括弧で囲む方法は 現在ではバッドプラクティス のようです。軽く調べてみた感じではFortran 2003よりvalue属性というものが追加されたので、そちらを使う方が良さそうです。
program main
implicit none
integer :: foo = 1
call sample_subroutine(foo)
contains
!!
!! 受け取った値をインクリメントします。
!!
subroutine sample_subroutine(foo)
! 引数の属性にvalue属性を追加します。
integer, value :: foo
foo = foo + 1
end subroutine
end program
私が意識していること
Fortranを使うときに私が意識していることを列挙します。1つ前の「注意したい仕様」を読んでいることを前提とした内容になっています。
コーディング規約
Fortranの公式コーディング規約というものは存在しないようです。ただ、いくつかの団体がコーディング規約を発表していますので、いずれかに準拠するのが良いのではないでしょうか。
なお、じゃんけんゲームのソースコードは我流で書いています。
数値と文字列の連結方法
Fortranでは数値と文字列を直接くっつけることはできません。また、データ型変換用の関数なども用意されていません。
そのため、少し手間をかけて実現する必要があります。手順は以下コードを参考にしてください。
program main
implicit none
! 連結したい数値です。
! ここでは変数に入れていますが数値リテラルでもかまいません。
integer :: number = 123
! 連結したい数値を文字列化したときに保持する変数を宣言しておきます。
character(4) :: tmporary_variable
! write文を使って文字列に変換しつつ、文字列型変数に値を出力します。
write (tmporary_variable, '(i0)') number
! 文字列同士になったので連結させることができます。
print '(a)', '123 == ' // tmporary_variable
end program
intent属性
コメントにてご指摘頂きましたが――サブルーチンと関数の引数には intent属性 を付けたほうが良さそうです。特に、後述するin指定子は便利であるように思われます。
intent属性とは、そのサブルーチンと関数の手続き内で引数がどのように使われるかを明示する仕組みのことです。intent属性には引数の振る舞いを指定する in・out・inoutの3つの指定子 があり、いずれかを指定することになります。
in指定子は 引数を定数化する指定子 です。in指定子が付けられた引数はサブルーチン・関数内で値を変更することができません。以下プログラムではin指定子が付けられた引数に再代入が行われていますが、これはエラーになります。
program main
implicit none
integer :: foo = 1
call sample_subroutine(foo)
contains
!!
!! 受け取った数値をインクリメントして標準出力します。
!!
subroutine sample_subroutine(foo)
integer, intent(in) :: foo
! in指定子が付いているのに再代入してはいけません。
foo = foo + 1
print '(i0)', foo
end subroutine
end program
PS C:\Users\tomomoss> gfortran .\test.f90
.\test.f90:15:4:
15 | foo = foo + 1
| 1
Error: Dummy argument 'foo' with INTENT(IN) in variable definition context (assignment) at (1)
out指定子は value属性を付けられなくなる指定子 です。先述のようにFortranの引数は原則として参照渡しになるのですがvalue属性を付けるなどすれば値渡しにすることができます。out指定子が付けられた引数は呼び出し側にデータを渡すための引数――つまり、 参照渡しであることを利用してデータを渡すための引数 であることを表す指定子であるため、常に参照渡しである必要があります。
inout指定子は、その引数がサブルーチン・関数側にデータを提供する引数であり、また、呼び出し側にデータを返す値であることも意味する指定子です。実態としてはout指定子に近いようです。