Moai+Easter Advanced Manual

Moai Manual Advanced Manual Annoucement FAQ
ご案内 Moaiエンジン CustomBoyエンジン HowToコンパイル Moai CGI Developers Raraku言語
Raraku言語マニュアル(前編) Raraku言語マニュアル(後編) Raraku Advanced document

はじめに

この記事は、Raraku言語についてのチュートリアルです。 Rarakuは我々が独自に開発したプログラミング言語です。

この記事ではプログラミングの初歩的な用語から段階を踏んで説明しています。 そのためプログラミングの全くの初心者でも頑張れば理解できると思います。 ただし、なんらかのプログラミング言語かスクリプト言語を弄ったことがあればもちろんベターです。 特にRarakuはC言語とGo言語の文法に似ていますので、 これらの言語を知っていればベストです。

このチュートリアルではC言語やGo言語を知っている方向けに「C言語では」「Go言語では」「C言語と異なり」「Go言語とは異なり」 といった表記を使って説明している箇所が随所にあります。 ただしこれらの言語を知らない方はその部分を読み飛ばしても特に問題ありません。

また時々「言語仕様の背景」という名前のコラムがありますが、こちらもほとんどの方は読み飛ばして構いません。 これはRarakuの言語仕様が現在にまで至る過程やなぜそのような仕様になったのかといったようなことが書かれています。 つまりコンパイラの内部処理や言語の実装そのものに興味がある方向けの記事です。


目次



Rarakuの実行テスト

Rarakuの実行テスト


Raraku言語を実行させる最初の確認作業として、極簡単な文字列(Hello)を表示する例を取上げます。 例えば以下のような内容のテキストファイルを作成し、ファイル名を test.rrks として保存します (ファイル名はなんでも構いませんが、拡張子は必ずrrks(Raraku Sourceの略)としておきます)。

Rrk_print( "Hello" )

次にrarakuコマンドを使ってtest.rrksを実行させます。 ただし大抵の場合、rarakuコマンドが置いてある位置はtest.rrksが置かれている位置とは異なるでしょう。 ここではWindows(dos)とLinux(shell)の場合に分けてその実行手順を以下に説明します(CygwinやMSYSをご使用の方はLinuxをご覧下さい)。

  • お使いのOSがWindowsの場合

  • 例えば C:\raraku-v1.0-win32\raraku.exe にrarakuコマンドが置かれているとしましょう。

    コマンドプロンプトを開き、以下のようにそのパスを指定して実行します (長いですのでTabキーの補完機能が有効な環境であれば、それを何回か押して補完しながら入力するとよいでしょう)。

    C:\raraku-v1.0-win32\raraku test.rrks
    

    しかし毎回このように長いrarakuコマンドのパスを打ち込むのも面倒ですので、 環境変数PATHにrarakuコマンドの置かれた位置を指定しておきます。 これには色々な方法がありますが、例えばコマンドプロンプト上で以下のように入力して実行します。

    set PATH=%PATH%;C:\raraku-v1.0-win32
    

    これ以降は以下のように入力するだけでrarakuが実行できるはずです。

    raraku test.rrks
    

    Helloという文字列が表示されればRarakuの実行テストは成功です。

    ここまでが基本事項ではあるのですが、ここから先はこの実行をさらに楽にする方法をあれこれ考えます。

    環境変数PATHの設定は(Windowsでの環境変数設定ダイアログで値を変えない限り)一つのコマンドプロンプトの中だけで有効です。 逆に言えば、通常、別の新しいコマンドプロンプトを開いた場合はまた上記のように設定し直す必要があります。 このPATHの設定のためのコマンドの入力は、特にパスが長い場合など面倒なこともありますから、 以下のような内容のバッチファイルをテキストエディタで作成しておくのもよいでしょう。

    @echo off
    set PATH=%PATH%;C:\raraku-v1.0-win32
    

    一行目の「@echo off」はRarakuと関係ありませんが、 これを指定しない場合「set PATH=%PATH%;C:\raraku-v1.0-win32」といったコマンドそのものの文字列が表示され、 ほとんどのケースで煩わしくなります。 今このバッチファイルを仮にraraku_setup.batという名前で現在いる位置(カレントディレクトリ)と同じ場所に保存したとします。 その場合、コマンドプロンプトから単に以下のように入力すれば環境変数PATHの設定を行うことができます (「set PATH=%PATH%;C:\raraku-v1.0-win32」と打ち込むよりは多少楽でしょう)。

    call raraku_setup.bat
    

    毎回コマンドプロンプトを開くのすら面倒であれば、以下のような内容のバッチファイルを作成しておきましょう。 そのようにすれば、このバッチファイルをダブルクリックするだけですべての設定と実行が完結することになります。

    @echo off
    set PATH=%PATH%;C:\raraku-v1.0-win32
    raraku test.rrks
    pause
    

    最後の行の「pause」はRarakuと関係ありませんが、 これを指定しない場合、rarakuコマンドが終了した瞬間にコマンドプロンプトが即座に終了してしまい(ウィンドウ自体が閉じてしまい)、 場合によっては実行結果を確認できなくなります。

    raraku_setup.batを作っている場合は、上記の替わりに以下のようにしてもよいでしょう。

    @echo off
    call raraku_setup.bat
    raraku test.rrks
    pause
    

    ただしこのバッチファイル内には「test.rrks」と直接書いてしまっていますから、 つまりは「test.rrks」専用です。 これとは別の「test2.rrks」を実行したい場合、「test.rrks」の部分を書き換えるか、 もう一つ「test2.rrks」専用のバッチファイルを作る必要があります。

    しかしrrksファイルの数が増えてきた場合、これでは煩わしいかもしれません。 その場合は以下のようなバッチファイルを作り、rrksファイルと同じフォルダに置いておくと便利でしょう。

    @echo off
    call raraku_setup.bat
    cmd
    

    これをダブルクリックで実行しますと、raraku_setup.batを最初に自動的に実行した上でコマンドプロンプトが開きます。 特定のrrksファイルが即実行されるわけではありませんが、 少なくとも「call raraku_setup.bat」と入力する手間は省けますので、 何もしなくとも最初から「raraku test.rrks」と入力して実行することができます。 この後さらに「test2.rrks」を実行したい場合も、(コマンドプロンプトを閉じずに続けて) 単に「raraku test2.rrks」と入力することで実行できます。

    このような場合、矢印キーをうまく使うとよいでしょう。 矢印キーの上下を押すと、コマンドプロンプトで今まで入力した履歴がもし残っていればそれを辿ることができます。 例えば直前に「raraku test.rrks」を実行していた場合、矢印キーの上を押すとこれを表示させることができます。 バックスペースキーを何回か押し、「raraku test」まで削り、そこから続きを入力して 「raraku test2.rrks」という文字列を完成させる手順で入力すればかなり楽ができます。

    このチュートリアルの例ではほとんど意識する必要はありませんが、 rarakuコマンドにはrrksファイルを与える以外にも様々なオプション引数があり、 例えば「raraku test.rrks」ではなく「raraku -shell dos test.rrks」と入力すると、 エラーメッセージの一部が色つきになります (その他、(詳細は後のセクションで述べますが)-pkg_pathオプションなどを指定することもよくあります)。

    オプション引数の指定が増えると、今度はそれを毎回入力するのが面倒です。 そのような場合、例えば以下のようなバッチファイルを作っておくとよいかもしれません。

    @echo off
    raraku -shell dos -pkg_path .. %1
    

    今、このバッチファイルの名前を仮にrun.batとしましょう。 上記の「%1」はこのrun.batファイルの第1引数を取得するというものです。 例えば「call run.bat test.rrks」と入力して実行すれば 上記の「%1」の部分が「test.rrks」という文字列に置き換えられ、 結局「raraku -shell dos -pkg_path .. test.rrks」が実行されることになります。 オプション引数の指定があまりに多い場合は、こちらの方がよいでしょう。

    また、若干裏技っぽいやり方ですが、 以下のようなバッチファイルを作ってドラッグアンドドロップで起動させるという方法も一応あります。

    @echo off
    call raraku_setup.bat
    raraku -shell dos -pkg_path .. %1
    pause
    

    今、このバッチファイルの名前を仮にDragAndDropRun.batとしましょう。 このDragAndDropRun.batファイルのアイコンに、rrksファイルのアイコンをドラッグアンドドロップすると 上記の「%1」の部分がそのrrksファイルのフルパスに置き換えられて実行されることになります。 ただ、(人やマウスの種類、アイコンの大きさの設定にもよるでしょうが)マウスによる操作は (特にアイコンからアイコンへのドラッグアンドドロップは、フォルダ内にファイルが増えてくると)ミスしやすいため、 このやり方は個人的ににはあまりお勧めしません。
    その他、Windowsにおけるファイル拡張子の関連付け機能を使って、rrksファイルをraraku.exeに関連付ける方法もあるでしょうが、 これは色々と問題があるように思います。

    まず、そもそもですがrrksファイルは基本的にはプログラマが編集するソースコードです。 ですから関連付けるとしても、これをダブルクリックしたときに raraku.exe が実行されるよりも、 普通は使い慣れたテキストエディタが開く方がよいのではないでしょうか? (編集するときは右クリックでメニューを出して指定する派の方や、そもそも編集などしない方もいるかもしれませんが…)

    また、詳しくは後のセクションで述べますが、他のヘッダをインポートしたり他のrrksとリンクするような複雑なrrksの場合、 ファイル拡張子の関連付けダイアログでは修正しにくく、あまり簡単には対応できません。 その場合は、おそらく別途バッチファイル等を利用し、その中で依存関係を記述し、それを介して実行する方がよいでしょう。

    ただ、すぐ後で述べますが、Rarakuには拡張子がrrkxというファイルもあります。 これは実行専用のバイナリファイルです(実行専用といっても結局rrkxファイルもraraku.exeで実行する必要はあります。 Javaをご存知の方はいわゆるバイトコードと呼ばれる類のものと同じです)。 Rarakuで作られたアプリケーションを使う側の方(プログラマではない方)へは ほとんどの場合、rrks ではなくこの rrkx ファイルが提供される形になることが想定されます。 そして rrkx であれば(上で述べた問題がないため)普通にraraku.exeに関連付けてよいのではないかと思います。

  • お使いのOSがLinuxの場合

  • 例えばホームディレクトリの直下の Download/raraku-v1.0-linux64/raraku にrarakuコマンドが置かれているとしましょう。

    以下のようにそのパスを指定して実行します (長いですのでTabキーの補完機能を活用しましょう)。

    ~/Download/raraku-v1.0-linux64/raraku test.rrks
    

    毎回このように長いrarakuコマンドのパスを打ち込むのが面倒であれば、 環境変数PATHにrarakuコマンドの置かれた位置を指定しておきます。 これには色々な方法がありますが、例えば以下のように実行しておきます。

    PATH=$PATH:$HOME/Download/raraku-v1.0-linux64
    

    これ以降は以下のように入力するだけでrarakuが実行できるはずです。

    raraku test.rrks
    

    Helloという文字列が表示されればRarakuの実行テストは成功です。

    ここまでが基本事項ではあるのですが、ここから先はこの実行をさらに楽にする方法をあれこれ考えます。

    環境変数PATHの設定は(シェルの設定ファイル等で値を変えない限り)一つのターミナルの中だけで有効です。 逆に言えば、通常、別の新しいターミナルを開いた場合はまた上記のように設定し直す必要があります。 このPATHの設定のためのコマンドの入力は、特にパスが長い場合など面倒なこともありますから、 以下のような内容のシェルスクリプトファイルをテキストエディタで作成しておくのもよいでしょう。

    #/bin/sh
    PATH=$PATH:$HOME/Download/raraku-v1.0-linux64
    

    今このバッチファイルを仮にraraku_setup.shという名前で現在いる位置(カレントディレクトリ)と同じ場所に保存したとします。 その場合、ターミナルから単に以下のように入力すれば環境変数PATHの設定を行うことができます (「PATH=$PATH:$HOME/Download/raraku-v1.0-linux64」と打ち込むよりは多少楽でしょう)。

    source raraku_setup.sh
    

    あるいは以下のような内容のシェルスクリプトファイルを作成しておくと、これだけですべての設定と実行が完結することになります。

    #/bin/sh
    PATH=$PATH:$HOME/Download/raraku-v1.0-linux64
    raraku test.rrks
    

    このファイル名をtest.shとしましょう。 これを実行するには以下のようにします。

    sh test.sh 
    

    あるいは予め「chmod」コマンドによりtest.shに実行可能パーミッションを付与しておけば、 単に以下のようにするだけで実行できるはずです。

    ./test.sh 
    

    デスクトップ環境によっては、拡張子がshであるファイルをクリックするだけで(シェルスクリプトとして)起動できるかもしれません。 ただしLinuxでのデスクトップ環境は非常に多様な上、仕様も頻繁に変わるため、一概には言えません。 やはりLinuxではターミナルから起動する方が確実であるとは思います。

    上記のシェルスクリプトファイル内には「test.rrks」と直接書いてしまっていますから、 つまりは「test.rrks」専用です。 これとは別の「test2.rrks」を実行したい場合、「test.rrks」の部分を書き換えるか、 もう一つ「test2.rrks」専用のシェルスクリプトファイルを作る必要があります。

    しかしrrksファイルの数が増えてきた場合、これでは煩わしいかもしれません。 その場合は以下のようなシェルスクリプトファイルを作り、rrksファイルと同じフォルダに置いておくと便利でしょう。

    #/bin/sh
    source raraku_setup.sh
    raraku $1
    

    今、このシェルスクリプトファイルの名前を仮にrun.shとしましょう。 上記の「$1」はこのrun.shファイルの第1引数を取得するというものです。 例えば「sh run.sh test.rrks」と入力して実行すれば 上記の「$1」の部分が「test.rrks」という文字列に置き換えられ、 結局「raraku test.rrks」が実行されることになります。 同様に「sh run.sh test2.rrks」と入力することで「test2.rrks」を実行できることになります。

    このような場合、矢印キーをうまく使うとよいでしょう。 矢印キーの上下を押すと、ターミナルで今まで入力した履歴がもし残っていればそれを辿ることができます。 例えば直前に「sh run.sh test.rrks」を実行していた場合、矢印キーの上を押すとこれを表示させることができます。 バックスペースキーを何回か押し、「sh run.sh test」まで削り、そこから続きを入力して 「sh run.sh test2.rrks」という文字列を完成させる手順で入力すればかなり楽ができます。
    このチュートリアルの例ではほとんど意識する必要はありませんが、 rarakuコマンドにはrrksファイルを与える以外にも様々なオプション引数があり、 例えば「raraku test.rrks」ではなく「raraku -shell dos test.rrks」と入力すると、 エラーメッセージの一部が色つきになります (その他、(詳細は後のセクションで述べますが)-pkg_pathオプションなどを指定することもよくあります)。

    オプション引数の指定が増えると、今度はそれを毎回入力するのが面倒です。 そのような場合、run.shファイルの内容を例えば以下のように修正すればよいでしょう。

    #/bin/sh
    source raraku_setup.sh
    raraku -shell dos -pkg_path .. $1
    


既に述べた通り、Rarakuでは拡張子rrksがソースコードのファイルとなり、これをrarakuコマンドの引数に与えることでこれを直接実行できます。

一方、rrksファイルを拡張子rrkx(RaRaKu eXecutableの略)というバイナリファイルに変換してから実行することもできます (むしろある程度の規模の本格的なプログラムでは最終的にこちらに変換することになるでしょう)。 この変換処理のことをコンパイルと呼びます(厳密には rrko(RaRaKu Objectの略)ファイルと呼ばれるファイルを一旦介します)。

バイナリファイルrrkxの方が実行速度は速くなります。 rrks ファイルを実行させる場合、内部的には一旦rrkxファイルの内容に相当するデータにコンパイルし、 それを実行しているため、その分時間が掛かります。 一方、rrkx ファイルから実行した場合、そのコンパイル処理がスキップできますのでその分実行速度が上がります。

とはいえ通常、小規模なコードではその速度差が気になるようなことはあまりないとは思いますので、 このチュートリアルでは当面の間はrrksソースファイルをそのまま実行させる方法をとることにします。

因みにこれらの拡張子を何と発音するかですが(まあ好きなように呼べばよいとは思うのですが)、 rrksは「ララクエス」とか「ララクス」、 rrkxは「ララクエックス」、 rrkoは「ララクオー」とか「ララコ」でいいんじゃないでしょうか?


目次に戻る

varia文とconst文と代入文

int型変数/定数の宣言


Rarakuにおける変数とは、ある実体を持つ値を一時的に指し示すための識別子です。 またそれが指し示す先は代入により変わることがあります。

参考: プログラミングを初めて勉強される方へのアドバイス
プログラミングでは様々な抽象的な概念が登場しますが、「変数」もそのうちの一つです。 そして(プログラミングに限った話ではありませんが)このような抽象的な概念をイメージとして捉える場合、 (特にそれに初めて触れる場合は)身近にある具体的なものに喩えて考える方法が有効です。 これをアナロジー(類推)と呼びます。 上で記述された変数の説明だけで理解できる場合は勿論それでよいのですが、万一そうではない場合、 様々なアナロジーに触れることで理解が促進できる場合も多いのです。

変数のアナロジーとして、例えばWebブラウザにおけるブックマークをイメージするとよいかもしれません。 ブックマークの「名前」と「URL」の関係は、変数における「識別子」とそれが指し示す「実体」の関係に似ています。 例えば「MyFavorite」という名前のブックマークがあるとすれば、 それに関連付けられている実際のURLは「http://www.google.com」や「http://www.yahoo.co.jp」など色々な実体をとり得るわけですが、 そのような関係ということです。 代入とは「MyFavorite」という名前はそのままに、実体であるURLを他のものに変えるような操作に相当します。

このチュートリアルでは、すべての概念においてこのようなアナロジーを逐一紹介することは残念ながらできませんが、 ネットで同様の概念を検索するとわかりやすいアナロジーが見つかるかもしれません。 万一躓いた場合、そのようなものを参考に理解を進めるとよいでしょう。

ここで解説されるRarakuの機能は膨大ですが、初学者がそのすべてを一度に学習するのは無理があるでしょうしその必要もありません。 ある程度学んだらその時点で実際にRarakuのプログラムを書いて動作させるなどし、よくわからない点や何らかの必要性を感じたらまたチュートリアルに戻るなどして、 少しずつ知識の幅を広めていくやり方がよいでしょう。

Close


「識別子」の厳密な定義については後で詳述しますが、 とりあえず今の段階では、アルファベットと小文字、そして「_」文字から構成される「名前を表す文字列」と考えましょう。

変数はを持っており、その型のカテゴリに属する値以外を指し示すことはできません (このような制限を設けることにより、多くの場合その制限がない場合と比べてプログラミングミスを防止できます)。

変数を定義するにはvaria文を使い、「varia 変数名 型」という書式で記述します(この語順はGo言語のvar文に似ています)。 このとき半角スペース、タブ、改行のいずれかでこれらを区切って記述する必要があります。

このような「型」指定が必須ではない言語もあります。 例えばPythonなどが有名です。
多くのプログラミング言語ではvariaに相当するものとしてvarやletなどのキーワードを採用していますが、 Rarakuではvariaというやや変なキーワードにしてあります。 これは後述するconst文と字数幅を合わせたいというのが一つの理由です。 もう一つの理由はパーザ系のプログラミングするときに変数を表す変数名としてvarを使いたい(と作者が感じた)主観的な理由です。

因みにvariaを何と発音するかですが(そもそもこんな英単語はないので別に正解も何もないのですが、一応variableの略なので)「ヴァリア」でいいんじゃないかと思います。 一方、constは他の言語でもよく登場するキーワードですが(元々はconstantの略だと思われますが)「コンスト」と発音されることが多いと思います。

また整数を示す型を整数型またはint型と呼びます(integerの略です)。 以下の例では int型の変数の宣言を行っています。

varia ival int

このとき変数は型に応じて自動的にデフォルト値で初期化されます。 例えばint型の場合は0で初期化されます。

型の後ろにさらに「=」を繋げることで初期値を明示的に指定することもできます。 以下の例では整数9を初期値として明示的に指定しています。

varia ival int = 9

このとき「=」の両側の部分を数学と同様に呼称します。 すなわち左側を左辺(式)、右側を右辺(式)と呼びます。 上の例では左辺は ival、右辺は 9 となります。

ただし数学とは異なり、この「=」は方程式や恒等式といった意味ではなく、 「今から左辺が右辺により更新される」といった意味になります。
ある変数について特に「それが代入される側であること」を強調したいとき、 その変数のことを単に左辺値と呼ぶ場合があります。 逆に「代入する側であること」を強調したり、別の式中に埋め込み可能であることを強調したいとき、 それを単に右辺値と呼ぶこともあります。
「varia」「変数名」「型」「=」「9」のように文を構成する最小単位をトークンと呼びます (英文における単語のようなものだと考えてもらっておおよそ差し支えないでしょう)。 Rarakuではトークンとトークンの間に自由に改行やスペース、タブ文字などを入れることができます。 上の例は、以下のように改行やタブを入れても意味を一切変えず一つの文とみなされます。

varia
	ival
	int
	=
	9

是非ともこれらをうまく使って見やすいコードにしてください(上の例はあまり意味もなく見やすくもない改行の入れ方ですが)。

識別子からなるトークンが連続する場合、それらを区切るため最低一つ以上のスペース、タブ文字、改行を間に挟む必要がありますが、 「=」のような記号だけからなるトークンがその間に入る場合、スペース、タブ文字、改行を一切書かずにくっ付けることもできます。 例えば以下の通りです。

varia ival int=9


一度宣言された変数には代入文により、別の値を代入することができます。 代入文は「変数名 = 数字」という書式で記述します。 以下の例では 変数ivalに整数5を代入しています。

ival = 5

C言語とは異なり、Rarakuの代入は式ではなく文となります。 このことは代入文自体は値を持たないことを意味します。 例えばRarakuでは「x = y = 5」のように「=」で繋げて代入を連続的に記述することはできません。 またこれによりC言語等でよく見られるif文の条件式に代入式を指定する慣用記法も必然的に禁じられます。 このif文における記法は便利ではありますが、一方で「==」を「=」と書き間違える危険性を孕んでいます。 ただしRarakuでは後のセクションで述べますが、それを安全にした代替の記法が存在します。

Rarakuにおける定数(constant)とは、ある実体を持つ値を一時的に指し示すための識別子です。 ただし定数の場合、それが指し示す先を変えることが許可されません。 つまり代入操作が禁止されます。

参考: なぜ変数だけでなく定数も必要か?
プログラミングの初学者の方であれば、このような疑問を持つかもしれません。

プログラミング言語の機能は、「なんらかの自由を与えるもの」と逆に「なんらかの制限を加えるもの」に大別されます。 自由を与えた場合、柔軟性は上がりますが、プログラム全体の安全性が下がります(バグの入る確率が上がるということです)。 制限を加えた場合、柔軟性は下がりますが、プログラム全体の安全性が上がります(バグの入る確率が下がるということです)。

例えば、変数と定数の関係はまさにそれです。 変数では値を変更できる分、柔軟性は上がりますが安全性は下がります。 定数では値を変更できない分、柔軟性は下がりますが安全性は上がります。

つまり値を変更する必要のないケースでは極力定数を使った方が安全であり望ましいと言えます。

Close


定数を定義するにはconst文を使い、「const 変数名 型 = 値」という書式で指定します(この語順はGo言語のconst文に似ています)。 以下の例では int型の定数(値は9)の宣言を行っています。

const ival int = 9

const文ではvaria文とは異なり、初期化が必須となります。 すなわち宣言時において型の後ろに続く「=」を省略することはできません。 またconst文で宣言された定数に再代入はできません。

Rarakuで複数の文を書く場合、通常は見易さのためその間に改行を入れて区切りましょう。 例えば以下の通りです。

varia i int = 8
varia j int = 9

文と文の区切りに関しては本当はまだ少し注意すべきことがありますが、 それについては後で詳しく述べます。

以下のように全く同じ変数名を持った複数の変数を宣言することはできません(定数についても同様です)。

varia ival int
varia ival int

この場合「varia [ival] is already declared(ivalは既に宣言されています)」といった旨のコンパイルエラーが表示されます。

ただしこれには例外もあり、ブロックが異なる場合は問題ありません。 ブロックについては「ブロックとスコープ」のセクションで詳しく説明します。

real型の宣言


Rarakuにおいて実数を示す型を実数型あるいはreal型と呼びます(浮動小数点型などと呼ばれる場合もあります)。 以下の例では real型の変数の宣言を行っています。

varia rval real

上の例では初期値を明示していませんが、この場合real型では0.0で初期化されます。

以下のように初期値を明示的に指定することもできます。

varia ival real = 9.5

realにおける代入や定数の宣言もintの場合と全く同様です。

識別子として使える文字


Raraku言語は(後述する文字列リテラルの内部やコメントの内部を除き)基本的にコード全体を半角英数字で記述しなければなりません。

また変数/定数名にはどんな名前をつけてもよいわけではなく、その名前は識別子である必要があります。 そして厳密に言うとRarakuにおける識別子とは以下のすべてを満たすものになります(C言語における識別子と同じです)。
  • 基本的にアルファベットの大文字小文字、または「_」(アンダーバー)、または数字を使う
  • ただし識別子の一番最初の文字のみ、数字を使ってはならない
「-」(ハイフン)などの文字は使えませんので注意してください(引き算の記号と区別できなくなるためです)。 例えば val、val3、my_val ような変数/定数名はいずれも問題ありません。 しかし 3val、my-valのような変数/定数名は使うことができません(意図した名前とはならず、コンパイルエラーとなります)。

さらにRarakuでは予約語というものがあり、それと一致する文字列は(たとえ上記の条件を満たしていたとしても)識別子として使用できません。 これらはRarakuにおいて特別な働きをするからです。 Rarakuにおける予約語は以下のようなものになります(アルファベット順)。

array auto
bool break
case conststr const continue
default defer dowhile
elif else enum extern
fallthrough false final for foreach function functype
goto global
handler
int int8 int16 int32 int64 if import immut introvert
longjmp lambda
natvfunc natvobj notnull null
pragma
real real32 real64 refer return register
setjmp shadow signed static string struct switch
tight tightstr trait true try typedef
uint uint8 uint16 uint32 uint64 unsetjmp unsigned using utilize
varia void volatile
while

extern、shadow、using、introvert、register、tightstr、signed、unsignedについては現在のRarakuでは何の意味も持ちませんが、 将来のバージョンに備えて予約語とされており、ユーザは識別子としては使用できません。
registerはC言語において予約語として存在します。

Raraku言語でかかれたコードをC言語へ移植する状況は普通に想定されるため、ユーザはこれを識別子としては使用できません。 とはいえ、これだけであれば他の予約語に対しても同じことは言えるのですが、さすがに有名な予約語については、 (後にその言語に移植する可能性を事前に想定しているならば)それを変数名等として使用することを普通は避けるかと思います。

一方、このregisterという予約語は、現在ではC言語プログラマでさえ直接使うことはほぼなく (そのためこれがC言語の予約語であることをC言語プログラマでさえ失念しやすく)、またこれを「登録する」という動詞の意味において 関数名やメソッド名等に使ってしまう状況は十分に想定されるため、最初から使用不可として予防線を張っているというのもあります。
その他、(予約語ではありませんが)ユーザは基本的に「Rrk」で始まる文字列を識別子として宣言すべきではありません。 Rarakuの標準ライブラリの中にあるすべてのものは「Rrk」で始まる名前を使っていますが、 それらの名前と衝突するかもしれないからです。

int型リテラル


Rarakuでは変数や定数ではない、コード中に直接与えられた数字のことを数値リテラルと呼びます。 特にその数値がint型(整数型)である場合、それを整数(型)リテラル、あるいはint型リテラルと呼びます。

さらにASCII文字とシングルクォートを指定することでその文字コードを整数として指定することもできます。 例えば 'a' と記述すると ASCIIでの「a」の文字コード(10進数で97)を指定したのと同じになります (つまり、これも実体は整数型の一種であることに注意しましょう)。

このような文字コードの指定をRarakuでは文字リテラル、 あるいは整数型であることを強調したい文脈では文字整数リテラルと呼びます。

varia文/const文の初期化においてint型リテラルを指定する場合、型名を省略することができます(int型での宣言とみなされます)。 例えば以下では 3 というint型リテラルを指定しており、int の記述を省略しています。

varia ival = 3

また同様にvaria文/const文の初期化においてint型変数/定数を単独で指定する場合も、型名を省略することができます(int型での宣言とみなされます)。 例えば以下では ival の初期化において iv というint型変数を単独で指定しており、int の記述を省略しています。

varia iv = 3
varia ival = iv /* int型変数で初期化 */

このように型名が直接指定されていない宣言において、他の補助的な情報(上記では右辺式の型)からその宣言の型を自動的に決定することを 型推論すると呼びます。

Rarakuでは桁数の多いint型リテラルの数字と数字の間に「_」(アンダーバー)を挿入して見やすくすることができます。 このとき、この「_」は必ず何らかの数字で挟まれていなければなりません(この「_」を二つ連続したりint型リテラルの両端の位置に記述するようなことはできないということです)。 この「_」は単に人間にとっての見やすさのためだけに挿入するものですから、最終的にはこれが削除された状態に変換されます。 例えば次の通りです。

varia iv int = 0
iv = 1000000
iv = 1_000_000

上の例で「1_000_000」という部分は、(最終的に「_」はすべて削除され)「1000000」と全く同じ意味になります。

uint型リテラルとuint型の宣言


Rarakuではint型リテラルの最後に「u」を付記することもでき、 例えば 0u、3u、1_000_000u といった形になります。 Rarakuでは、このような最後に「u」が付記されたint型リテラルを基本的に「負になることはない値」とみなします。 そしてこれらは int型変数ではなく、替わりに「非負な整数型」を持つ変数に格納しなければなりません。 この「非負な整数型」を特にuint型あるいはunsigned整数型と呼びます。 Rarakuではuint型は「uint」という名前の型で指定します。 またuint型の数値リテラルをuint型リテラルと呼びます。

uint型をunsigned整数型と呼ぶのに合わせ、int型を特にsigned整数型と呼ぶ場合もあります。

例えば以下ではuint型リテラル「1_000_000u」をuint型変数uvalへ代入しています。

varia uval uint = 1_000_000u

int型リテラルの場合と同様に、varia文/const文の初期化においてuint型リテラルを指定する場合、型名を省略することができます(uint型での宣言とみなされます)。 例えば以下ではuint型リテラル「0u」を指定しており、uint の記述を省略しています。 結果としてuvalの型は「uint型」と型推論されます。

varia uval = 0u

これまで整数型の数値リテラルを(-1, 0, 3, 19 などといった)10進数で記述していました。 一方、Rarakuでは最初に0xで始めて16進数で整数型の数値リテラルを記述する方法もあり、 例えば 0x10(10進数で16)、0xF(10進数で15)といった形になります。 Rarakuでは、このような16進数で記述された整数型の数値リテラルをuint型とみなします。

例えば以下ではuint型リテラルである0x11をuint型変数uvalへ代入しています。

varia uval uint = 0x11

このような16進数で記述されたuint型リテラルの場合も、今までと同様に型推論できます。 例えば以下では、uvalの型は「uint型」と型推論されます。

varia uv = 0x11

int型とuint型の代入と種類


int型変数とuint型変数は同じ整数ではありますが型が異なるため、(若干の例外を除き)基本的にはそれらを相互に代入できません。 例えばint型変数では負の整数が格納されている可能性があるため、これをそのままuint型に代入すると場合によっては何か問題が起きるかもしれません。 また一般にuint型の方がint型よりも(正の数として)大きい値を格納することができますが、それをint型に代入した場合も何か問題が起きるかもしれません。 そのためこれらを相互に代入することは(直接的には)できないようになっています。

しかしそうは言っても実際のプログラミングでは、このような代入が特に問題ないケースも多々あります。 そのような場合キャストを用いることで相互に代入が可能になります。 キャストについては「型のキャストと暗黙変換」のセクションで詳しく述べます。

Rarakuにはintとuint以外にも整数型を表す型名が存在します。 Rarakuで扱える整数型をすべて挙げると以下のようなになります (これは他の多くの言語で扱える整数型の仕様と同じです)。

  • int8型
  • 8bitで表現可能なsigned整数型です。 最小値は -2の7乗、最大値は 2の7乗-1 です。 すなわち-128から127までの整数を格納できます。

  • int16型
  • 16bitで表現可能なsigned整数型です。 最小値は -2の15乗、最大値は 2の15乗-1です。 すなわち-32768から32767までの整数を格納できます。

  • int32型
  • 32bitで表現可能なsigned整数型です。 最小値は -2の31乗、最大値は 2の31乗-1です。 すなわち-2147483648から2147483647までの整数を格納できます。

  • int64型
  • 64bitで表現可能なsigned整数型です。 最小値は -2の63乗、最大値は 2の63乗-1です。 すなわち-9223372036854775808から9223372036854775807までの整数を格納できます。

  • int型
  • int型はint64型の別名でありこの二つは全く同じ型です (32bit/64bit版のいずれのRarakuでも、intはint64型です)。

  • uint8型
  • 8bitで表現可能なunsigned整数型です。 最小値は 0、最大値は 2の8乗-1 です。 すなわち0から255までの整数を格納できます。

  • uint16型
  • 16bitで表現可能なunsigned整数型です。 最小値は 0、最大値は 2の16乗-1 です。 すなわち0から65535までの整数を格納できます。

  • uint32型
  • 32bitで表現可能なunsigned整数型です。 最小値は 0、最大値は 2の32乗-1 です。 すなわち0から4294967295までの整数を格納できます。

  • uint64型
  • 64bitで表現可能なunsigned整数型です。 最小値は 0、最大値は 2の64乗-1 です。 すなわち0から18446744073709551615までの整数を格納できます。

  • uint型
  • uint型はuint64型の別名でありこの二つは全く同じ型です (32bit/64bit版のいずれのRarakuでも、uintはuint64型です)。

ただし今の段階では上記は参考程度に考えておき、このチュートリアルでも当面の間は整数としてint型だけを使うことにします。

real型リテラル


Rarakuではreal型(実数型)の数値リテラルを実数(型)リテラル、あるいはreal型リテラルと呼びます。

real型リテラルは以下のようにすべて10進数で記述します。 それ以外の表記は現状サポートされていません。

-1.0
0.0
0.3
19.5

C言語のように実数リテラルを「.」で始めることはRarakuではサポートされません。 例えば「0.1」を「.1」のように記述することはRarakuではできません。 また例えば「2.0e3」といったようないわゆる10のべき乗を用いた簡略表記もRarakuでは現状サポートされません。 Rarakuではこの場合、掛け算の演算子「*」とべき乗の演算子「^^」を用いて「2.0*10^^3」のように表記できます (コンパイル時に計算されるため、「2000.0」と直接書いたのと実行速度は変わりません)。 掛け算とべき乗の演算子については「算術演算子とオペランド」のセクションで詳述します。

varia文/const文の初期化においてreal型リテラルを指定する場合、型名を省略することができます(real型での宣言とみなされます)。 例えば以下では 3.5 というreal型リテラルを指定しており、real の記述を省略しています。

varia rval = 3.5

また同様にvaria文/const文の初期化においてreal型変数/定数を単独で指定する場合も、型名を省略することができます(real型での宣言とみなされます)。 例えば以下では rval の初期化において rv というreal型変数を単独で指定しており、real の記述を省略しています。

varia rv = 3
varia rval = rv /* real型変数で初期化 */

Rarakuにはreal以外にも実数型を表すreal64型、real32型が存在します。 real64型は64bit、real32型は32bitで表現できる実数値を示します。 C言語で言えば前者はdouble型、後者はfloat型に相当します。 real型はreal64型の別名であり、これらは完全に同じものです。

このチュートリアルでは当面の間は実数型としてreal型だけを使うことにします。

変数/定数の値を確認する


変数の値を確認表示するにはRrk_print関数を使います。 とりあえず Rrk_print( 変数名\n ) のように書くのが一番簡単だと思います。 定数の場合も同様に Rrk_print( 定数名\n ) とします。 例えば以下のようになります。

varia ival = 5
Rrk_print( ival\n ) /* コンソール上に 5 と表示され、改行される */

このように識別子の後ろに「括弧で囲まれた記述」が続くものを関数の呼び出し(function call)と呼びます。 上の例で「Rrk_print( ival\n )」は関数の呼び出しだけからなる文です。 関数については「ユーザ定義関数」のセクションで詳しく述べます。

変数名の後ろにある「\n」は改行を意味する文字列です(「\n」の替わりに「"\n"」と書いても全く同じ意味になります)。 これはRarakuではバックスラッシュ演算子と呼ばれるシンタックスシュガーですが、これについては「文字列」のセクションで詳しく述べます。

ここでシンタックスシュガー(syntax sugar)とは「簡易記法」といった程度の意味を表す用語です。 つまり単純に「簡易記法」と言ってもよいのですが、その言語を使うプログラマが考え出した簡易記法ではなく、 その言語自体が仕様として備えている簡易記法を意味します(後者の意味を強調したい文脈で使う用語ということです)。 ちなみにこれは和訳で「糖衣構文」と呼ばれることもあります。

尚、改行したくない場合は「\n」の替わりに「""」を付けます。

後ろに「""」といったものをわざわざ付けるのはこれを文字列として扱って欲しいことを明示するためです。 これがないとプログラマが文字列を与えるべき所に誤って整数値を与えてしまったのか、 それとも今回のように文字列に変換して欲しいという明確な目的を持って与えたのかの判断がつかなくなります。 Rarakuでは「""」がない場合は前者とみなします(その方が安全だからです)。 その他キャストを使う方法もありますが、ここではまだその説明は置いておきます。

尚、一般的なプログラミング言語と比べると、Rarakuでは比較的多くのケースで文字列連結演算子が省略でき、上記のケースもそれに相当します。

変数と定数をまとめて宣言する


前項までvaria/const文に関する基本的な説明は終りました。 ここまでの知識でも全く構わないのですが、ここから先はvaria/const文に関するシンタックスシュガーについて説明します。 好みに応じてお読みください。

varia文では、「,」で区切ることによって複数の変数をまとめて宣言することもできます。 このときキーワードvariaは文の一番初めにだけ記述します。 例えば以下の通りです。

varia i int, j int, k int

上記では、i, j, k の3つのint型変数が宣言されます。 それぞれに初期値は指定されていませんので、この場合3つともデフォルトの値である0で初期化されます。 また上記では、i, j, k の型がすべてint型ですが、この場合次のように i と j の型指定は省略して記述することもできます。

varia i, j, k int

上記では、最後の k のみが int型変数として宣言されていますが、 その情報から前二つの i と j もまた int型変数と判断されます。 そしてこの場合もまた、それぞれに初期値は指定されていませんので、この場合3つともデフォルトの値である0で初期化されます。

「,」区切りの指定でも初期値を明示的に与えることができます。 例えば以下の通りです。

varia i int=1, j int=2, k int=3

上記もまた、i, j, k の3つのint型変数が宣言されますが、今度はそれぞれに初期値が指定されています。 この場合、iは1, jは2, kは3 となります。

varia文で初期値を指定した場合、その値から型推論することにより型の明示的な指定が省略できますが、 これは「,」で区切った指定の場合でも同様です。 例えば以下の通りです。

varia i=1, j=2, k=3

上記では、それぞれに1、2、3という初期値が指定されており、この値から整数型と型推論できますので、intの指定が省略できます。

次に初期値を与えない指定とそれを明示的に与える指定を混ぜた場合を見てみます。 例えば以下の通りです。

varia i, j, k=3

上記では、i と j には型も初期値も指定されていません。 一方、最後の k においてのみ初期値3が指定されていますから k は整数型と型推論されます。 この最後に型推論された k の型より、それより手前の i と j も整数型と判断されます。 ただし、i と j には初期値は指定されていませんから、これら二つは整数型のデフォルト値 0 で初期化されます (言い換えれば k のみが 3 で初期化されるということです)。

次に異なる複数の型の変数指定を混ぜた場合を見てみます。 例えば以下の通りです。

varia i, j, k int, p, q real

上記では、i と j には型が指定されていませんが、k において int があるため、それより手前の i と j も int と判断されます。 さらに p には型が指定されていませんが、q において real があるため、それより手前の p も real と判断されます。 初期値は指定されていませんので、それぞれの型のデフォルト値で初期化されます。 すなわち i, j, k はすべて 0 となり、p, q はすべて 0.0 となります。

以下のように異なる型の数値リテラルを初期値として指定した場合も、同様に解釈されます。

varia i, j, k=3, p, q=1.5

上記では、i と j には型が指定されていませんが、k において 3 で初期化されているため k が整数型と型推論され、 これに伴って i と j も整数型と判断されます。 i と j には初期値も指定されていませんが整数型であることはわかるため、そのデフォルト値 0 で初期化されます。 さらに p には型が指定されていませんが、q において 1.5 で初期化されているため、q が実数型と型推論され、 これに伴って p も実数型と判断されます。 p には初期値も指定されていませんが実数型であることはわかるため、そのデフォルト値 0.0 で初期化されます。 結局上記は以下のように記述したのと全く同じ結果になります。

varia i=0, j=0, k=3, p=0.0, q=1.5

Rarakuでは一つの文内に改行やタブを入れてもその意味を一切変えません。 例えば上の例は以下のように書くこともできます。

varia
	i=0,
	j=0,
	k=3,
	p=0.0,
	q=1.5

今回は数行にも渡っていますがこれで一つの文になり、「q=1.5」とあるところで文が終了します (直後に「,」がないからです)。

「,」は一番最後の指定の直後にも付加することができます。 ただしその場合は「,」の後ろにさらに「;」が必須になります。 例えば以下のような記述が可能です。

varia
	i=1,
	j=2,
	k=3,
	p=1.2,
	q=1.5,
;	

上記では、一番最後の指定である「q=1.5」の後ろに「,」を付加しています。 「,」を一番最後にも付加することにより、 これらの指定を後で追加したり削除したりする場合、単に該当行を修正するだけで済みます。

この「単に該当行を修正するだけで済む」に関して、 もう少し具体的な説明が欲しいという方のために、少しシミュレーションしてみましょう。

今仮にこの「q=1.5」の後ろには「,」を付加できない仕様であったとしましょう。 その場合、例えばその後ろに新しく「r=1.7」を追加しようとするとき、まず「q=1.5」と「r=1.7」の間に「,」を忘れずに入れる必要があります。 さらに「r=1.7」の後ろには逆に「,」を付加しないよう留意しなければなりません。 一方、「,」を一番最後の指定の後ろにも記述できる仕様であれば、 そのような考慮をする必要が一切なく、以下のように単に「r=1.7,」を後ろに追加するだけで済みます。

varia
	i=1,
	j=2,
	k=3,
	p=1.2,
	q=1.5,
	r=1.7, /* これを追加 */
;	

逆に「r=1.7,」が不要ならば単にその一行を消すだけで修正は正しく完了します。

ここまですべての指定を一つのvaria文の中に記述してきました。 このようにするとキーワードvariaを書く手間が省けるのでその分わずかに楽ができますが、 一方でこれを後で読み直したときに読みやすいかどうかも考慮する必要があります。 これを一般に可読性と呼びます。 またコードが読みやすい状態を可読性が高い、コードを読みやすくするように修正することを可読性を上げるなどと呼びます。

そしてもしこのように書くことで読みにくくなる(可読性が下がる)ようであれば、無理に一つのvaria文の中ですべての指定を行わない方がよいでしょう。 より読みやすい記述となるように工夫してみてください。 例えば以下のように型が違う(あるいは意味的に別グループとみなせる等でもよいですが)ならば、 それを目立たせる意味で別のvaria文に分けるのもよいでしょう。

varia
	i=1,
	j=2,
	k=3,
;
varia
	p=1.2,
	q=1.5,
	r=1.7,
;	

あるいは以下のように同じ型は一つの行で記述するというのでもよいかもしれません。

varia
	i, j, k int,
	p, q, r real,
;	

Go言語では(関数内において)「:=」演算子を使って「var」(Rarakuで言うところの「varia」)を完全に省略した文を記述ができますが、 Rarakuでは敢えてそのような記法を用意していません。 これは変数を宣言する文というものは(たとえそれが関数の中のささいな変数でも)それなりに目立たせるべきとRarakuでは考えるからです。 これまで見てきたように「,」を使えば(文開始のvariaというキーワードを最低一つは残しつつ)記述の手間もそれなりに省くこともできます。

ただし後のセクションで述べるif文やfor文においては特例としてGoと同様に「:=」を使った記述ができる場合があります。

ここまでvaria文のみを取上げてきましたが、const文でも同様に「,」区切りで指定ができます。 ただしconst文の場合はすべての指定に必ず明示的な初期化が必要です。 例えば以下のようになります。

const
	ci=0, cj=1, ck=2,
	cp=1.1, cq=2.2, cr=3.3,
;	

一方、必ず明示的な初期化が必要ということは、const文では以下のような記述はできないことになります。

const
	ci, cj, ck=2, /* コンパイルエラー: ciとcjが明示的に初期化されていない */
;	

仮にciとcjとckすべてを同じ2で初期化したい場合、以下のように記述しなればなりません(見やすいように改行を入れてあります)。

const
	ci=2,
	cj=2,
	ck=2,
; /* OK */	

マジックナンバー(数字の直書き)の重複を避けたければ、既に前方で宣言した定数を右辺値として使い、以下のように記述することもできます。

const
	cubic_leng=2,
	ci=cubic_leng,
	cj=cubic_leng,
	ck=cubic_leng,
; /* OK */	



C言語等とは異なり、Rarakuの文では極少数の例外を除き、基本的に文末に「;」を付ける必要はありません(付けてもよいですが)。 例外的に「;」が必須なケースについてはそれが出てきた時点できちんと説明しますが、主なものを一覧にすると以下の場合になります。

  • varia/const/tight/refer/final文において複数の宣言をまとめて記述し、さらに「,」を文末に書きたい場合
  • インクリメント/デクリメント演算子だけからなる文
  • switch文のcaseでnoop(何もしない文)を明示化したい場合

また通常ほぼ発生しないケースですが以下の場合も必要になります。

  • 戻り値のないreturn文の直後にさらに無名ブロックが来るような状況(構造体の初期化子と区別する必要があります)
  • global関数宣言の直後に無名ブロックが来るような状況(関数の定義と区別する必要があります)
  • 引数の無いsetjmp/longjmp文において、直後に識別子で始まる文が来る場合 (ただしそもそもsetjmp/longjmp文を使うこと自体が希で通常は替わりにtry文を使うことになるとは思います)

また別に文末というわけではありませんが、以下の場合、構文上のデリミタとして必須になります。

  • for文の初期化文、条件式、後処理文を区切る二つの「;」
  • if文で「:=」による変数宣言を行った場合、それと条件式を区切る「;」


Rarakuでは文と文の切れ目は自動的に判断されます。 これまで見てきたように通常は見易さのため、文と文の間には改行を入れて記述します。 例えば以下の通りです。

varia i = 8
varia j = 9

しかしながら、なんらかの理由で改行を入れたくない場合、それを入れずに書くことも可能です。

ただし後のセクションで述べる「//によるコメント」や「#で始まる文」は例外的に文末に改行が必要となります。

例えば以下は上記と全く同じ意味になります。

varia i = 8 varia j = 9

ただこのように1行で書く場合、以下のように文と文の間に「;」を入れた方が少し見易いかもしれません。

varia i = 8; varia j = 9

しかし繰り返しますが、特に理由がないなら上記のように1行にまとめて書くべきではありません。 通常は見易さのため、改行を入れるべきです。

参考: 改行を入れない記述を許可している理由
Rarakuでは文と文の間に改行を入れていない場合でも、コンパイルエラーとまではなりません。 この理由は、Rarakuではプログラマが直接読むことを想定していない機械的なコード生成としての用途も想定しているからです。

Rarakuではこのような機械的に生成されたコードをCodeAtlasと呼んでいます。 例えばlibRrkではRarakuBIRDと呼ばれる「HTML内にRarakuのコードを書ける機能」を提供していますが、 これはCodeAtlasによって実現されています。

例えばRarakuを使って別の新しいスクリプト言語を開発するような用途を考えます。 今この(架空の)スクリプト言語をMyScriptとしましょう。

簡単のためMyScriptは1行につき一つの文を書く文法であるとします。 またMyScriptを最終的にRarakuのソースコードに自動生成して実行するものとします。 例えばMyScriptで「VECTOR( 8, 9 )」と記述した場合、以下のようなRarakuのソースコードに自動生成して実行するものとします。

varia x = 8
varia y = 9

MyScriptの表記に誤りを入れてしまった場合に備え、コンパイルエラーも表示させたいとしましょう。 コンパイルエラーの処理についてはRarakuに任せるものとします。

上記のように元々1行であったものを2行に変換してしまうと、 コンパイルエラー時の行表示に狂いが生じてしまいます。 そこで、以下のように1行にまとめて書いた形に自動生成すれば、この行表示の狂いを防止できるというわけです (もっともこの例の場合、配列や構造体を使うなど他にもいくらでも代替手段はあります)。

varia x = 8 varia y = 9

実はRarakuでは、C言語のプリプロセッサにおける「#line」文に相当するものも用意されており、 それを使っても上記の問題に対処できますが、話が脱線しすぎるのでそれについての説明は割愛します。
上記のように一行で複数の文を書く場合、Swiftなどでは文と文の間に「;」の指定が必須となりますが、 Rarakuではこれは必須ではありません。 コードの自動生成という観点ではその方がコーディングがほんの少し簡単になると思われるためです。

ただしくどいようですが、これはあくまで自動生成されたコード(通常は人が読むことがない類のコード)での特別措置であり、 通常のコードでは、ほとんどの場合、改行を入れるべきです。

Close

varia文とconst文と代入文のまとめ


変数や定数の宣言は最も基本的な処理対象の使用を宣言するものになります。 この宣言が行われていないものを以降の処理で扱うことは禁止されます。 これにより、以降の処理で変数や定数名をタイピングミスしたとしても それをすぐに検出することが可能になります。

また変数では値を再代入によって変更できる分柔軟性はありますが、値の変遷を把握しなければならない分安全性は下がります。 一方、定数では再代入が禁じられる分柔軟性は下がりますが、考慮すべき労力が削減される分安全性は上がります。

プログラミングではこのように、「なんらかの自由を与えるもの」と逆に「なんらかの制限を加えるもの」のトレードオフとバランスが重要です。



目次に戻る

bool型とif文

bool型の宣言


Rarakuにおいて真偽を示す型をbool型と呼びます(booleanの略です)。 このbool型変数は true または false の二通りの値しかとりません。

以下の例では bool型の変数の宣言を行っています。

varia bval bool

上の例では初期値を明示していませんが、この場合bool型ではfalseで初期化されます。

以下のように初期値を明示的に指定することもできます。

varia bval bool = true /* 変数bvalを宣言し、初期値としてtrueを指定 */

boolにおける代入や定数の宣言もintの場合と全く同様です。

true または false をbool型リテラルと呼びます。 varia文/const文の初期化においてbool型リテラルを指定する場合、型名を省略することができます(bool型での宣言とみなされます)。 例えば以下では true というbool型リテラルを指定しており、bool の記述を省略しています。

varia bval = true

また同様に右辺にbool型変数/定数を指定する場合も、型名を省略することができます(bool型での宣言とみなされます)。 例えば以下では bval の初期化において、その右辺に bv というbool型変数を指定しており、bool の記述を省略しています。

varia bv = true
varia bval = bv /* bool型変数で初期化 */

結果がbool型となるような式のことを条件式と言います。 条件式は次に述べるif文などにおいて、処理を分岐する条件の記述手段として使用されます。

if文


プログラミングではある条件が成立する場合のみ、特定の処理をしたい場合があります。 そのような場合if文を使います。 これは「if 条件式 { ... }」といった書式で記述します。

if文に与えた条件式の値が true の場合のみ、{ ... } 内の処理が実行されます。 以下の例をご覧ください。

varia ival int = 5
varia bval bool = true
if bval {
	ival = 7
}
Rrk_print( ival\n )

上の例では 「if bval {」から「}」までの3行が一つのif文です。 このように一つの文が複数行に跨ることもあります。 また { ... } 内にはif文とは別の文(「ival = 7」という代入文)が入れ子構造になって含まれています。 このように一つの文の中に他の文が含まれることもあります。

このような入れ子構造では見やすさ向上のため、慣習的に「ival = 7」の前にタブ文字を入れて開始の位置を後ろへズラします。 これを字下げインデントなどと呼びます。 Rarakuではインデントがなくともコンパイルエラーにまではなりませんが、やはりコードは見辛くなります。 特別な理由がない限り、入れ子構造ではその中身を常にインデントしておくべきです。

タブ文字ではなく半角スペースを何個も置いてインデントする流派もあります(多くの場合、おそらくタブ文字では見かけの字幅が広すぎるからという理由だと思います)。 しかしながら、いまどきのテキストエディタではタブ文字の見かけの字幅をカスタマイズできますので、 タブ文字でインデントしておき、必要ならそれをお好きな字幅にして表示するのが結局のところ一番柔軟であるように思えます。 ちなみに筆者が使っているテキストエディタ(Vimを使っておりますが)では、設定ファイルをカスタマイズしタブ文字の見かけの字幅を4としてあります。

上の例では bval の値は true となっていますので、中身の「ival = 7」が実行され、最後の Rrk_print( ival\n ) では 7 と表示されることになります。

bval の値が false の場合、{ ... } の中身は実行されません。 しかしだからと言って、{ ... } の中身にどんなデタラメな文字列を書いてもよいということにはなりません。 例えば以下をご覧ください。

varia ival int = 5
varia bval bool = false
if bval {
	ivvv = 7
}

上の例では、bval が false となっていますので if文における { ... } 内は実行されません。 しかしこの例では { ... } の中身で変数名をスペルミスし、本来は「ival = 7」と書かなければならないところを「ivvv = 7」と記述しています。 このとき、Rarakuは次のようなコンパイルエラーを出します。

The identifier [ivvv] may not exist or may be accessed before the declarative statement is executed.

つまりたとえ { ... } の中身が実際には実行されないとしても、その中身に文法的に問題がないかどうかはきちんとチェックされるということです。

ただ場合によってはこのような制限を回避して、コード中に完全に自由な文字列を書きたいこともあります。 例えばコードの意味を説明する注釈などを自然言語(人間が使う言語)で記述したい場合等です。 そのためにはコメントと呼ばれるものを使いますが、それについては「コメントとコメントアウト」のセクションで詳しく述べます。

参考: C言語のif文との違い
この「参考」はC言語をご存知の方のみお読みください。

Rarakuのif文とC言語のif文の違いは以下になります。
  • Rarakuのif文では「{」と「}」を省略できない。

  • たとえif文の中身が一つの文しかない場合でも必ず「{」と「}」を記述する必要があります。 このような仕様にすることで、「dangling else」と呼ばれる問題を解消できます。

  • Rarakuのif文では、条件式を囲う「(」と「)」を記述する必要がない。

  • この辺りの仕様はGoに似ています。

    ただC言語のソースコードからRarakuのコードへ移植する場合等で、 条件式を囲う括弧は面倒なのでとりあえず付けたままにしておくといったようなことは可能です (それは結局括弧付きの条件式を与えていることになります)。 例えば以下は、bvalがbool型やint型であれば問題ありません。

    varia bval bool = true
    if ( bval ) {
    	Rrk_print( bval\n )
    }
    

    ただしすぐ後に述べますが、全体を「(」と「)」で囲うことができない特別なケースも存在するため、 括弧は付けないものだと考えておいた方が確実ではあります。

  • Rarakuのif文では、条件式の部分に(「=」を使った)代入処理を書くことはできない (Rarakuでは代入は「文」となり、それ自体値を持たない)。

  • このような仕様にすることで、「==」を勢い余って(勢い足らず?)「=」と書いてしまうようなコーディングミスは Rarakuではコンパイルエラーとして確実に検出できることになります。

  • 「:=」を使った変数宣言付きのif文では、(変数宣言を含めた)全体を「(」と「)」で囲うことはできない。

    Rarakuのif文では、条件式の直前の部分に「:=」を使った変数宣言を行い、 尚且つif文の実際の条件式をその後ろに書くといったことができます(この場合、変数宣言と条件式の間には「;」が必須となります)。 この仕様はGoに似ています。 例えば以下の通りです(あまり有用性を感じない例ですが)。

    if bval:=true; bval {
    	Rrk_print( bval\n )
    }
    

    ただしこの場合、以下のように(変数宣言を含めた)全体を「(」と「)」で囲うことはできません。

    if( bval:=true; bval ){ /* NG: compile error! */
    	Rrk_print( bval\n )
    }
    



    参考: (変数宣言を含めた)全体を「(」と「)」で囲うことができない理由
    この理由は実装上の都合によるところが大きいです。

    仮に(変数宣言を含めた)全体を「(」と「)」で囲うことを許可すると、括弧の中身に変数宣言が存在する可能性を考慮しなければならなくなり、 括弧全体が単なる条件式ではなくなります。

    それでも「ifの直後に括弧があるなら、最後にそれに対応する括弧が付く」というルールで構文解析すれば問題なさそうに見えますが、 そのルールでは、条件式が括弧で始まるような複雑なものであった場合、これが「(変数宣言を含めた)全体を囲うための括弧」なのか「条件式の括弧」なのかを この括弧の近傍だけで判別することができなくなってしまいます。 例えば以下のようなケースです(まだ説明していない比較演算子や論理演算子等が登場していますが、その意味はC言語と同じです)。

    varia major_ver = 4 
    varia minor_ver = 4 
    if  ( major_ver == 4 && minor_ver >= 4  )
     || ( major_ver >= 5 && major_ver <= 13 )
    {
    	Rrk_print( major_ver\n )
    }
    

    このルールで解析した場合、上記では「||」が登場するまで判別不能になります。 このように判別を保留する手法は構文解析を複雑化させるため、Rarakuでもこのような処理はできるだけ避けています。

    そのような理由もあり、Rarakuでは(変数宣言を含めた)全体を「(」と「)」で囲うことができなくなっています。

    Close


    この記法は例えばファイルをオープンする場合など戻り値をチェックした上でif文で分岐するような処理を記述するのに便利ですので、 「ファイル入出力とハンドラ型」のセクションにおいて改めて詳述します。

  • if文の条件式にnotnull指定を与える場合、条件式全体を「(」と「)」で囲うことはできない。

    Rarakuにも無効値を表すnull(C言語で言うところのNULL)が存在しますが、 いわゆるnull安全(null-safe)を実現するため、通常の型の変数にnullを代入することが許可されません。 nullを代入可能な変数を宣言したい場合、その型の後ろに「^?」をつける必要があり、 これをnullable変数と呼びます。またこれと対比する文脈において、通常の変数をnotnull変数と呼びます。

    nullable変数からnotnull変数へ変換するには、if文でnotnullを指定しなければなりません。 詳しくは「nullableとnotnull」のセクションで解説しますが、Rarakuでは以下のような記述が存在します。

    varia str string^? = null
    if notnull str {
    	/* str is notnull in this block */
    }
    

    この場合のif文の条件式も全体を「(」と「)」で囲うことはできません。

Close


参考: if文の条件式に整数を与えた場合
一般にbool型変数にint型の値を代入することはできませんが、 if文(並びに後述するelif文でも同様ですが)の条件式では、例外的にint型の数値を指定できます。 例えば以下の例をご覧ください。

varia ival int = 5
if ival {
	Rrk_print( ival\n )
}

ここでivalはbool型ではなくint型ですが、if文の条件式に単独でint型の数値を与えた場合、これが特別な条件式として機能します。 この場合、ivalが非ゼロであれば true、ゼロであれば false となる条件式とみなされます。 つまり上記は以下のように書いたのと全く同じです。

varia ival int = 5
if ival != 0 {
	Rrk_print( ival\n )
}

整数値以外にも配列変数や構造体変数やnatvobj型を指定することもでき、その場合も全く同様に機能します。 配列変数については「配列とモディファイア」のセクションで、 構造体変数については「構造体」のセクションで、 natvobj型については「ファイル入出力とネイティブオブジェクト型」のセクションで詳しく述べます。

Close


else文


if文の条件式が true の場合と false の場合それぞれで、別々の処理を実行させたい場合があります。 そのような場合、if文だけですと次のように記述しなければなりません。

varia ival int = 5
varia bval bool = false
if bval {
	ival = 7
}
if !bval {
	ival = 9
}

上記の例では最初のif文の条件式 bval の値は false となりますから、最初のif文の中身「ival = 7」は実行されません。 次のif文の条件式は「!bval」となっています。この頭についている「!」は論理否定と呼ばれるもので、 bool型条件式の値を反転させます。つまりbvalの値がtrueのときはそれをfalseに変え、falseのときはそれをtrueに変えます。 これにより「!bval」の値はtrueになり、2番目のif文の中身「ival = 9」は実行されることになります。

else文を使うとこのような処理をもう少しスッキリ書くことができます。 else文はif文の後ろに連結して記述し、if文の中身が実行されなかった場合に替わりに実行すべき処理をelse文の中身として記述します。 例えば以下は上記と全く同じ処理をelse文を使って書いたものです。

varia ival int = 5
varia bval bool = false
if bval {
	ival = 7
} else {
	ival = 9
}

if文とは異なり、else文では条件式を記述する部分はなく、else の直後に { ... } を記述します。

else文の「else」とは「さもなくば」といった意味と解釈してよいでしょう。 つまり「既に挙がっている条件のどれでもない場合は」ということですから、 おのずと残りの条件は決まり、そもそも明示的に条件式を指定する必要性そのものがないわけです。

C言語のelse文とは異なり、Rarakuのelse文では「{」と「}」を省略することはできません。 たとえelse文の中身が一つの文しかない場合でも必ず「{」と「}」を記述する必要があります。 このような仕様にすることで、「dangling else」と呼ばれる問題を解消できます。

Rarakuでは「} else {」という3つのトークンの並びのシンタックスシュガーとして「}:{」と記述することもできます。 例えば上の例は以下のように書くこともできます。

varia ival int = 5
varia bval bool = false
if bval {
	ival = 7
}:{
	ival = 9
}


elif文


true の場合の処理と false の場合の処理といった二者択一ではなく、もっと多くの選択肢から一つの処理を実行させたい場合があります。 例えば整数ivalが 0より大きい場合、0にちょうど等しい場合、0より小さい場合の3通りについて、それぞれ別の処理をさせたいとします。 そのような場合、if文とelse文だけで書く縛りを入れますと次のように記述しなければなりません。

varia ival = 5
if ival > 0 {
	Rrk_print( "This is positive number.\n" )
} else {
	if ival == 0 {
		Rrk_print( "This is zero.\n" )
	} else {
		Rrk_print( "This is negative number.\n" )
	}
}

上の例では、else 文の中身にさらに別の if文とelse文を含ませています。 このように if文やelse文の中身にさらに別のif文とelse文を入れ子構造で書くこともできます。

ここで「ival > 0」という条件式は「ival の値が 0 よりも大きいか否か」という意味になります(数学における「>」と同じです)。 大きければその値は true となりますし、さもなくば(等しいか小さければ)その値は false となります。 また「ival == 0」という条件式は「ival の値が 0 とちょうど等しいか否か」という意味になります(数学における「=」と同じです)。 等しいならばその値は true となりますし、さもなくばその値は false となります。 最後のelseに到達した場合、上で篩いにかけたどの条件にも当てはまりませんが、これが「ival の値が 0 よりも小さい」場合になります。

Rrk_print( "任意の文字列" ) と記述することで、任意の文字列をコンソール上に表示できます。 この例では、ivalが 0より大きい場合は「This is positive number.」と表示し、 0にちょうど等しい場合は「This is zero.」と表示し、 最後に 0より小さい場合は「This is negative number.」と表示します。

elif文を使うとこのような処理をもう少しスッキリ書くことができます。 elif文はif文の後ろに連結して記述し、if文の中身が実行されなかった場合に限り次のif文を実行します。 これはちょうど else と if を合体させたような働きをすることから、それを合体した「elif」というような名前になっています 例えば以下は上記と全く同じ処理をelif文を使って書いたものです。

varia ival = 5
if ival > 0 {
	Rrk_print( "This is positive number.\n" )
} elif ival == 0 {
	Rrk_print( "This is zero.\n" )
} else {
	Rrk_print( "This is negative number.\n" )
}

上の例ではまず最初のif文における条件式「ival > 0」が実行され、その値がtrueかfalseかが考慮されます (これを条件式が評価されるとも言います)。 条件式が評価された結果、それが true ならば if文における中身が実行され、その後にある elif文とelse文は実行されません

if文における条件式が false の場合に限り、次にelif文における条件式「ival == 0」が評価されます。 条件式が評価された結果、それが true ならば elif文における中身が実行され、その後にある else文は実行されません

if文やelif文におけるすべての条件式が falseの場合に限り、最後のelse文の中身が実行されます。 この場合、上で篩いにかけたどの条件にも当てはまらないケース、すなわち ival が 0 よりも小さい場合になります。

このようにして上記3つのケースのいずれか一つのみを選択的に実行させることができるわけです。 尚、この選択肢の数が4つや5つになっても全く同様で、その場合は中間にあるelif文を増やします。 例えば4つの場合、if文、elif文、elif文、else文といった順番になります。

ちなみに「elif」を何と発音するかですが、「エリフ」でいいんじゃないかと思います)。

if文、elif文、else文の中身は空(文が一つも記述されていない状態)にしても構いません。 例えば以下の例ではif文の条件式がtrueの場合、何も行われません。

varia ival = 5
if ival > 0 {
} elif ival == 0 {
	Rrk_print( "This is zero.\n" )
} else {
	Rrk_print( "This is negative number.\n" )
}

特にelse文にあたる処理が必要ない場合はelse文の記述そのものを省略することもできます。 例えば以下の通りです。

varia ival = 5
if ival > 0 {
	Rrk_print( "This is positive number.\n" )
} elif ival == 0 {
	Rrk_print( "This is zero.\n" )
}

しかしelse文にあたる処理が必要ない場合でも、敢えてelse文の枠組みだけを残して記述することもできます。 例えば以下の通りです。

varia ival = 5
if ival > 0 {
	Rrk_print( "This is positive number.\n" )
} elif ival == 0 {
	Rrk_print( "This is zero.\n" )
} else {
}

このとき else 文の中身は空であり、結局else文が最初から存在していないのと同じですが、 ブログラマがelse文を書き忘れたわけではないことを明示するため、敢えてこのような書き方をする場合もあります。

参考: C言語でのelse ifと書くこととの違い
この「参考」はC言語をご存知の方のみお読みください。

RarakuではC言語のように「else if」と記述することはできません。 そのような場合必ず「elif」を使用します。 さらにRarakuのelif文では、その中身を囲う「{」と「}」を省略することはできません。 このような仕様にすることで、「dangling else」と呼ばれる問題を解消できます。

一方、elif文の条件式を囲う「(」と「)」は記述する必要がありません。 ただC言語のソースコードからRarakuのコードへ移植する場合等で、 (else ifの部分など直す必要がある部分は勿論直さなければなりませんが)条件式を囲う括弧は面倒なのでとりあえず付けたままにしておく といったようなことは可能です(それは結局括弧付きの条件式を与えていることになります)。 例えば以下は問題ありません(移植作業中の状況を想定して敢えてC言語風に文末に「;」を付けてありますが、勿論これは省略できます)。

varia ival = 5;
if( ival > 0 ){
	Rrk_print( "This is positive number.\n" );
} elif( ival == 0 ){
	Rrk_print( "This is zero.\n" );
} else {
}

ただし elif文においても、if文と同様、条件式の直前の部分に「:=」を使った変数宣言やnotnull指定を行うことができ、 この場合(変数宣言を含む)全体を括弧で囲うことはできません。 そのため、elif文においても括弧は付けないものだと考えておいた方が確実ではあります。

Close


参考: elif文のシンタックスシュガー
Rarakuでは「} elif 条件式」という記述のシンタックスシュガーとして「}: 条件式」と記述することもできます。

より正確には「:」の直後のトークンが「{」以外である場合に限り、「} elif」と解釈されます。 一方「:」の直後のトークンが「{」である場合は、「} else {」と解釈されます。

上の例は(elseのシンタックスシュガーも合わせて用いれば)以下のように書くこともできます。

varia ival = 5
if ival > 0 {
	Rrk_print( "This is positive number.\n" )
}: ival == 0 {
	Rrk_print( "This is zero.\n" )
}:{}

elifを使ったのと文字数は大して変わりませんが、elifについてこのような記法を用意したのは、 elseのシンタックスシュガーである「}:{」と統一性を持たせたいことが理由の一つです。

またもう一つの理由は、このようにした場合、条件式の始まる列の位置を(if文の側で特にスペースを挿入して調整することなく) すべて同じに揃えることができます。 上の例では「ival > 0」と「ival == 0」の始まる列の位置が同じになっています。 筆者の経験上、このように似たような記述が複数行に渡り並ぶ場合、誤りを誘発しやすいと考えられる部分の列の位置を揃えた方が 万一そこで何か記述ミスした場合に、目視でそれを発見できる確率が上がるように思えます。

ただ実際のところ、列を揃えるという目的にしても、最初のif文にせいぜい数個のスペースを入れるだけで済むわけです。 またコロン「:」は、セミコロン「;」と字面上ちょっと紛らわしいという問題はあります(Rarakuでは「;」はあまり登場しませんけれども)。 そう考えると結局このようなシンタックスシュガーの是非は、単なる好みの問題のようにも思います。

ちなみに以下のようにelifだけを「:」を使って書いたり、elseだけを「:」を使って書いたりしても構いません (ただしifについてはこのようなシンタックスシュガーはありません)。

varia ival = 5

if ival > 0 {
	Rrk_print( "This is positive number.\n" )
}: ival == 0 {
	Rrk_print( "This is zero.\n" )
} else {
}

if     ival > 0 {
	Rrk_print( "This is positive number.\n" )
} elif ival == 0 {
	Rrk_print( "This is zero.\n" )
}:{}

尚、この記事ではこれ以降、このシンタックスシュガーを使わずelifとelseを用いる場合もあります。 例えばコード中のコロン「:」を指してそれをelifとかelseと呼んで解説するのも分かりにくいと思われる場合などです。

Close


if文、else文、elif文の { ... } の部分をブロックと呼びます。 ブロックには複数の文を記述でき、それらを一つのまとまりとしてコードの中で区分します。 ブロックの詳しい働きについては後の「ブロックとスコープ」のセクションで詳しく述べます。

bool型とif文のまとめ


if文を使うことで条件に応じて別の処理を行うように切り分けることができます。 この条件分岐処理は、プログラムに知的な仕事をさせるための最初の一歩となります。



目次に戻る

コメントとコメントアウト

基本事項


コメントとはプログラムの実行に一切影響を及ぼさない文字列、あるいはそれらが書かれた範囲のことです。 このコメント部分にはいかなる文字列を書いても構いません(全角文字を記述することもでき、文字コードも問いません)。 この性質を利用し、例えばプログラマがそのコードの意味について説明した文章(これを注釈と呼びます)をそこに書くなどができます。 あるいはコードの一部を一時的に無効化するなどができます。

このコードの無効化を特にコメントアウトと呼びます。 またこの無効化を行うことをコメントアウトするといいます。

注釈としてのコメントと、コードの無効化としてのコメントアウトを用語として明確に区別することもあります。

Rarakuにおいてコメントアウトを行うには「/*」と「*/」でコメントアウトしたい範囲を囲みます。 あるいは行頭に「//」や「#!」や「# 」(#の直後に半角スペース)を記述すると行末までがコメントアウトされます。

「# 」の場合、より正確には#の直後の半角スペースは、タブ文字や改行コードでも構いません。

例えば以下のようになります。

varia i int
/*
この範囲はコメント
varia j int
*/
varia k int /* この行の後半はコメント */
//varia l int この行はコメント
# varia m int この行はコメント

上記の例で、それぞれの行の有効/無効は以下のようになります。

  • varia i int と書かれた行
  • コメントアウトされておらず有効。
  • varia j int と書かれた行
  • コメントアウトされており無効。
  • varia k int と書かれた行
  • 後半はコメントアウトされており無効。
  • varia l int と書かれた行
  • コメントアウトされており無効。
  • varia m int と書かれた行
  • コメントアウトされており無効。

従ってこれらをまとめますと、上記は実質的には以下のように記述したのと全く同じことになります。

varia i int
varia k int

「/*」と「*/」を使ったコメントの内部に「//」や「# 」を記述してもそれは単に無視されます。 たとえば次のような例では「//」より「/*」と「*/」の効果が優先され、(「*/」の直後にある)varia k int と書かれた文は有効になります。

/* varia i int
//varia j int */ varia k int

一方、先に「//」が来て、その行内に「/*」と「*/」を使ったコメントが入り込んでいる場合、「//」の効果の方が優先されます。 例えば、以下では varia k int と書かれた文も含めコメントアウトされます。

// /* varia j int */ varia k int

#ifを使ったコメントのネスト


「/*」と「*/」を使ったコメントをネストさせることはできません。

たとえば次のような例では1行目の「/*」でコメントが始まり、2行目のvaria i int の行末の時点で「*/」が現れますので その時点でコメントが終了します。 結果的に3行目に記述された「*/」は不正な文字列として残り、コンパイルエラーとなります。

/*
/* varia i int */
*/

このようなコメントアウトのネストを行いたい場合、「/*」「*/」の替わりに「#if 0」と「#endif」を使うことができます (この記述はC言語の慣用的な記法に倣ったものです)。 例えば以下のようになります。

#if 0
この範囲はコメントアウトされている
/* varia i int */
#endif
varia j int

「#if 0」や「#endif」を記述した行において、「#」記号より前にその他の余計な文字を挿入してはいけません。 ただし「#」記号の前に半角スペースやタブ文字を挿入してインデントするのは構いません。 また「#」記号と「if」や「endif」の間に(半角スペース等を含め)いかなる文字も含めてはいけません ((C言語のプリプロセッサとは異なり)「#if」や「#endif」で一つのトークンということです)。 「0」や「#endif」の後ろにコメント等を記述することは可能ですが、その場合、半角スペースを最低一つ以上挟む必要があります。

#if 0 /* for experimental implementation */
…
#endif

「#if 0」と「#endif」では、これら自体をネストして使うこともできます。 例えば以下の通りです。

#if 0
	#if 0
	…
	#endif
#endif

上記ではまずネストの内側にある「#if 0」と「#endif」の対応でコメントアウトされ、 さらにその外側にある「#if 0」と「#endif」の対応でもコメントアウトされています。

「#*」と「*#」を使ったコメント


「/*」と「*/」の替わりに「#*」と「*#」を使うこともできます。 というよりこの記法は「/*」と「*/」にいくつかの制限を加え、多少安全にしたものです。

この記法は開始パターンである「#*」が行頭にあるか否かで大きく二つに分けて考えます。

  • 開始パターン「#*」が行頭にある形式

  • ここで言う行頭とは、「#*」が本当に1カラム目から始まるか、 あるいは「#*」の前に半角スペースやタブ文字でインデントされている状態を意味します (「#*」より前のカラムにその他の式や文などが含まれていてはいけません)。

    この形式では、中身のコメントに改行を入れると 終了パターンである「*#」も(「#*」の時と同様の意味で)行頭に置かなければなりません。 さもなければコンパイルエラーとなります。

    一方、中身のコメントに改行がない場合は、 終了パターンである「*#」はどこに置いても構いません。

    例えば以下の通りです。

    #*
    	Multiline 
    	comments.
    *#
    #* Multiline 
    	comments.
    *#
    
    	#*
    		Multiline 
    		comments
    		with indent.
    	*#
    		#*
    		 * Multiline 
    		 * comments
    		 * with indent.
    		*#
    	#***
    	 * Multiline 
    	 * comments
    	 * with indent.
    	*#
    
    #* One-line comments. *#
    #* There is something after me. *# varia i=0
    

  • 開始パターン「#*」が行頭ではない形式

  • 「#*」が行頭ではない場合、この形式になります。

    この形式では、開始パターンである「#*」と終了パターンである「*#」は同じ行にある必要があります。 例えば以下の通りです。

    
    varia j=0 #* There is something before me. *#
    
    varia k=0 #* There is something before and after me. *# varia l=0
    

    仮にこの形式で、中身のコメントに改行を入れるとコンパイルエラーとなります。 例えば以下のような記述は許可されません。

    varia i=0 #* This is
       Compile error! *#
    
    varia i=0 #* This is
       Compile error! *# varia j=0
    
    varia i=0 #* This is
       Compile error!
    *#
    


「#*」と「*#」では、(終了パターンである)「*#」をコメント内部に含めることができません。 それを含めた地点でコメント本体が終了してしまうからです。 これを含める必要がある場合、替わりに「#=*」と「*=#」で囲います(「#」と「*」の間に一つ「=」を入れます)。 例えば以下の通りです。

#=*
	Comment containing "*#".
*=#

ただし「#=*」と「*=#」を使ったコメントでは、今度は(終了パターンである)「*=#」をコメント内部に含めることができなくなります。 さらにこれを含める必要がある場合、替わりに「#==*」と「*==#」で囲います(「#」と「*」の間に二つ「=」を入れます)。 例えば以下の通りです。

#==*
	Comment containing "*#".
	Comment containing "*=#".
*==#

以下同様に「=」の個数を必要に応じて増やすことでいかなる文字列もコメント内部に含めることが可能になります。 このとき開始パターンにおける「=」の個数と 終了パターンにおける「=」の個数は必ず同じでなければなりません。

これを利用するとコメントのネストと似たようなことも可能です。 例えば以下の通りです。

#=*
  * An example of a Raraku code is shown below.
  *
  *   #* Declare of i. *#
  *   varia i=0
  *
  *   #* print i. *#
  *   Rrk_print( i\n )
*=#
varia j=0



#ifを使ったコメントのON/OFF


「#if 0」を「#if 1」に変えることでコメントアウトを解除することができます

#if 1 /* for experimental implementation */
…
#endif

これは「0」を「1」に変えるだけで済むため、「#if 0」と「#endif」を直接消去するよりも楽です。 何らかのテストのためコメントアウトのON/OFFを頻繁に行う必要がある場合に便利です。

あまり使うことはないかもしれませんが、「#else」を使うことにより、以下のような選択的なコメントアウトを行うこともできます。

#if 1 /* for experimental implementation */
	こちらはコメントアウトされていない(有効)
#else
	こちらはコメントアウトされている(無効)
#endif

上の例では「#if 1」と「#else」までの範囲はコメントアウトされておらず、 「#else」と「#endif」までの範囲はコメントアウトされています。 ここで以下のように「#if 1」を「#if 0」に変えるとこの対応が逆になります。

#if 0 /* for experimental implementation */
	こちらはコメントアウトされている(無効)
#else
	こちらはコメントアウトされていない(有効)
#endif

上の例では「#if 0」と「#else」までの範囲はコメントアウトされ、 「#else」と「#endif」までの範囲はコメントアウトされていません。

尚、C言語のプリプロセッサとは異なり、この表記はあくまで擬似的なものであるため、 たとえば「#if TARGET」のようにマクロ変数を指定するなどといった本格的なことはできません。 また「#elif」と書くことは現状のRarakuではできません。

参考: #line文による「コンパイルエラー」時の行表示の強制変更
以下はほとんどの方には関係のない機能の話ですので、興味がある方だけお読みください。

Rarakuでは「#line 行番号」という書式で、いわゆるコンパイルエラー時の行表示を強制変更することができます。 C言語のプリプロセッサにおける「#line文」をご存知の方はそれと同じです。

これはコメントともコメントアウトとも全く関係ない機能であり、このセクションで説明するのもおかしな話ではあるのですが、 同じ # で始まる文ではありますのでここで紹介しておきます。

例えば以下の例をご覧ください。

varia ;  /* 不正な文でありコンパイルエラーとなる */

上記は1行目に不正な文が登場しており、必ずコンパイルエラーとなる例です。 このとき以下のように1行目にそのエラーが存在する旨の表示がなされます。 「line=[1」」とある部分です。

[Error]: file=[my_file.rrks] line=[1]
  ... error message ...

ここで以下のように#line文を挿入したとします。

#line 100
varia ;  /* 不正な文でありコンパイルエラーとなる */

この場合、Rarakuは「#line 100」と記述した行を強制的に第100行目とみなします。 またそれ以降の行もこれを基準に順に加算して第101行目、第102行目とみなします。 上記の例の場合、コンパイルエラーはこの次の行で発生しますから、以下のような表示に変わります。 「line=[101」」とある部分です。

[Error]: file=[my_file.rrks] line=[101]
  ... error message ...

この「#line文」は通常の用途で使うことはほぼありません。 これが役に立つのは例えば新たなスクリプト言語やマクロを開発し、そのオブジェクトコード(ホスト言語)としてRaraku言語を採用するような 極めて特殊な用途においてです。 そのような用途では、コンパイルエラーの行表示が狂う可能性がありますが、その場合の調整役として「#line文」を使うことができます。

Close


「/*」と「*/」を使ったコメントの問題点


Rarakuでは敢えて「/*」と「*/」に関してはネストを禁じ、「#if 0」と「#endif」では許可する仕様にしてあります。

その理由は、仮に「/*」と「*/」をネストできる仕様にした場合、コメントの内部にうっかり「/*」文字列が含まれていた場合に、 かなり分かりにくいことになってしまう恐れがあるからです。 例えば以下をご覧下さい。

/*
 * HTTP header example.
 *   POST /res HTTP/1.1
 *   Host: example.net
 *   User-Agent: FireMoai
 *   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/*;q=0.8
 *   Accept-Language: ja,en-US;q=0.7,en;q=0.3
 *   Accept-Encoding: gzip, deflate
 *   Connection: keep-alive
 *   Content-Length: 200
 */
varia i0 int

/*
index1
*/
varia i1 int

/*
index2
*/
varia i2 int

/*
index3
*/
varia i3 int

コメントの開始記号である「/*」は言ってみればあまりにシンプルな記号が二つ並んでいるだけなので、 上記のようにコメント中に開始パターンである「/*」や終了パターンである「*/」がしれっと含まれていた場合、 気付くのはなかなかに困難と考えられます(ちなみに上記の例では「 * Accept:」で始まる行の最後のあたりに 「/*」というパターンが含まれています)。 それにも関わらず、(その気付きにくい部分において)開始パターンや終了パターンがしっかり機能してしまうのがむしろ問題になります。

Rarakuでは「/*」と「*/」がネストできない仕様となっていますので、上記のケースのように開始パターン「/*」が含まれていた場合は単に無視され、 終了パターン「*/」が含まれていた場合は、発生するエラーメッセージの行番号が「*/」を含む行番号として表示される形になります。

一方、仮にネスト可能な仕様であった場合、 特に開始パターン「/*」が含まれていた場合、例えば上記の例ではおそらく「varia i3 int」を過ぎた所でコメントの終了パターン「*/」がないことが判明し、 エラーメッセージとして報告される行番号が実際に問題がある箇所とは全く関係のない(はるかにかけ離れた)行番号になると考えられます。

ネスト可能な仕様であっても、終了パターン「*/」が含まれていた場合は、 そのエラーメッセージの表示のされ方はあまり問題にならない(ネスト不能な仕様と同等になる)と思われます。

テキストエディタのハイライトが正常に機能する範囲であれば、書いたその瞬間に色づけのおかしさで問題に気付くことができるかもしれませんが、 コメントアウトする範囲が広い場合、その色づけがうまくいかないかもしれません。

「/*」と「*/」のネストを許可すると、広範囲を安易に「/*」と「*/」でコメントアウトしてしまうという別の問題もあります。 これも混乱を招くためあまり望ましくありません。 例えば以下をご覧下さい。

/*
/*
 * HTTP header example.
 *   POST /res HTTP/1.1
 *   Host: example.net
 *   User-Agent: FireMoai
 *   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 *   Accept-Language: ja,en-US;q=0.7,en;q=0.3
 *   Accept-Encoding: gzip, deflate
 *   Connection: keep-alive
 *   Content-Length: 200
 */
varia i0 int

/*
index1
*/
varia i1 int

/*
index2
*/
varia i2 int
*/
varia i2_after int

/*
index3
*/
varia i3 int

そもそも対応が一目でわかりにくいですが、一行目にまず「/*」があり、 また「varia i2 int」の直下に、それに呼応すると思われるもう一つの「*/」があります。 つまり上記のコードは、(仮に「/*」と「*/」がネストできる仕様であるなら)最初の行から「varia i2 int」の直後までをコメントアウトする意図で書かれたものと思われます。 しかし実際には、「 * Accept:」で始まる行の最後のあたりに「*/*」が含まれているため、 意図通りにはなりません。

Rarakuでは「/*」と「*/」がネストできない仕様ですので、上記のケースの場合「*/*」の位置でエラーメッセージが発生します (これはある意味、本来指摘されるべき位置でのメッセージです)。 一方、仮にネスト可能な仕様であった場合、「Content-Length: 200」の直後にある「*/」において、一行目の「/*」から始まるコメントアウトが終了します。 そのため、「varia i2 int」の直後の「*/」においてエラーメッセージが表示されることになりますが、 この地点では、コメントアウトは既に終わっておりますので、コメントアウト関係のエラーだとは連想しにくい全く異質なエラーが表示されると考えられます。 上記の場合、(前後の文脈にもよりますが)例えば「*」は掛け算の記号とみなされ、また直後の「/」が割り算の記号とみなされ、 結果的に「数式としておかしい」といった旨のエラーが表示されるかもしれません。

では「#if 0」「#endif」に関してはネスト可能にして大丈夫なのかということになりますが、 これらは行頭(あるいはそれより前は必ずタブ文字かスペースによるインデント)に置かなければコンパイルエラーになるか、 さもなければ完全に機能を失ってスルーされます。 そのためこれまで述べたような行の後方の途中に目立たないように隠れて機能するといった状況は発生せず、その分堅牢であると言えます。 フェアな比較のため、先ほどの例における「/*」「*/」の対応をすべて「#if 0」「#endif」で置き換えたものを以下に示します。

#if 0
#if 0
 * HTTP header example.
 *   POST /res HTTP/1.1
 *   Host: example.net
 *   User-Agent: FireMoai
 *   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,#endif*;q=0.8
 *   Accept-Language: ja,en-US;q=0.7,en;q=0.3
 *   Accept-Encoding: gzip, deflate
 *   Connection: keep-alive
 *   Content-Length: 200
#endif
varia i0 int

#if 0
index1
#endif
varia i1 int

#if 0
index1
#endif
varia i2 int
#endif
varia i2_after int

#if 0
index3
#endif
varia i3 int

今度は「 * Accept:」で始まる行の最後のあたりに「#endif」が含まれていますが、 これは行頭にはないため、「#if 0」の終了パターンとしては機能せず、単に無視されます(これはC言語の#endifの挙動に沿ったものです)。 よってこの場合、「varia i2 int」の直後にある「#endif」が期待通り一行目の「#if 0」に対応するわけです。

「#*」と「*#」に関しては「/*」と「*/」同様ネストはできません。 外側を「#=*」と「*=#」に変更し、内側の「#*」と「*#」を囲うといったことはできますが、 記号文字列自体を変更しているため、厳密な意味でのネストではありません。

これらは行頭(あるいはそれより前は必ずタブ文字かスペースによるインデント)に置かなければ、 一行以内に書かなければならないという制限を持ちます。 さらに開始パターン「#*」を行頭に置いて複数行に渡るコメントを書いた場合、 その終了パターン「*#」も行頭に置かなければコンパイルエラーとなります。 これを踏まえて、先ほどの例と同じものを「#*」と「*#」で書き直すと以下のようになります。

#=*
#*
 * HTTP header example.
 *   POST /res HTTP/1.1
 *   Host: example.net
 *   User-Agent: FireMoai
 *   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*#*;q=0.8
 *   Accept-Language: ja,en-US;q=0.7,en;q=0.3
 *   Accept-Encoding: gzip, deflate
 *   Connection: keep-alive
 *   Content-Length: 200
 *#
varia i0 int

#*
index1
*#
varia i1 int

#*
index1
*#
varia i2 int
*=#
varia i2_after int

#*
index3
*#
varia i3 int

この場合、そもそも「#=*」と「*=#」は他とは別の記号ですので、それらの記号が他に現れない限りは この範囲は確実にコメントアウトされることになります。 また「 * Accept:」で始まる行の最後のあたりに「*#」が含まれていますが、 これは行頭にはないため、仮に「#=*」と「*=#」を削除したとしても、この位置において期待通りコンパイルエラーとなります。


基本的に「/*」と「*/」や「#*」と「*#」は(コードを説明するための)コメントの記述として、 「#if 0」と「#endif」は(一時的にコードを無効化するための)コメントアウトとして使うほうがよいでしょう。 「/*」と「*/」では対応できない文字列(「*/」など)が含まれる場合は「#*」と「*#」を使用します。 逆に「#*」と「*#」を書くことを禁止された配置にコメントを書きたければ、「/*」と「*/」を使用します (ただし、禁止されているということは、その配置はそれなりに危険である可能性もあるので、 中身として記号を多く含むようなコメントを書く場合は注意してください)。 このように取り決めておけば、上記は次のように書けます。

#if 0 /* experimental */
#===*
 * HTTP header example.
 *   POST /res HTTP/1.1
 *   Host: example.net
 *   User-Agent: FireMoai
 *   Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 *   Accept-Language: ja,en-US;q=0.7,en;q=0.3
 *   Accept-Encoding: gzip, deflate
 *   Connection: keep-alive
 *   Content-Length: 200
 *===#
varia i0 int

/*
index1
*/
varia i1 int

/*
index2
*/
varia i2 int
#endif /* experimental */

/*
index3
*/
varia i3 int

上記で「#===*」と「*===#」の部分は、単に「#*」と「*#」でも何の問題もありませんが、 中身がごちゃごちゃしているため、そこに確実に存在しない文字列であると一目でわかりやすい「===」を敢えて含めて書いています。 テキストエディタのハイライト機能が正常に機能しているならば、色付けによってどのみち一目でわかるとは思いますが、念のための措置です。

コメントとコメントアウトのまとめ


コメントとはコードに関する説明を自然言語で記述することであり、 コメントアウトとはコードのある部分を一時的に無効にすることです。 両者は記法としては同じものですが、その意味に応じて使い分けた方が混乱を避けられるでしょう。

コメントとしては「/*」「*/」や「# 」や「#*」「*#」を使うことを推奨します。 一方、コメントアウトとしては「//」や「#if 0」と「#endif」を使うことを推奨します。 「#if 0」と「#endif」の場合、それをネストさせることもできます。



目次に戻る

ブロックとスコープ

ブロックの基本的な働き


ブロック(block)とは { ... } で囲まれた領域のことです(「{」と「}」記号も含めてそう呼ぶことが多いと思います)。

このセクションで説明されているブロックやスコープの働きはC言語のそれとほぼ同じです。 ただしRarakuでは関数の外側(グローバルスコープ)にもブロックを書くことができます。 またRarakuでは変数の宣言(varia文)をブロック内のどこにでも書くことができます (C言語の場合のようにブロックの開始に書かなければならないといったような制限はありません)。

if文やelse文やelif文における { ... } も実はブロックの一種です。 一方、ifやelseやelifといったキーワード、ならびに条件式などの余計なものを一切付けず、 単独で { ... } だけを書くこともできます。 これがいわば「純粋なブロック」です。 C言語にもこの種のブロックがありますが、特別な呼称はなく単にブロックと呼ばれているように思います。 ただしこのチュートリアルでは説明上紛らわしいので、このようなブロックを無名ブロック(anonymous block)と呼ぶことがあります。

ブロックは複数の文を一つのまとまりとしてプログラム内を区分する役割があります。 またブロックの重要な働きとして、変数や定数の宣言の効果が及ぶ範囲を、その中だけに限定させることができます。 例えば以下の例をご覧ください。

{
	varia ival = 1
	ival = 2
} /* ブロックの終了 : ここで varia文の効果が終わる */
ival = 3 /* NG: ブロックの外では ival は宣言されていないため、コンパイルエラー */

上記ではブロックの内部で ival という変数を宣言していますが、この宣言の効果が及ぶ範囲はこのブロック内だけです。 一方、最後の行ではブロックの外で ival という変数に値を代入しようとしていますが、ブロックの外では ival という変数はまだ宣言されていないため、 Rarakuは次のようなコンパイルエラーを出します。

The identifier [ival] may not exist or may be accessed before the declarative statement is executed.

このような変数や定数の宣言の効果が及ぶ範囲のことをスコープと呼びます。 ブロックを記述することにより、そこに一つのスコープを与えることができます。 ブロックで全く囲まれていない一番外側の階層は一つのソースファイル全体に効果がおよぶスコープになりますが、 これを特にファイルスコープと呼びます。

このような限定的な範囲(スコープ)を与えること自体、一見するとあまりメリットがないように見えるかもしれません。 プログラム全体で宣言の効果が及ぶ方が余計なことを考えなくて済みそうです。

しかしコードの行数が増えてくると、変数の値の変化を追いにくくなり、結果的にバグを起こしやすくなります。 一つの変数が生存する範囲をなるべく狭く抑えることにより、コードの可読性と信頼性を上げることに繋がります。 変数へのアクセスが実際に発生し得る範囲をブロックの内部に閉じ込め、スコープを目に見える形で明確化することには一定の意義があります。

異なるブロックに同名の変数宣言がある場合


まず次の例をご覧ください。

{
	varia ival = 1 /* ブロック外で宣言したival */
	ival = 2
}
{
	varia ival = 3 /* ブロック内で宣言したival */
	ival = 4
}

上の例では名前の同じ二つの変数を定義していますが、それらは所属するブロックが異なります。 このような場合、それぞれのivalは別の変数とみなされます。 従って「varia [ival] is already declared(ivalは既に宣言されています)」といった旨のコンパイルエラーはこの場合表示されません。

また3行目の「ival = 2」では同じブロック内に宣言されている変数ival(1で初期化されている方)にアクセスします。 6行目の「ival = 4」も同様に同じブロック内に宣言されている変数ival(3で初期化されている方)にアクセスします。

次にブロック外とブロック内でそれぞれ同じ名前で宣言した場合、どうなるかを見てみます。 以下の例をご覧ください。

varia ival = 1 /* ブロック外で宣言したival */
{
	varia ival = 2 /* ブロック内で宣言したival */
	ival = 3 /* ブロック内で宣言したivalにアクセスする */
}
Rrk_print( ival\n ) /* ブロック外で宣言したivalにアクセスする. 2と表示される */

上の例では名前の同じ二つの変数を定義しています。

一つはブロックの外で宣言したival、もう一つはブロックの内部で宣言したivalです。 このような場合も異なるブロックで宣言されているとみなされ、これらのivalは別の変数として扱われます。

このとき4行目の「ival = 3」における ival は、これと同じブロック内に宣言されている変数ival(2で初期化されている方)にアクセスします。 一方、最後の行の「Rrk_print( ival\n )」における ival は、これと同じブロック外に宣言されている変数ival(1で初期化されている方)にアクセスします。 ブロック外のivalには何も(追加的に)代入されていないため、「Rrk_print( ival\n )」によって表示される値は 1 となります。

ブロック外で宣言されている変数に対しブロック内からアクセスする


まず次の例をご覧ください。

varia ival = 1 /* ブロック外で宣言したival */
{
	ival = 2 /* ブロック外で宣言したivalにブロック内からアクセスする */
	varia ival = 3 /* ブロック内で宣言したival */
	ival = 4 /* ブロック内で宣言したivalにアクセスする */
}
Rrk_print( ival\n ) /* ブロック外で宣言したivalにアクセスする. 2と表示される */

まず3行目の「ival = 2」が記述された時点では、同じブロック内には ival の宣言はまだ行われておりません。 このような場合、ブロックの外側へと順に辿り、最初に見つかる ival の宣言を探します。 そのようにして見つかった ival の宣言(に相当する変数)がアクセス対象となります。 つまり上の例では1行目における変数ivalへ2を代入することになります。 結果的に最後の行の「Rrk_print( ival\n )」によって表示される値は 2 となります。

一方、5行目の「ival = 4」が記述された時点では、同じブロック内に ival の宣言が既に行われています。 よってこれについては4行目における変数ivalへ4を代入することになります。

ブロックはネストすることもできます。 例えば以下の通りです。

varia ival = 1 /* ブロック外で宣言したival */
{
	varia ival = 2 /* ブロック内で宣言したival */
	{
		ival = 3 /* ブロック内で宣言したivalへアクセス */
	}
	ival = 4 /* ブロック内で宣言したivalへアクセス */
}

一番内側のブロックの内部には「ival = 3」といった文がありますが、このブロック内では変数ivalの宣言はありませんから、 外側へと辿り宣言を探します。 一つ外へ出ると「varia ival = 2」で宣言された変数 ival が最初に見つかりますから、その変数ivalへアクセスすることになります。 また「ival = 4」といった文が最後にありますが、これは「varia ival = 2」と同じブロックに所属しますから、 こちらも同じく、その変数 ival へアクセスすることになります。

if文、else文、elif文に伴うブロックに対しても、これまでに述べてきた規則がそのまま当てはまります。 例えば以下の例をご覧ください。

varia ival = 1 /* ブロック外で宣言したival */
if ival > 0 {
	ival = 2 /* ブロック外で宣言したivalへアクセス */
} else {
	varia x = 3
	ival = x /* ブロック外で宣言したivalへアクセス */
}

上記の例では、if文のブロック内には変数ivalの宣言はありませんので、一つ外のブロックで宣言されたivalへ 2 が代入されることになります。 また else 文のブロック内にも同様に変数ivalの宣言はありませんので、一つ外のブロックで宣言されたivalへ x が代入されることになります。 また else 文のブロック内で宣言された変数 x は、このブロックを抜けた瞬間にその宣言の効果がなくなり、アクセスはできなくなります。

ブロックとスコープのまとめ


ブロックを使うことにより、複数の文を一まとめにしたものを構成でき、 また変数/定数のスコープをそのブロック内に限定させることができます。 ブロックはネストでき、その場合は変数の宣言への探索は内側から外側へ、下から上へと辿ることにより行われます。



目次に戻る

算術演算子とオペランド

足し算引き算の演算子とオペランド


Rarakuにおける足し算、引き算は「+」「-」記号を使って普通の数式のように記述します。 例えば以下のようになります。

varia i int
i = 10 +  4 /* 10 +  4 = 14 */
i = 10 -  4 /* 10 -  4 = 6 */
i = 10 + -4 /* 10 + -4 = 6 */

この「+」や「-」のように何らかの式を結んで組み合わせ、特定の演算を施す記号を一般に演算子と呼びます。 特に「+」や「-」のように算術に関する演算子を算術演算子と呼ぶ場合もあります。 また演算子によって演算を施される側の式を一般にオペランド(被演算子)と呼びます。 例えば「10 + 4」の場合、「+」が演算子であり、「10」と「4」が演算子「+」のオペランドになります。

足し算引き算における「+」や「-」は、オペランドを2つとる演算子です。 このようにオペランドを2つとる演算子のことを二項演算子と呼びます。 これに対し、オペランドを1つしかとらない演算子を単項演算子、 オペランドを3つとる演算子を三項演算子と呼びます。 単項演算子と三項演算子については後のセクションで詳しく説明します。

足し算と引き算のオペランドは整数型かまたは実数型でなければなりません。 両方のオペランドが整数型の場合は結果も整数型となります。 両方のオペランドが実数型の場合は結果も実数型となります。

片方のオペランドが整数型、もう一方のオペランドが実数型という指定も許可されており、その場合は整数型のオペランドの方が強制的に実数型に変換されて演算が行われます。従ってその結果は実数型となります。

Rarakuの算術演算子はC言語のそれとほぼ同じです。 ただしRarakuでは「べき乗のための演算子」が存在します。 またRarakuでは、インクリメント/デクリメント演算子に関してC言語と若干の仕様の違いがあります。

掛け算割り算の演算子とオペランド


Rarakuにおける掛け算、割り算は「*」「/」演算子を使って普通の数式のように記述します。 例えば以下のようになります。

varia i int
varia r real
i = 10   * 4   /* 10   x 4   = 40 */
i = 10   / 4   /* 10   / 4   = 2 (小数部分は切り捨て) */
r = 10.0 / 4.0 /* 10.0 / 4.0 = 2.5 */

足し算や引き算と同様、実数型や整数型を指定することができ、同様の規則で結果の型が決定されます。

ただし割り算においてはいくつか注意する点があります。 両方のオペランドが整数型の割り算の場合は、演算結果は小数部分が切り捨てられた整数型となります(これはC言語などの「/」演算子の仕様と同じです)。 一方、少なくとも一方のオペランドが実数型である割り算の場合は、演算結果は小数部分が切り捨てられることはなく、数学的に期待通りの実数型となります。

また、これも重要な点ですが、割り算の演算子「/」の右側のオペランドの値は 0 や 0.0 であってはいけません

割り算の演算子「/」の右側のオペランドの値が 0 や 0.0 であった場合、Rarakuはエラーを出します。 より正確には、それがリテラルとしての指定であれば、Rarakuはコンパイルの段階でコンパイルエラーを出し、 それが変数としての指定であれば、Rarakuは実行時にランタイムエラー「Division by zero.」を表示して実行を中断します。

剰余の演算子とオペランド


「%」演算子を使うことにより、割り算の余りを計算できます。 例えば以下のようになります。

varia i int
varia r real
i = 10   % 3   /* 10   % 3   = 1 */
r = 10.5 % 2.0 /* 10.5 % 2.0 = 0.5 */

割り算と同様、実数型や整数型を指定することができ、同様の規則で演算されます。 剰余の演算子に馴染みのない方向けにもう少し丁寧に説明すると以下のようになります。

  • 両方のオペランドが整数型の場合
  • 左オペランドを右オペランドで整数的に割り算した場合における余りがこの演算子の演算結果となり、その型は整数型となります。 例えば上記の例の「10 % 3」では、10 を 3 で割った時の余りの 1 がその答えとなります。

  • いずれか一方、または両方のオペランドが実数型の場合
  • 左オペランドを右オペランドで実数的に割り算した場合における余りがこの演算子の算術結果となり、その型は実数型となります。 例えば上記の例の「10.5 % 2.0」では、10.5 を 2.0 で割った時の余りがその答えとなりますが、 これはさらに丁寧に言えばまず 10.5 の中に 2.0 が(整数個数として)5 個含まれており、 その後に残った(小数としての)端数部分 0.5 を余りと考えるということです。

C言語をご存知の方は以下のように考えて構いません。 両方のオペランドが整数型の場合、C言語における「%」演算子の結果と同じです。 少なくとも一方のオペランドが実数型の場合、C言語におけるfmod関数の結果と同じです。

割り算と同様、剰余の演算子「%」の右側のオペランドの値は 0 や 0.0 であってはいけません。

剰余の演算子「%」の右側のオペランドの値が 0 や 0.0 であった場合、Rarakuはエラーを出します。 より正確には、それがリテラルとしての指定であれば、Rarakuはコンパイルの段階でコンパイルエラーを出し、 それが変数としての指定であれば、Rarakuは実行時にランタイムエラー「Modulo by zero.」を表示して実行を中断します。

べき乗の演算子とオペランド


「^^」演算子を使うことにより、べき乗を計算できます(「^」ではなく「^^」です)。 例えば以下のようになります。

varia i int
varia r real
i = 10   ^^ 2   /* 10   の 2 乗 (= 100)  */
i = 10   ^^ 3   /* 10   の 3 乗 (= 1000) */
r = 10.0 ^^ -1  /* 10.0 の-1 乗 (= 0.1) */
i = 10   ^^ -1  /* 10   の-1 乗 (= 0) (小数部分は切り捨て) */

底に相当する左オペランド、指数に相当する右オペランドともに、実数型や整数型を指定することができます。 演算結果の型については、これまでの演算子と同様の規則で決定されます。 すなわち両方のオペランドが整数型である場合は結果も整数型になり、 少なくとも一方のオペランドが実数型である場合は結果は実数型になるということです。

指数に相当する右オペランドが実数型の場合、通常これは数学的な解釈と一致します。

例えば右オペランドが 0.5 の場合、これは左オペランドの平方根を計算することになります。 ただし左オペランドが -1 、左オペランドが 0.5 といったような指定は -1 の平方根を計算することになり、 (数学的には虚数となってしまいますが)Rarakuではnonと呼ばれる特殊な実数型の値が演算結果となります。

Rarakuでは複素数型は言語仕様としては提供されません。 そのため、このような複素数が絡んだ計算を実装したい場合は、後のセクションで述べる構造体等の力を借りる必要があります。

また右オペランドが -1 の場合、これは左オペランドの逆数を計算することになります。

ただし左オペランドが 0、右オペランドが -1 といったような指定は 0 の逆数を計算することになります。

この時、両方のオペランドがともに整数型の場合、Rarakuはエラーを出します。 より正確には、それがリテラルとしての指定であれば、Rarakuはコンパイルの段階でコンパイルエラーを出し、 それが変数としての指定であれば、Rarakuは実行時にランタイムエラー「Raising 0(int) to a negative exponent.」を表示して実行を中断します。

一方、少なくとも一方のオペランドが実数型の場合、両方とも整数型の場合と異なりRarakuは明示的にエラーを出しません。 この場合はinfと呼ばれる特殊な実数型の値が演算結果となります。

両方のオペランドが整数型である場合、結果は整数型になることに注意してください。 これは右オペランドが非負の整数である場合はあまり問題になりませんが、一方でそれが負の場合、切捨てが発生する可能性があります。 例えば左オペランドが 2、右オペランドが -1 の場合、これは左オペランド 2 の逆数を計算することになり、その値は正確には 0.5 ですが 結果は最終的に整数型に矯正されるため、0.5 が切り捨てられて 0 となります。

C言語をご存知の方は以下のように考えて構いません。 両方のオペランドが整数型の場合、まずC言語におけるpow関数を適用し、その戻り値(実数型)を整数としてキャストした値が演算結果となります。 一方、少なくとも一方のオペランドが実数型の場合、C言語におけるpow関数の戻り値がそのまま演算結果となります。

ただしRarakuの「^^」演算子に整数を指定する場合、片方をsigned型の変数、もう一方をunsigned型の変数というように混ぜて指定することが許可されません (この仕様は掛け算などに見られる一般的な二項演算子と同じであり、つまり一方を他方の型に合わせるためのキャストが必要になるということです)。 これについては「型のキャストと暗黙変換」のセクションで詳しく述べます。

算術演算子の優先順位


演算子にはデフォルトの優先順位があり、例えば算術演算子の場合、べき乗がまず最優先で計算されます。 次に掛け算割り算剰余が計算されます。最後に足し算引き算です。 例えば以下の例をご覧ください。

varia i int
i = 5 + 4 * 3 ^^ 2

上の例ではまず「3 ^^ 2」が計算され、9が得られます。 次に「4 * 9」が計算され、36が得られます。 最後に「5 + 36」が計算され、41が得られます。 最終的にiには41が代入されます。

この優先順位を強制的に変えたい場合は ( ... ) でその部分を囲みます。 例えば以下の例をご覧ください。

varia i int
i = (5 + 4) * 3 ^^ 2

上の例ではまず「(5 + 4)」が最初に計算され、9が得られます。 次に「3 ^^ 2」が計算され、9が得られます。 最後に「9 * 9」が計算され、81が得られます。

複合代入演算子


ある変数において、その値を1増やしたいとします。 これは例えば以下のように記述することで実現できます。

varia i int = 10
i = i + 1

上の例では、「=」の左辺と右辺の両方に変数 i が現れています。 数学的には奇妙に思える表記ですが、これは方程式ではなく代入文です。 従ってこれらの i が同時に同じ値になるわけではありません。

この文の読み方はまず左辺を無視し、右辺だけを考えます。 右辺を先に評価して計算しその値を求めます( i には既になんらかの値が入っているためこれは可能です)。 それが終わった後に左辺で示された変数(この場合 i)へ今計算したその値を代入します。 これにより結果的に変数 i の値が 1増えた形(この例の場合11)に更新されるわけです。

ところでRarakuでは演算子「+=」を使うことにより、上記と等価な処理をもっと短く記述することができます(C言語における「+=」と同じです)。 以下の例をご覧ください。

varia i int = 10
i += 1 /* i = i + 1 と全く同じ */

演算子「+=」は左側のオペランドで指定された変数を、右側のオペランドで指定された数値で加算します。 即ちこの場合、変数 i に 1 が加算されるわけです。

このように変数にある処理を施して更新するための演算子を複合代入演算子と呼びます。 引き算、掛け算、割り算、剰余についても同様の複合代入演算子が用意されており、 これらは足し算における複合演算子「+=」の「+」の文字をそれぞれ該当する算術演算子に置き換えた表記になります。

以下はRarakuで用意されている算術に関する複合代入演算子とその使用例です。

varia i int = 10

/* 演算子「+=」*/
i += 1 /* i = i + 1 と全く同じ */

/* 演算子「-=」*/
i -= 1 /* i = i - 1 と全く同じ */

/* 演算子「*=」*/
i *= 2 /* i = i * 2 と全く同じ */

/* 演算子「/=」*/
i /= 2 /* i = i / 2 と全く同じ */

/* 演算子「%=」*/
i %= 2 /* i = i % 2 と全く同じ */

上の例ではint型(整数型)変数での使用のみを紹介しましたが、real型変数に対しても同様に複合代入演算子を使うことができます。

ただしC言語等とは異なり、Rarakuでは複合代入演算子を用いた記述は完全な文となります。 つまりこれを別の式中に埋め込んで記述するようなことはできません(Rarakuでは通常、文は右辺値としての値を持ちません)。 尚、後述するfor文の「後処理文」で、変数値を増減させるために使用することは可能です。

インクリメントとデクリメント


まず以下の例をご覧ください。

varia i int = 10
i += 1 /* 1増やす */
i -= 1 /* 1減らす */

上の例では複合代入演算子を使って整数型変数の値を1増やしたり、1減らしたりしています。 しかしプログラミングにおいてこのような整数を1増減する操作は非常によく行われるため、 多くの処理系ではこの操作のためだけの高速な命令が用意されています。

整数型変数を1だけ増減させる処理は、プログラミングにおいて頻繁に行われます。 例えば後述するfor文やwhile文などでは、繰り返しが終了するタイミングを制御するために使用されます。

このような操作には名前もついています。 整数を1増やすことをインクリメント、整数を1減らすことをデクリメントと呼びます。

Rarakuを実行する仮想マシンでもインクリメントとデクリメントのための専用の命令が用意されており、 それを利用するためにインクリメント演算子「++」やデクリメント演算子「--」を識別子等の前に置きます。 以下の例をご覧ください。

varia i int = 10
++i; /* 1増やす(ただしi += 1より高速) */
--i; /* 1減らす(ただしi -= 1より高速) */

上記は結果的に「i += 1」や「i -= 1」とやってることは同じですが、より高速なRaraku仮想マシンの命令に変換されます。 また単純にコード上での表記も短くなります。 ただしインクリメント演算子やデクリメント演算子は(複合代入演算子とは異なり)整数型変数でのみ使用可能です(real型変数に対しては使用できません)。 また増減幅も 1 固定です。

インクリメント/デクリメント演算子だけからなる文の文末の「;」は必須です。 Rarakuでは殆どの文で「;」を省略できる中、これは数少ない例外となります。

ただし厳密に言えば、ファイルやブロックの最後の文である場合や、 次に続く文がいわゆる文の開始を表すキーワード(例えばvariaやifなど)である場合では、 この「;」が省略可能なこともあります。 とはいえそのような正確な条件を毎回考えるのも面倒です。 インクリメント/デクリメントでは必ず「;」を付けるものと覚えてしまった方が楽でしょう。

一般にプログラミング言語におけるインクリメント演算子とデクリメント演算子には前置式と後置式があります。 前者は「++i」、後者は「i++」と書く方式です。 Rarakuでは前者、つまり「++i」と書くことを強く推奨します。

Rarakuでも一応「i++」と書くこともできますが、Rarakuにおいてこれは「i += 1」の完全なシンタックスシュガーとなります。 つまりこう書いた場合、Raraku仮想マシンにおけるインクリメント/デクリメント用の高速な命令に変換はされません。

またRarakuでは「++i」と書いた場合はそれを式中で使うことができますが、一方で「i++」と書いた場合それは完全な文となり、 式中に記述することはできません(Rarakuでは通常、文は右辺値としての値を持ちません)。 つまり前者はインクリメント式ですが、後者はインクリメント文となるわけです。

参考: このような仕様である理由
「i++」を式としてサポートしない理由は、主にRarakuの実装側の都合です。 仮にこれを式とした場合、パーザ上で考えなくてはならない問題が増えること、 様々な可能性に配慮するために結局は複雑なRaraku仮想マシン命令群に変換する必要があり大して高速にはならないこと、 またその影響が他に波及して一部のRaraku仮想マシン命令の実装が余計に複雑化してしまうこと、 さらにはその労力の割に、「i++」を式として使う実用上のメリットがあまりない(全くないとは言いませんが)上、一応代替の記法(後述)も存在するためです。

Close


例えば、(可読性が悪いため推奨しませんが)「++i」であれば、一応(C言語と同様)以下のように記述することが可能です。

varia i int
varia j int
i = 10
j = ++i /* ++i; j=i と同じ : jの値は11 */

上記では「++i」は代入文の右辺であり、つまり「++i」単独の文ではないため、文末の「;」は省略できます(書いても構いませんけども)。

一方、「i++」の場合、以下のように記述するとコンパイルエラーとなります。

varia i int
varia j int
i = 10
j = i++ /* コンパイルエラー: 「i++」は一つの文であり、代入文の右辺に記述することはできない */

上の例では(まずiのコピーをjに作ってからiをインクリメントすることを期待しているのでしょうから)通常は素直に以下のように書くべきです。

varia i int
varia j int
i = 10
j=i; ++i /* OK: まずjにiを代入し、iをインクリメントする */

このように通常は代入文とインクリメント/デクリメントの文を別々に書いた方が可読性は高くなります。

一応以下のようにすれば(可読性は低いですが)強引に「i++」に相当する式を右辺値として埋め込むことはできます。

varia i int
varia j int
i = 10
j = ++i-1 /* 一応OK: jに10が代入され、iがインクリメントされる */

このような記法が必要になる状況というのはほとんどないと思います(ほとんどの場合、別の文に分けて書くなどもっと望ましい記法があると考えられます)。 例えば以下のようにインデックスを0から順にインクリメントしつつ参照し、かつそれぞれを1文で済ませるように記述したいような状況が (特に後のセクションで述べる配列の初期化などにおいて)考えられなくもないですが、 やはり使用は必要最小限に留めるべきでしょう。

varia idx = 0
Rrk_print( ++idx-1 \n ) /* 0 */
Rrk_print( ++idx-1 \n ) /* 1 */
Rrk_print( ++idx-1 \n ) /* 2 */

上記では「++i」と「i++」の場合について説明しましたが、これは「--i」と「i--」においても全く同様です。

インクリメント・デクリメントの文では、前置式と後置式のどちらであるかが構文上わかりにくいことがあります。 例えば以下をご覧ください。

varia i int
varia j int
i++j;

非常にわざとらしく邪悪にした例ではあるのですが、上記のように記述した場合、字面上は「i++」と「j」で分けることもできますし、「i」と「++j」で分けることもできます。 しかしRarakuではインクリメント演算子だけからなる文には「;」が必須です。 仮に前者のように分けるとした場合、「i++」の直後に「;」(あるいは文を分かつようななんらかのキーワード)が来なければなりませんが、 そのようなものはなく即座に「j」が続いています。 一方、後者のように分けるとした場合、「++j」の直後に「;」が来ており、問題なくこの文が完結します。 よってこのような場合、Rarakuでは後者と解釈されます(ちなみに残りの単に「i」とだけ書かれた文は特に意味を持ちません)。

Rarakuはトークンとトークンの間であれば自由に改行を入れることができ、それによって構文の意味が変化することは(一部の例外を除き)ない言語です。 この例の場合、トークンは「i」「++」「j」「;」のように分割されます。 よって仮に以下のように改行を入れて書いたとしても、依然として「++j」が一つの塊と解釈されます。

varia i int
varia j int
i++
j;

この例で「i++」を一つの塊と解釈して欲しい場合は、以下のように「;」を間に置く必要があります(その場合、今度は最後の「j」が単独では意味のない文になります)。

varia i int
varia j int
i++;j

ただ結局iをインクリメントし、その後に単独のjを置きたいのならば、 「i++」を使うより以下のように「++i」を使った方が安全と思われます。

varia i int
varia j int
++i;j

「++i」の場合、(今、プログラマが本当に「++i」の意図で書いているものとして) その後ろに「;」を書き忘れたとしてもコンパイルエラーが表示されるからです。

算術演算子とオペランドのまとめ


算術演算子では基本的には数学における演算と同じ優先順位で計算されます。 ただし数学とは異なり、「=」は方程式としての意味ではなく、代入の意味となります。 また複合代入演算子を使うことで算術演算子と代入を同時に行うといったこともできます。

インクリメント/デクリメント演算子だけからなる文の文末の「;」は必須となります。 「;」をつけるのを忘れないようにしましょう。



目次に戻る

文字列

文字列リテラル


文字列には大きく二通りあります。 文字列リテラルとそれを格納するための文字列変数/定数です。

文字列リテラルはソースコード中に埋め込まれた生の文字列です。 Rarakuでは文字列リテラルの記法として以下のようなものがあります。

  • ダブルクォート「"」で囲う記法
  • 「@'」と「'」で囲う記法
  • 「@['」と「']」で囲う記法
  • 「@識別子パターン'」と「'識別子パターン」で囲う記法

下にあるものほど開始と終了の指定は長くなりますが、 (上にあるものでは)囲うことが難しい文字列に対しても問題なく囲うことができます。 ではまずはそれぞれの文字列リテラルについて詳しく見ていきましょう。

文字列リテラル:ダブルクォート「"」で囲う記法


たぶん多くのプログラミング言語でだいたい同じような仕様になっている記法で、 例えば以下のようになります。

"hello"

最初は「"」で始まり、次に「"」が来た時点でその文字列リテラルの範囲が終了することになります。 このとき「"」で囲まれた中身(この場合「hello」の部分)が実際の文字列値であり、 「"」自体はあくまでそれを囲う記号に過ぎず、実際の文字列値には含まれません。

ところで「"」が来た時点で範囲が終了するということは、「he"llo」のような途中に「"」そのものがあるような文字列を指定したい場合に 困ることになります。このままでは「he」の次に来る「"」の地点で文字列リテラルの範囲が終了してしまい、 実際の文字列が「he」になってしまうからです。

このような状況に対処するため、エスケープ文字があります。 エスケープ文字は直後の文字に特別な意味を与えたり、 またその逆に特別な文字を字面通りの意味に戻したりします。

「"」で囲う記法の場合は「\」文字がエスケープ文字となります。 例えば中身の文字として「"」自体を(字面通りに)記述したい場合、以下のように「\"」と記述することでそれが可能になります。

"he\"llo"

上記では「\"」という二文字の塊が(字面通りの)「"」に置換されます(これにより、ここでは文字列は終了しません)。 次に「"」が現れるのは「o」の直後ですが、この「"」は終了記号としての「"」です。 結局「he"llo」が実際の文字列値になります。

また以下のように「\n」で改行コードを、「\t」でタブ文字を、「\\」で「\」文字自体を記述できます。

"CR is \n"
"Tab is \t"
"Backslash is \\"

このようにエスケープ文字の働きによって意味が変わった一連の文字列をエスケープシーケンスと呼ぶことがあります。

「"」で囲う中身の文字列値は複数行であってはいけません。 言い換えれば「"」で囲う中身の文字列値に生の改行コードが含まれていてはいけません。

これはC言語やgo言語の仕様と同じです。

例えば以下はコンパイルエラーとなります。

"---
+++
===" /* NG: Compile error! */ */

上記は、「---」「+++」「===」の三行を一つの文字列リテラルとして記述しようとしたものですが、 このとき「---」と「+++」の間に生の改行コードが一つ(また「+++」と「===」の間にももう一つ)あることになり、 コンパイルエラーとなります。

生の改行コードには「\n」(いわゆるUnix/現在のMacOS形式)と「\r\n」(いわゆるWindows形式)の二種類があります。

仮に、上記の記述を許可したとしますと、生の改行コードの実体が何であるかによって結果が変わってきます。 これがもし「\n」であった場合は以下と同じになります。

"---\n+++\n==="

一方で、これがもし「\r\n」であった場合は以下と同じになります。

"---\r\n+++\r\n==="

つまり一見すると同じような記述であるのに、生の改行コードの種類によって文字列リテラルの値が変わる (プログラムの挙動が変わる)といったあいまい性が発生するところに問題があるわけです (しかもこの(生の)改行コードは、テキストエディタの改行変換機能によって容易に変更できてしまいます)。 そういった理由で「"」の中身において、生の改行コードが入るのを禁止しています。

もっとも、この生の改行コードの区別は、(例えばHTMLなど)柔軟に対応されるケースも多いため、 ジャンルによってはあまり問題になりません。 しかし例えばHTTPヘッダのように、これを厳密に扱わなければならないこともあります。

Rarakuでは「\r」が単独で改行となる形式(いわゆる古いMacOS形式)を改行とみなしません。 そのため、ここでも「\r」が単独で現れるケース(Wandering \r)は考えない(その場合は単にその他の文字と同等と考える)ことにします。
生の改行コードはエスケープ文字で明示的に無効にすることもでき、 その場合であれば見かけ上複数行にすることは可能です。 例えば以下の通りです。

"---\
+++" /* OK */

上記では「---」の直後に「\」があり、さらにその直後に生の改行コードがあります。 このとき生の改行コードは直前の「\」により無効化され、空文字に変換されます。 つまり上記では「---」と「+++」の間には本当に何もなくなり、これらが連続して「---+++」と繋がることになります。 結局上記は「"---+++"」と記述したのと同じです。

ただ、実際のプログラミングではこのように書くことはあまりないかもしれません。 上記のようにエスケープ文字を使う方法ですと、もしも二行目「+++」の前にタブなどのインデント文字が存在する場合、 その文字も文字列リテラルの中に含まれてしまい、多分意図した内容とはならないためです。

替わりに以下のように書く方が、ほとんどの場合望ましいでしょう。

"---"
"+++" /* OK */

詳しくは「文字列の連結」の項で述べますが、Rarakuでは文字列リテラルが単に連続する場合、 それらは自動的に連結されます。従ってこれは「"---+++"」と記述したのと同じになります。

尚、「---」の後に本当に改行したければ以下のように書くこともできます。

"---\n"
"+++" /* OK */


次に、「"」で囲われる文字列が全角文字である場合について考えましょう。

ASCII文字は1byteで構成されますが、一方で全角文字は2byte以上から構成されます。 全角文字の例としては、ひらがなや漢字、ハングル文字などが挙げられます。

最近の言語では文字列リテラルとして指定できる文字コードがUTF-8を始めとしたUnicodeに限定されるものもあります。 確かにUTF-8が理想ではあるのですが、Rarakuではそのような制限は設けていません。 例えばShift_JISなどいわゆる古典的な文字コードには色々と問題があるのは重々承知しておりますが、 場合によってはそのようなもので記述しないとかえって煩雑になるケースも想定できなくもないため、 一応ほとんどの文字コードが指定可能としてあります。

ただしRarakuではISO-2022-JPとISO-2022-KRで文字列リテラルを記述することはできません。 理由はこれらの文字コードがプログラミングとは非常に相性が悪いからです。

これらの文字コードでは全角文字に「"」「'」「]」「=」などの文字が含まれる可能性があり、 単純なパターンマッチでは文字列リテラルの正確な終了判定すらできません。

一応、「文字列リテラルの中身がこれらの文字コードである」ことを前提に、 前方からシークすれば終了判定が可能ですが、そもそも「文字列リテラルの中身がこれらの文字コードである」ことを 確実に自動判別する方法がありません。これらの文字コードにEsp文字(十六進数で1b)が多く含まれる傾向があるのは確かですが、 通常の文字列リテラルでもEsp文字が含まれている可能性はあります。 例えばLinuxのコンソールでの表示文字に色付けを行いたいとして、文字列リテラルにEsp文字を含ませるような状況は十分想定できます。 後で述べる「pragma charset_auto:乙」を宣言した場合は自動判別可能ですが、それを宣言しなかった場合は結局判別できません。

ISO-2022-JPとISO-2022-KR専用に特別な記述子を用意して、 そのような記述子で開始される文字列リテラルに関しては これらの文字コードとみなす方法も考えられます。 しかしそこまでやって終了判定の問題をなんとかクリアしたとしても、その先にも問題は山済みです。 これらの文字コードは他の文字コードと違い状態を持つため、どのみち通常の方法ではまともな文字列加工ができそうにありません。

結局、これらの文字コードを最終的に得たい場合でも、まずはプログラミングに適した他の文字コードで加工し、 最終的に加工が完了した段階で、これらの文字コードに変換するようにするしかないように思います。 そして結局最後に変換するならば、わざわざ文字列リテラルでこれらの文字コードを指定可能にする必要性もないと思われます。

ここで結論をまず述べますが、文字列に全角文字が含まれる場合は、そもそも「"」で囲うべきではなく、 次に紹介する「@」で始まる記法で囲うべきです。 理由はそのような場合に「"」で囲うと色々問題があるためです。

正確に言えば、文字コードがUTF-8やEUCである場合、全角文字を「"」で囲ってもとりあえずは問題ないのですが、 それら以外の文字コードである場合に問題となります。 一方、「@」で始まる記法で囲う場合であれば、中身の文字コードが何であれとりあえず問題がないため、 特に中身の文字列に全角文字を含む場合はそちらを使うことを強く推奨しています。

しかしここでは敢えて「"」で囲い、具体的に何が問題なのかを詳しく見ます。 例えば文字コードがShift_JISの「ソース表」という全角文字の場合、「"」では以下のように書かなければなりません。

"ソ\ース表\" /* for Shift_JIS */

上記は非常に奇妙な表記ですが、「ソ」と「表」には特別な問題があり、 そのため「ソ」の替わりに「ソ\」と、また「表」の替わりに「表\」と書いてあるということです。 尚、残りの「ー」(長音符号)と「ス」にはこの問題はないため、そのまま表記しています (というよりこれらには逆に余計な「\」をつけてはいけません)。

上記のように書く理由は以下のようになります。

Shift_JISの場合、「ソ」は2byteで構成される文字であり、しかもその2byte目はちょうどASCII文字の「\」と同じ値になります (16進数表記で書くと、「ソ」は「835c」、ASCII文字の「\」は「5c」になります)。 つまり「ソ」と書くと、字面では分かりませんが(純粋なバイト列としては)その後ろ半分に「\」が一つ存在するのと同じになります。

一方、「"」で囲われた内部では「\」はエスケープ文字とみなされるため、 「\」が一つだけ単独で存在する場合、直後の文字に特殊な影響を与えます。 また「\」の字面通りの意味も消滅するため、「ソ」自体の後ろ半分(2byte目)がなかったことになり、 いわゆる文字化けが発生するわけです。 さらには「ソ」をうっかり最後の「"」の直前に書いてしまうとその「"」は字面通りの意味の「"」とみなされ、 終了記号としての意味を失います(つまり文字列リテラルがそこで終了しなくなります)。

「"」で囲われた内部で「\」を字面通りの意味で書くためには「\\」と書かなければなりません。 ただ今回、(字面ではわかりにくいですが)「ソ」自体の後ろ半分に一つの「\」があるわけですから、 「ソ」の直後にもう一つの「\」を付け足すことで、結果的に「\\」がそこに書かれたのと同じ状況が作れます。 そのため、上記の例では「ソ」の替わりに「ソ\」と書いてあるわけです。

「ソ」以外にも、「表」(16進数表記で955c)も同様に後ろ半分が「\」と同じになります。 従って、これについても「ソ\」と同様に「表\」と書くことで対処しています。
同様の問題は他の文字にもあります。 Shift_JISに関して全て挙げると以下になります(参考:https://uic.jp/charset/show/cp932/)。

― ソ Ы Ⅸ 噂 浬 欺 圭 構 蚕 十 申 曾 箪 貼 能 表 暴 予 禄
兔 喀 媾 彌 拿 杤 歃 濬 畚 秉 綵 臀 藹 觸 軆 鐔 饅 鷭
纊 犾

割と大量にあります(最初の「―」は長音記号ではなくダッシュです)。 一行目のものは(「Ы 浬 箪」あたりは若干微妙ですが)普段よく使われるものでしょう。 一方で二段目以降は(個人差はあるでしょうが、饅頭の「饅」以外は)普段はあまり使わないでしょう。 一行目からさらに、プログラミングの文字列リテラルとしてある程度よく使われるであろう文字を 個人的な偏見で厳選しますと以下になります。

― ソ 構 十 申 貼 能 表 暴 予

これくらいであれば(人間が)記憶できそうですが、 しかしどのみち、常にこれらの文字が来るか否かを気にしながら文字列リテラルを使うわけにもいきません。 しかも「ソ」を「ソ\」などと書く記法はShift_JISのためだけの記法ですので、いざファイルの文字コードを コンバータなどで別のものに変えるとなった場合、この部分を漏れなく修正する必要が生じます。 次に紹介する「@」で始まる記法は、この問題を解消したものになります。

文字列リテラル:「@'」と「'」で囲う記法


この記法では「"」で囲う替わりに「@'」と「'」で囲います (「@'」の間に余計なスペース等を入れることはできません)。 例えば以下のように記述します。

@'hello'

上記は「"hello"」と同じ意味になります。 この記法では「'」と「'」の間に「'」を記述することは(デフォルトでは)できません。 その位置で文字列リテラルの終わりとみなされてしまうためです。

Rarakuにおいて、この「@」で始まる記法は、正式にはヒアドキュメントと呼ばれるものになります。 Rarakuのヒアドキュメントは比較的多くの機能があるので、詳細は後の「ヒアドキュメント」セクションで述べますが、 ここではその中でも一番簡単でよく使われるであろう記法を少しだけ取り上げています。
「@」で始まる点はC#のVerbatim記法とも似ていますが、実際の仕様はかなり異なります。

この記法では「\」はエスケープ文字とはみなされません(というよりデフォルトではエスケープ文字自体が無効になっています)。 よって「"」や「\」を記述した場合はそれは文字通りの意味とみなされますし、 「\n」は(改行ではなく)「\」と「n」の二文字、「\t」も(タブ文字ではなく)「\」と「t」の二文字とみなされます。

この記法で中身の文字列値に「'」を含める必要がある場合、エスケープ文字を明示的に有効にする必要があります。 一旦エスケープ文字を有効にすれば、あとはダブルクォート「"」で囲う文字列における「\」の場合と同じ要領で使うことができます。

例えばエスケープ文字「$」を明示的に有効にしたい場合、「@」と始めの「'」の間に「,$」と記述します(カンマが必要であることに注意してください)。 このとき、以下のように「@,$'」と「'」で中身の文字列値を囲う形になります(「@,$'」の間に余計なスペース等を入れることはできません)。

@,$'aaa$'bbb'

上記は「"aaa'bbb"」と同じ意味になります。 つまり「aaa」と「bbb」の間にある「$'」が一文字の「'」と解釈されます。

ただし「$」がエスケープ文字となったため、今度は一文字の「$」については替わりに「$$」と記述しなければなりません。 また「$n」と記述するとそれは改行の\n、「$r」は復帰記号の\r、「$t」はタブ文字の意味になります。

この記法は、Windowsにおけるファイルのパスや、「"」を含む文字列、全角文字を含む文字列などを記述したい場合、 (エスケープ文字を使わずに)そのまま記述できて便利です(ただし「'」を含まないものとします)。 「"」で囲う場合と比較しますと、以下のようになります。

"C:\\my_raraku\\rrk_pkg\\std"
@'C:\my_raraku\rrk_pkg\std'

"href=\"javascript:void(0);\" onClick=\"show();\""
@'href="javascript:void(0);" onClick="show();"'

/* Shift_JIS SO/HYO */
"ソ\ース表\"
@'ソース表'

この記法では「"」とは異なり、複数行に渡る文字列を(本当に複数行の意味で)囲うことができます。 例えば以下の通りです。

@'---
+++
===
'	

上記のように記述した場合、生の改行コードは強制的に「\n」とみなされます(たとえ生の改行コードが「\r\n」であっても「\n」に変換されるということです)。 つまり上記は「"」を使って以下のように書いたのと必ず同じになります。

"---\n+++\n===\n"

これはgo言語における「`」で囲む文字列リテラルや、Rust言語の仕様と同じです。

複数行に渡る文字列を「@'」と「'」で囲う場合において、「@'」の直後が改行であった場合(つまり一番最初の文字が改行であった場合) はそれが無視されます。 例えば以下をご覧下さい。

@'
---
+++
===
'

上記では「@'」の直後に改行がありますが(つまり一番最初の文字が改行ですが)それは無視され、 以下と同じ意味になります。

"---\n+++\n===\n"

一方、一番最初の文字が改行ではない場合はそれは無視されません。 一つ前の例では、「@'」の直後は「-」でしたから(つまり一番最初の文字が「-」でしたから)それは文字通り解釈されたというわけです。

一番最初に本当に改行を置きたい場合、以下のように最初に空行を入れます。

@'

---
+++
===
'

上記では(「@'」の直後にある)最初の改行は無視されますが、(---の直前にある)二番目の改行は無視されません。 つまり以下と同じ意味になります。

"\n---\n+++\n===\n"

このような仕様になっている背景については、後の「ヒアドキュメント」セクションで述べることにします。

文字列リテラル:「@['」と「']」で囲う記法


「@'」と「'」で囲う記法は「"」で囲う記法にあった問題を色々解消できるものではあるのですが、 中身の文字列値に「'」を含めることができないという弱点がありました。 それを解消したのが「@['」と「']」で囲う記法です。 例えば以下をご覧下さい。

@['aaa'bbb"ccc\dddソース表']

上記は中身の文字列値は字面通り「aaa'bbb"ccc\dddソース表」となります(「ソース表」の部分はShift_JISコードとします)。 この記法でもデフォルトではエスケープ文字が無効であり、また「'」「"」「\」や全角文字をすべてそのまま書くことができます。 つまり「"」で囲う記法や「@'」と「'」で囲う記法にあった弱点が両方とも解消されます。

開始パターンと終了パターンがほんの少し長くなるというコストを支払うことにはなりますが、 (それをせず)エスケープ文字を使って結局長く(しかも読みづらく)なるのであれば、 支払う価値のあるコストと言えるでしょう。 参考のため上記を他の記法で書いたものも一緒に並べますと、以下のようになります。

@['aaa'bbb"ccc\dddソース表']
@,$'aaa$'bbb"ccc\dddソース表'
"aaa'bbb\"ccc\\dddソ\ース表\"

「['」の間に余計なスペース等を入れることはできません。 またこの記法で万一最初の「@」を書き忘れますと、 「[」は配列初期化子の意味として解釈されることになります (配列初期化子については「配列」のセクションで詳述します)。
上記のように並べると、まるで「"」で囲う記法が全然有用でないように見えるかもしれませんが、そうでもありません。 中身の文字列値に「"」、「\\」、全角文字のいずれも含まれず、しかも複数行に渡る記述ではない場合、 「"」で囲う記法はもっともすっきりと短く書ける記法です。 そして実際のプログラミングにおいては、そのような状況の方がやはり多く、 むしろ最も頻繁に使うのは「"」で囲う記法になるでしょう。

この記法は全体的に優秀でバランスがよく、実用上問題になることはほぼないと考えられます。 この記法で(これまで述べた範囲で)唯一含めることができないのは(終了文字列パターンである)「']」と 「\r\n」というWindows系の改行パターンだけです。 場合によってはこれらを文字列に含ませたい状況もあるかもしれません。

念のために補足しますと、ここで言う「']」とは「'」と「]」の二つがこの順番で連続する(文字列長が2の)文字列です。 順番が逆の「]'」といった文字列や、そもそも「'」と「]」が連続しておらず単独で離れて出現する場合は、 もちろんそのまま含めることができます。

まず終了文字列パターンである「']」に関してですが、 これに対処するおそらく一番楽な方法は、開始パターンと終了パターンで「=」を使うことです。

「@['」と「']」で囲む替わりに「@[='」と「'=]」で囲うと、「']」を含めることが可能になります。 例えば以下のようになります。

@[='aaa']bbb'=]

上記は"aaa']bbb"と結果的には同じです。 この場合、含めることができないのは、今度は(終了文字列パターンである)「'=]」となり、 「']」よりもさらに珍しい文字列になります。

さらに「'=]」も含めたい場合、替わりに「@[=='」と「'==]」で囲うと、「'=]」を含めることが可能になります。 例えば以下のようになります。

@[=='aaa'=]bbb'==]

上記は"aaa'=]bbb"と結果的には同じです。 この場合、含めることができないのは、今度は(終了文字列パターンである)「'==]」となり、 「'=]」よりもさらに珍しい文字列になります。

以下同様にして「=」の数を必要に応じて増やしていくことができます。 この「=」は開始時においては「[」と「'」の間に、 終了時においては「'」と「]」の間に連続して書かなければならないことに注意してください。 また開始時に指定する「=」の個数と 終了時に指定する「=」の個数は必ず一致していなければなりません。

尚、指定できる「=」の個数に上限はありませんが、あまり多くなると今度は人間の目でぱっと見で数えにくくなりますので ほどほどにしておいた方がよいでしょう。 (あまりないとは思いますが)「=」を数多く指定せざるを得ない特殊な状況の場合は、 別の方法を検討した方がよいかもしれません。

次に改行コードを「\n」ではなく、明示的に「\r\n」にしたい場合ですが、 これに対処するおそらく一番楽な方法は、開始パターンで「@」と「[」の間に「/"\r\n"」と指定することです(ダブルクォートが必要です)。 例えば以下のようになります。

@/"\r\n"['
---
+++
===
']

この指定の意味は、生の改行コードを強制的に「\r\n」に変換するというものになります (デフォルトでは既に述べた通り「\n」に変換されますが、その挙動を「\r\n」に変更するということです)。 これは結局以下と同じ意味になります。

"---\r\n+++\r\n===\r\n"

「@'」と「'」で囲む記法の時に説明した、エスケープ文字を有効にする方法を使うこともできます。 「@['」の場合、「@」と「[」の間に「,$」を入れることで「$」をエスケープ文字とすることができます。 つまり「@,$['」と「']で中身の文字列値を囲うことになります。 例えば上記の例は以下のように書くこともできます。

@,$['---$r$n+++$r$n===$r$n']

上記では「$」がエスケープ文字となりましたので、 「$r」は\r、「$n」は\nの意味になります。 よって「$r$n」と記述することで明示的に「\r\n」を挿入することもできるわけです。

エスケープ文字を使って「']」をそのまま字面通りに埋め込むこともできます。 そのためには「'$]」と記述します。「'$]」であって、「$']」ではないことに注意してください。 「']」の間に割り込む形で「$」を入れます。

もしも「$']」のように書きますと、その「$」は文字通り「$」として解釈され、 しかも直後の「']」は終了パターンとして解釈されます(つまり文字列リテラルがそこで終了してしまいます)。

例えば以下の通りです。

@,$['---'$]+++']

上記は以下と同じ意味になります。

"---']+++"

文字列リテラル:「@識別子パターン'」と「'識別子パターン」で囲う記法


ユーザが自由に指定可能な識別子によって文字列を囲う記法です。 最初と最後の識別子パターンは同じ文字列でなければなりません。

一般的にはヒアドキュメントと言った場合、このような記法を表すことが多いように思います。

例えば以下のような記述です。

@__here__'hello'__here__

上記の例は「"hello"」と同じ意味になります。 また「@」の後、開始パターンとして「__here__'」を指定しておりますので、 この文字列リテラルの終了パターンも「'__here__」にする必要があります。 「'」の位置にも注意してください。「'」は一番最内に位置します。 尚、一つの文字列リテラルが完全に終わった後、次の文字列リテラルで同じ識別子を再び使うことは問題ありません。

上の例では「__here__」という識別子を使いましたが、 ユーザはどのような識別子を指定しても構いません。 ただしそれが「識別子」としての条件を満たす必要はあります。 Rarakuでの「識別子」とは、最初の文字は「_」またはアルファベット(数字はNG)、 二文字目以降は「_」またはアルファベットまたは数字からなる文字列です。

この記法でもやはりエスケープシーケンスはデフォルトでは無効になっています。 つまり中身の文字列値に終了パターン(この例では「'__here__」)が出現しない限り、 任意の文字列をそのまま含めることができます。

この記法は、既に述べた「@['」と「']」と書く記法において、「[」と「]」の部分が識別子に置き換わったものと考えてもとりあえずは構いません。 ただし「@['」と「']」と書く記法では、「=」を追加指定することができましたが、この識別子による記法ではそれはできません (というより必要に応じて識別子自体を自由に変えればよいので「=」を指定する意味がそもそもありません)。

エスケープシーケンスを有効化する方法や、生の改行コードを\r\nに強制的に変更する方法などはすべて 「@['」と「']」と書く記法と同様の指定で可能です。
「@」で始まる記法(ヒアドキュメント)についてはまだまだ多くの機能がありますが、 この「文字列」のセクションで紹介するのはここまでにしておきます。 ヒアドキュメントに関する厳密な仕様と残りの機能については「ヒアドキュメント」のセクションで詳しく述べます。


ここまでShift_JISの全角文字を「"」で囲うことの問題点、そしてそれに対処するための「@'」と「'」、 あるいは「@['」と「']」で囲う記法について説明してきました。

しかし、そうはいっても実際のコーディングにおいては、とりあえず「"」で囲いたい状態にしておきたい場合があるかもしれません。 例えばC言語などから持って来たコードをRarakuに移植するような状況を考えます。 元のC言語では文字列リテラルがShift_JISで書かれており、全角文字も「"」で囲われているものとしましょう (しかも文字コードはとりあえず元のままでなければならないとします)。 これをRarakuに持ってくる場合、最終的には「@'」と「'」、あるいは「@['」と「']で(全角文字の部分は) すべて書き換えるのが理想的ではあるのですが、修正箇所が多いなどのため、なるべく「"」のままにして修正箇所を最小限に抑えたいとします。

繰り返しになりますが「"」で囲う場合に問題となるのは、全角文字そのものの後半に「\」が含まれる場合です。 最低限、そのような明らかに危険な全角文字は確実に検出してコンパイルエラーにすることができればとりあえずはよいわけです。

Rarakuではそのような検出を行うための若干裏技のような方法が存在します。 以下のようにpragma文をファイルの最初の方に宣言しておくと、 そのファイルの文字コードを自動的に判別させることができます。

pragma charset_auto:乙

最後が「乙」となっているのは誤植でも冗談でもなく、 ここは本当に「乙」と書かなければなりません(他の漢字ではダメです)。 Shift_JISであってもEUC-JPであってもUTF-8であっても「乙」です。 それどころかたとえ日本以外の文字コード、例えば中国のGB2312/GB18030、台湾/香港のBig5、韓国のJohabであっても ここに指定するのは必ずです!

全角文字圏(漢字を使用する圏)専用の指定であるため、 このようにやや横暴な仕様になっています。 尚、これはRaraku言語において文字列リテラルやコメントの外で漢字を指定できる唯一つの記法となっています。

Rarakuでこの指定がなされた場合、次の手順で「"」内における危険な記述を検出します。

  1. 「乙」のバイト列がどうなっているかでそのファイルの文字コードが何であるかを推定する。
  2. その文字コードの全角文字が「\」を含むか否かを推定する。 含むものであるならば次の手順3を行い、含まないものであれば何もせずここでOKとする。
  3. 「"」で囲った中身の文字列値に(十六進数で)81以上の文字が含まれ、かつその直後に「\」が来るようなパターンが存在する場合、 それは危険性のあるパターンとしてコンパイルエラーとする。

上記の3によって、「ソ」や「表」のような文字列が「"」内に含まれていた場合は確実にコンパイルエラーになります。

ただしこの判定は若干厳しく、特に問題のないと思われる文字、例えば「ダ」などでも その直後に「\」があるならばコンパイルエラーとなります。 例えば以下のように、フォルダ名が日本語であるWindowsのパスを「"」で囲っていた場合、 「ダ\」の部分がコンパイルエラーとなります。

"C:\\マイフォルダ\\マイファイル.txt"

厳密に言えばこれは放置しておいてもとりあえず問題ない部分ですが、これはやむを得ません。 コンパイルエラーが出た場所は観念して「@['」と「']」等で書き換えましょう。

文字列変数/定数


文字列変数/定数はvaria/const文により宣言して使いますが、その際に指定できる型は二つあります。 string型とconststr型です。 どちらも文字列リテラルを格納できる型となりますが、string型はその値の文字列の内容が変更可能であることを示し、 conststr型は大雑把に言えばそれが変更不可であることを示します(これについての正確な挙動は後で詳しく述べます)。

conststrはconstとstringを合体させたような名前になっています。 ただし、ちょっとややこしいですが後述するconst string型とconststrは意味が異なります。 因みにconststrを何と発音するかですが「コンストストァ」(適当)とかでいいんじゃないかと思います。

以下では文字列変数/定数を文字列リテラルで初期化しています。

/* string型の文字列変数str を 文字列リテラル"hello"で初期化 */
varia str string = "hello"

/* conststr型の文字列変数cstr を 文字列リテラル"world"で初期化 */
varia cstr conststr = "world"

/* conststr型の文字列定数ccstr を 文字列リテラル"Hello world"で初期化 */
const ccstr conststr = "Hello world"

/* string型の文字列定数text を 文字列リテラル@'I say "Hello".'で初期化 */
const text conststr = @'I say "Hello".'

初期化値が文字列リテラルで与えられる場合、varia/const文でのconststrは省略できます。 例えば以下の通りです。

/* varia str conststr = "hello" と全く同じ */
varia str = "hello"

この場合のstrはconststr型になることに注意してください(string型ではありません)。

同様に初期化値が単独の文字列変数/定数で与えられる場合、varia/const文でのconststr/stringは省略できます。 例えば以下の通りです。

varia csv  = "hello"
varia cstr = csv /* csvはconststr型の変数 */
varia sv string = "hello"
varia str  = sv  /* svはstring型の変数 */

この場合、cstrはconststr型、strはstring型と自動的に推論されます。

文字列の連結


Rarakuでは、文字列連結演算子「&」を使うことにより、連結された文字列を作ることができます

これはNim言語と似ています。 Javaなど一般的な言語では「+」がこの役割を担っていることも多いですが、 Rarakuでは数式の「+」と混ぜて使う状況で紛らわしいので「&」を採用しています。

例えば以下では、str1 と " " と str2 を連結した文字列を作り、それをstr3へ代入しています。

varia str1 = "hello"
varia str2 = "world"
varia str3 string
/* str1 と " " と str2 を連結した文字列を作り、それをstr3へ代入 */
str3 = str1 & " " & str2

文字列連結演算子「&」の使用において、整数変数/定数/リテラルや実数変数/定数/リテラル、bool型変数/定数/リテラルなどを混ぜて一緒に指定すると、 それらの中身の値が自動的に文字列に変換されます。

varia ival = 10
varia rval = 2.5
varia bval = false

varia str = 
	"ival=" & ival & "\n" &
	"rval=" & rval & "\n" &
	"bval=" & bval & "\n"
Rrk_print( str )

上の実行結果は以下のようになります。

ival=10
rval=2.500000
bval=false

この「&」演算子は括弧の中では省略できることがあります。 例えば以下のように全体をまず括弧で括り、その中に先ほどと同様に文字列リテラルと識別子を交互に記述する場合、 その間に挟まっている「&」は省略可能です。

varia ival = 10
varia rval = 2.5
varia bval = false

varia str = ( 
	"ival=" ival "\n"
	"rval=" rval "\n"
	"bval=" bval "\n"
)
Rrk_print( str )

あるいは括弧の外であっても、文字列リテラル同士が隣り合う場合に限り「&」演算子を省略できます。 例えば以下の通りです。

/* varia str = "hello " & "world" & "\n" と書いたのと全く同じ */
varia str = "hello " "world" "\n"
Rrk_print( str )

識別子と識別子が連続する場合は(括弧内外に関わらず)「&」を省略することはできません。 つまり以下のように記述した場合、コンパイルエラー(Invalid dual token)となります。

varia str1 = "hello"
varia str2 = "world"
varia str3 string
/* コンパイルエラー : str1 と str2 を連結した文字列を作りたいなら str1 & str2 とする必要がある */
str3 = str1 str2


さらに括弧内において、文字列リテラルと「( ... )で囲まれた式」が交互に現れた場合、その間の「&」を省略できます。 例えば以下のように記述することが可能です。

/* OK */
const msg = ( "ans=" ( 1 + 1 ) "\n" )

さらに括弧内において、文字列リテラルと「関数呼び出し」で囲まれた式が交互に現れた場合、その間の「&」を省略できます。 「関数呼び出し」については後述しますが、とりあえず今は識別子の後ろに「( ... )」という形が来るものだと考えてもらって構いません。 例えば以下のように記述することが可能です。

const s = ( ":" func1() ":" func2() ":" )

括弧内で「&」が省略可能と述べましたが、 これは式としての括弧内のみならず、「関数呼び出し」での括弧内でも全く同様です。 例えば以下のようにRrk_print関数の括弧内において、「&」を省略した記述ができます。

varia ival = 10
varia rval = 2.5
varia bval = false

Rrk_print( 
	"ival=" ival "\n"
	"rval=" rval "\n"
	"bval=" bval "\n"
)

文字列連結演算子としては「&」演算子の他に「&=」演算子があります。 実際の用途では「&」は省略可能なことが多いため、むしろ「&=」を明示的に使う機会の方が多いかもしれません。

これはJavaなどで使われる「+=」演算子と似ています。

「&=」演算子を使うことにより、右辺の文字列を左辺の文字列の後ろへ連結できます。 ただし連結される側(左辺値)の文字列はstring型である必要があります。

conststr型に対しては、「&=」演算子を使うことはできません。

例えば以下では str1 に "hello"、" "、str2 を順に後ろに連結しています。

varia str1 string
varia str2 = "world"
str1 &= "hello"
str1 &= " "
str1 &= str2

右辺については文字列以外として整数型、実数型、bool型などを与えても構いません。 その場合右辺は自動的に文字列に変換されます。 例えば以下では str1 に int型の変数や数値リテラルを順に後ろに連結しています。

varia str1 string
varia ival = 10
str1 &= ival
str1 &= 20

文字列リテラルの変数埋め込み


Rarakuでは「@」で始まる記法(ヒアドキュメント)を用いた場合、(まだ紹介はしておりませんでしたが)変数埋め込み機能を使って、 文字列の連結とほぼ同様のことを実現できます。 これは文字列リテラル内部に(文字列リテラルの外部で宣言済みの)変数を埋め込み、その値で展開するというものです。

これを有効化するには、例えば「@['」と「']」で囲む記法においては、「@」と「[」の間に「$」を指定(つまり「@$['」と指定)します。 この場合、文字列リテラル内部で「${変数名}」というパターンがあると、その部分を(宣言済みの)変数の値で展開します。 例えば以下の通りです。

const val = 10
const str = @$['aaa${val}bbb']

Rrk_print( str )

上記の例では、文字列リテラルの開始パターンは「@$['」となっています(「$」を指定しています)。 そのため、文字列リテラル内の「aaa${val}bbb」の部分は、(文字列リテラルの外にある)変数valの値(今回は10)で置き換えられます。 すなわちこの部分は結果的に「aaa10bbb」と展開されます。 これは文字列の連結を使って、const str = ( "aaa" val "bbb" ) としたのと同じことです。 この例の場合、どちらを使っても簡潔さに大差はないかもしれません。

「@,$」と指定した場合は「$」をエスケープ文字とする指定でしたが、 「@$」と書いた場合($の前にカンマがない場合)、「$」を変数埋め込み用記号とする指定になることにご注意ください。

尚、開始パターンにおいてこの指定をせずに単に「@['」と書いた場合、 上記の「aaa${val}bbb」の部分は、もちろん字面通り「aaa${val}bbb」となります。
「"」で囲う記法については、文字列の変数埋め込みはサポートされません。
文字列の変数埋め込みに関してのさらなる詳細は「ヒアドキュメント」のセクションで述べます。

conststr型の詳細


conststr型ではそれが指し示す文字列を加工して変更することは許可されません。 例えば「&=」を使った連結などが禁止されます。 あるいは、関数の引数においてこの型で宣言された文字列は、その関数内部においてその値の書き換えが禁止されます。

これはもっと厳密に言えば、conststr型で宣言された識別子は、以降その識別子を介した値の変更ができないということになります。 逆に言えばその識別子を介さない場合(別の識別子が同じものを参照している場合)、値の変更ができてしまう場合もあるということです。 これに関してはすぐ後の代入の項目で述べます。

const文やvaria文、string型やconststr型が混在した宣言の違いがわかりにくいかもしれません。 variaとconst、stringとconststrを使った全ての組み合わせを以下に列挙してみます。

const cc_str conststr = "world"
varia vc_str conststr
varia vs_str string
const cs_str string   = "hello"

  • const cc_str conststr
  • cc_strへの代入やcc_strそれ自体の内容の書き換えなどがすべて禁止されます。

  • varia vc_str conststr
  • varia文でconststr型変数を宣言した場合、初期化は省略できます。 また以降の代入がすべて許可されます。 すなわち以下の文でも問題ありません。

    varia vc_str conststr
    vc_str = "world"
    vc_str = "apple"
    

    conststr型変数 vc_str に別の文字列を代入した場合、vc_str 変数はその新しく代入した値の実体を新しく指し示します。 これは代入される前の元の実体を直接加工して書き換えているわけではないことに注意してください。 上の例では"apple"を代入していますが、これは"world"の実体を書き換えているわけではありません。 "world"と"apple"の実体はそれぞれ別の領域に元々存在しており、vc_strは単にそれを指し示しているにすぎず、 代入という操作ではその指し示す先が変わっているだけになります。

    Rarakuでは文字列リテラルの実体はすべてコンスタントプールと呼ばれる領域に存在しています。 これは文字列変数が存在する領域とは全く別の領域にあり、プログラムがロードされるタイミングで確保/初期化されます。

    このような単に指し示しているだけの状態を参照またはシャローコピーと呼びます。 シャローコピーは実体に対して何か加工しているわけではないので、一般に高速となります。 これに対し、「=」の右側(右辺値)を元に、それと全く同じ値を持つ領域を新たに確保する(クローンを作る)ことをディープコピーと呼びます。 ディープコピーは新しい実体を作るため、一般に低速となります。

  • varia vs_str string
  • vs_strへの代入やvs_strそれ自体の内容の書き換えなどがすべて許可されます。

  • const cs_str string
  • const文でstring型定数を宣言した場合(以降const string型と呼びます)、初期化が必須となります。 また以降の代入がすべて禁止されます。 ここで言う代入とは「=」による代入のみであり、「&=」を使った文字列の追加は含まれないことに注意してください。 const string型の場合、関数の引数にそれを与えたときその内容が書き換わる可能性があります。

    上記の説明だけですと、const stringが何のために存在しているのかわからないかもしれません。 Rarakuではconststr型も用意されていますので、普通に考えるとそちらで十分に思えます。

    Rarakuでのconst string型は、主に関数の引数に指定して使うことを想定しています。 関数についてはまだ説明していませんので、これについてここで学習する必要はありませんが、 少し言及すると関数の中で文字列strの内容を更新するつもりでうっかり「str = "hello"」と書いてしまう類のエラーを検出するために使用します。 これについては「ユーザ定義関数」のセクションで詳しく述べます。
    Rarakuの文字列型ではconst指定されたものをvaria指定されたものへ代入することが可能です。 すなわち以下は許可されます。

    const cc_str conststr = "hello"
    varia vc_str conststr = cc_str /* OK */
    
    const cs_str string = "hello"
    varia vs_str string = cs_str /* OK */
    

    これはちょうどconst intをvaria intに代入可能なのと同じです。

    一方、これは後の章で述べる配列とは異なる挙動になっています (Rarakuの配列では、const 配列をvaria 配列に代入することが許可されません)。 そもそも文字列とは配列的な性格を持つデータ構造であるという見方もできます。 その観点からすればconst stringをvaria stringに代入可能とすべきではないという見方もできますが、 Raraku文字列においてはそのような代入が可能となっています。 その理由は、Rarakuの文字列型におけるvariaとconstの指定の違いは、 単に「=」による再代入が可能か否かだけであり、それ以外にその二者で違いはないからです。

    そもそもstring型は(書き換えが生じないことを想定したconststr型の対極として) 書き換えが生じ得る状況でのみ使用することを想定したデータ構造です。 その観点から、const指定であっても中身の文字列値の書き換えを禁止するような制限は付加されません。 この点は配列とは全く異なる性質になります(const 配列ではその要素の書き換えは一切禁止されるからです)。

    一方、conststr型は、書き換えが生じ得ない状況でのみ使用することを想定したデータ構造です。 その観点から、varia指定であっても中身の文字列値の書き換えは禁止されます。 この点も配列とは全く異なる性質になります(varia 配列ではその要素の書き換えは許可されるからです)。

    結局、string型とconststr型のいずれの場合も、書き換えの是非に関してはconstとvariaで差はありません。 つまり文字列においてvariaとconstの指定の違いは、単に「=」による再代入が可能か否かの違いしかないことになります。

stringとconststrの代入


Rarakuではstring型同士の代入、conststr型同士の代入は勿論、string型からconststr型へ、 あるいはconststr型からstring型への代入も可能です。 この場合起こり得る組み合わせのパターンは全部で4通りですが、それぞれについての挙動は以下のようになります。
  • string型からconststr型へ代入する場合

  • 文字列の内容はディープコピーされず、単に右辺値を参照します。

    varia str1 string   = "hello"
    varia str2 conststr = str1 /* point to str1 */
    

    実際のプログラミングではこの組み合わせの代入は頻繁に登場すると思われます(例えば代入先が関数の引数である場合など)。 そのためRarakuでは効率上の理由からこれを(ディープコピーではなく)参照とする仕様を採用しています。

    ただしこの仕様のため、「conststrでさえあれば絶対に値が変わらない」とは言い切れなくなるケースが生じます。 例えば以下をご覧ください。

    varia str1 string   = "hello"
    varia str2 conststr = str1 /* point to str1 */
    str1 &= " world"       /* ここでstr1を介して値を変更 */
    Rrk_print( \=str2\n ) /* "hello world"と表示される */
    

    上記ではstr2はconststrで宣言されており、 これで確かにstr2を介した値の変更は禁止されます。 ところがstr2はstr1と同じ領域を指し示しており、 すぐ下でstr1を介した値の変更が行われています。 そのためstr2の値も変化したかのように見えるわけです。 もっとも上記はかなりわざとらしい順番と組み合わせで記述した例ではあるのですが、 このような状況には注意が必要です。

  • conststr型からconststr型へ代入する場合

  • 文字列の内容はディープコピーされず、単に右辺値を参照します。

    varia str1 conststr = "hello"
    varia str2 conststr = str1 /* point to str1 */
    

  • string型からstring型へ代入する場合

  • 文字列の内容はディープコピーされず、単に右辺値を参照します。

    varia str1 string = "hello"
    varia str2 string = str1 /* point to str1 */
    

  • conststr型からstring型へ代入する場合

  • 文字列の内容は自動的にディープコピーされます。 このとき右辺値とはまた別の(しかし値は同じのstring型の)実体が作られ、それを参照する形になります。

    varia str1 conststr = "hello"
    varia str2 string   = str1 /* deep-copy */
    

    ただし関数の引数がstring型の場合、特例としてその引数にconststr型変数/定数や文字列リテラルを直接指定することは許可されず コンパイルエラーとなります。 関数やその引数については次のセクションで詳しく述べます。

    万一このような指定がどうしても必要なら、後のセクションで述べるキャストを使えば強引に指定可能ではありますが、 その場合でもやはりディープコピーは行われます。 関数の引数がconststr型ではなくわざわざstring型となっている理由は、多くの場合その文字列の実体に対して 関数の中で何らかの加工を行いたい(とその関数の作成者が考えている)からです。 関数の引数がstring型に対して(関数のユーザが)string型を与えた場合は何も問題ありませんが、 一方でconststr型等を与えた場合、ディープコピーが行われた元とは別の実体が関数内で加工され、 元の実体には何の加工もされないことになります。

参考: 文字列リテラルによる代入/初期化におけるディープコピー
文字列変数/定数を文字列リテラルによって代入/初期化する場合、 ディープコピーが発生する場合と発生しない場合があり、 それぞれ以下のようになります。
  • string型変数を文字列リテラルで初期化する場合

  • コンスタントプールにある文字列がstring型変数へと自動的にディープコピーされます。

    varia str string = "hello" /* deep-copy */
    

  • conststr型変数を文字列リテラルで初期化する場合

  • コンスタントプールにある文字列を単に参照します。 ディープコピーは発生しません。

    varia str conststr = "hello" /* point to constant-pool */
    

Close


また文字列連結演算子「&」などで右辺に文字列が新しく生成される場合、 左辺の文字列変数は最終的にそれを参照する形になります。 例えば以下をご覧ください。

varia str = "hello" & " world"

上記では「"hello"」と「" world"」という二つの文字列の実体がまずあり、 文字列連結により「"hello world"」という値を持つ第三の実体が別途生成され、 最終的にstrはそれを参照する形になります。

文字列の確認表示


文字列変数の中身を確認表示するにはRrk_print関数を使います。 関数については後のセクションで詳述しますが、今の時点ではとりあえず Rrk_print( 文字列変数名 ) のように書けば その文字列変数の値を確認表示できると考えてください。

例えば以下のようになります。

varia str string = "hello"
str &= " "
str &= "world"
Rrk_print( str ) /* コンソール上に hello world と表示される */

あるいは以下のように直接文字列リテラルを指定することもできます。

Rrk_print( "hello world" ) /* コンソール上に hello world と表示される */

あるいは以下のように文字列リテラルと文字列変数を連結した結果を指定することもできます。

varia str string = "hello"
Rrk_print( "str=" str ) /* コンソール上に str=hello と表示される */

あるいは以下のように文字列リテラルと数値を並べて書くと、数値の部分が自動的に文字列に変換され、それらを連結した結果を表示します。

varia ival int = 100
Rrk_print( "ival=" ival ) /* コンソール上に ival=100 と表示される */

「"」で囲った文字列リテラルにおいて、その内部の「\」文字はエスケープ文字となります。 エスケープ文字は後ろに来る文字の意味を変えます。 例えば「\n」は改行を意味します。また「"」や「\」自体を文字列の値として含めたい場合は「\"」や「\\」と記述します。

Rrk_print( "hello world\n" ) /* コンソール上に hello world と表示され、最後に改行される */

「@['」と「'] 」で囲った文字列リテラル(ヒアドキュメント)の場合、デフォルトではエスケープ文字は無効となります。 また「']」自体を文字列の値として含めたい場合は、「@[='」と「'=]」で囲います。

Rrk_print( @['C:\User\Default'] "\n" ) /* コンソール上に C:\User\Default と表示され、最後に改行される */

これまでの知識を使うともう少し体裁の整った形で数値などを表示できます。 以下は文字列リテラル、数値、文字列リテラルの順に並べて書き、数値の部分を自動的に文字列に変換しつつ、それらを連結した結果を表示し、 最後に改行しています。

varia ival int = 100
Rrk_print( "ival=[" ival "]\n" ) /* コンソール上に ival=[100] と表示され、最後に改行される */

バックスラッシュ演算子


Rarakuではバックスラッシュ演算子と呼ばれるものがあります。 これは文字通り「\」で始まる演算子であり、その後ろの文字列に何か特別な意味を与え、さらにそれを文字列リテラルに変換します (この変換が行われるのはコンパイル時です)。 例えば「\n」は「"\n"」という文字列リテラルに変換されます。

これまで数値変数を確認表示するのに Rrk_print( ival\n ) といった表記を使ってきましたが、 ivalの後ろの「\n」の部分がまさにバックスラッシュ演算子になります。 つまり「ival\n」は「ival"\n"」と書いたのと全く同じです。 もっと言えば「ival & "\n"」と全く同じです。 このとき ival が文字列に変換され、最終的に全体が連結されることになります。

「\n」ではなく「"\n"」と書いても全然構わないのですが、改行文字というのは至る所で頻繁に使われるものであり、 また特に(他の文字列と一緒ではなく)単独でこれが現れる場合(典型的にはRrk_print( ival"\n" )というような場合ですが)、 その度に前後にダブルクォートを付けた上でこれを記述する必要があります。 一方「\n」と書けば文字数が2文字で済む上、キーボードからシフトキーを使わずに入力することができます。 何より「ival\n」の方が見た目もすっきりします。

改行を連続させたい場合は「\n\n」のように記述することもできます。 ただこのように連続させた場合についてはバックスラッシュ演算子はあまり向いていません。 というのもその連続数が増えてくると結局「"\n\n"」とダブルクォートで囲って書くのとほぼ差はなくなって来るからです。 バックスラッシュ演算子はこのような連続した状況ではなく、単独のものが飛び飛びで頻繁に現れるような状況に向いています。

ivalと後ろの「\n」は(今までそうしてきたように)スペース等を入れずにくっ付けて書くことができますが、 間にスペースを入れても構いません。結局「ival "\n"」ということですから結果は全く同じになります。 一方、ivalの前に「\n」を書く場合は、以下のように「\n」と「ival」の間に必ずスペースを一つ以上入れる必要があります。

varia ival=5
Rrk_print( \n ival\n )

もしもこれをくっつけて書くと「\nival」までが一つの塊として字句解析されてしまい、コンパイルエラーとなります。

バックスラッシュ演算子は「\n」以外にもいくつかあります。 以下に現在サポートされているものをまとめます。

  • 「\n」
  • 文字列リテラル「"\n"」に変換され、改行(コード0x0a)を意味します。

  • 「\r」
  • 文字列リテラル「"\r"」に変換され、改行(コード0x0d)を意味します。

  • 「\t」
  • 文字列リテラル「"\t"」に変換され、タブ文字を意味します。

  • 「\c」
  • 文字列リテラル「","」に変換され、カンマを意味します。

  • 「\'」
  • 文字列リテラル「"'"」に変換され、シングルクォートを意味します。

  • 「\q」
  • 文字列リテラル「"\""」に変換され、ダブルクォートを意味します。

  • 「\w」
  • 文字列リテラル「" "」に変換され、半角スペースを意味します。

  • 「\,」
  • 文字列リテラル「", "」に変換され、カンマ+半角スペースを意味します。 「\c」との違いは、後ろに半角スペースが自動的に一つ追加されることです。 大抵の場合、この半角スペースがあった方が表示は見やすくなります。

  • 「\=識別子」
  • 識別子の前に「"識別子="」という値の文字列リテラルが挿入されます。 これは例を見た方がわかりやすいでしょう。

    varia ival=5
    Rrk_print( \=ival\n )
    

    上の例では「\=」の直後にivalという識別子が続いています(「\=」と識別子の間にスペース等を入れることはできません)。 このとき、「\=ival」の部分は「"ival=" ival」というトークン列に自動的に変換されます。 これを実行すると以下のように表示されます。

    ival=5
    

    同じ結果を表示させるのに「\=」を使わずに書くと「"ival=" ival」と冗長な記述になってしまいます。

最後に上記を組み合わせた少し複雑な例を見てみましょう。

varia
	ival=10,
	jval=20,
	kval=30,
;
Rrk_print(  \=ival \, \=jval \, \=kval \n )
Rrk_print( "ival=" ival ", jval=" jval ", kval=" kval "\n" )

上記はival, jval, kvalの値を一気に確認表示するというものです。 二つのRrk_printを使った文がありますが、前者がバックスラッシュ演算子を使った場合、後者がそれを使わなかった場合です。 実行結果は以下のようにどちらも全く同じです。

ival=10, jval=20, kval=30
ival=10, jval=20, kval=30

バックスラッシュ演算子単独ではわずかな差でしかないのですが、 このような混み合った例ではその差が積み重なり、 全体としてバックスラッシュ演算子を使った方がすっきりします。

文字列の文字をインデックスでアクセスする


文字列はその文字列長の範囲内で各文字にアクセスすることができます。 Rarakuでは、文字列識別子 [ インデックス ] という書式を式中に書くことにより、その文字列の各文字にアクセスできます(この書式はC言語等と同じです)。 最初の要素のインデックスは0、次の要素のインデックスは1、その次の要素のインデックスは2、 このように0から始めて1ずつインデックスが増加して行く形になります。 例えば以下のようにstrという識別子の文字列があり、この文字列長が3の場合、str[0]、str[1]、str[2] のように各文字にアクセスできます。

varia str string = "abc"
Rrk_print( "str[0]=" str[0] \n )
Rrk_print( "str[1]=" str[1] \n )
Rrk_print( "str[2]=" str[2] \n )

上記を実行すると 97, 98, 99 のように表示されると思います。 これは各文字の文字コードを示しています。 文字コードではなく文字そのもので表示して欲しい場合は、後述する文字列標準関数RrkChar_toStrを使います。 これについては「文字列のための標準関数」のセクションで詳しく説明します。

Rarakuではこのようなインデックスでのアクセスは読み込みでのみ許可されることに注意してください。 つまりこれを書き換えるような処理は一切許可されません。 例えば以下はコンパイルエラーになります。

varia str string = "abc"
str[1] = 'B' /* コンパイルエラー: インデックスによるアクセスで代入しようとしている */

上記ではstr[1]に文字'B'を代入しようとしていますが、これは書き換えにあたる処理であるため、許可されません。 このような処理が必要な場合、後述する文字列標準関数RrkStr_set_cを使います。 これについては「文字列のための標準関数」のセクションで詳しく説明します。

strの文字列長がNの場合、その最終文字のインデックスは、その文字列長から1を引いたN-1となることに注意しましょう。 つまりstr[N-1]が最終文字となります。 このときもしもstr[N]のようにした場合、これはnull文字(整数値としては0の文字で'\0'とも表記される)となります。 またNより大きい値をインデックスとして与えた場合(例えばstr[N+1]などとした場合)ランタイムエラーとなります。

文字列のまとめ


Rarakuの文字列は文字列リテラルと文字列変数/定数に大別されます。 文字列リテラルでは「"」で囲う記法と「@'」と「'」で囲う記法(ヒアドキュメント)があります。 通常は「"」で囲う記法で構いませんが、「"」文字自体や全角文字を含むようなケースでは後者を使用することを推奨します。 そのほかバックスラッシュオペレータのように特殊な記法もありますが、これも文字列リテラルの一種となります。

文字列変数/定数に関してはstring/conststrの二種類があり、 前者は文字列の連結など内容の書き換えが可能ですが、後者はそれが許可されないものになります。 Rarakuの文字列の連結では多くの場合、文字列連結演算子「&」の指定が省略可能です。



目次に戻る

文字列のための標準関数

基本事項


Rarakuでは文字列に関して様々な操作を行うための標準関数が用意されています。 ここではまずこれを使うために知っておくべき基本事項を説明します。

Rarakuでは言語仕様を小さく抑えるため、 言語レベルでの文字列のサポート機能をあまり提供していません。 C言語と同様、足りない機能については標準関数に頼るというスタンスを取ります。

文字列のための標準関数を使うには、予め import std/str を実行しておく必要があります。 これはimport文と呼ばれます。

import文は「Rarakuの所定の位置」にあるrrkh(RaRaKu Headerの略)ファイルを読み込みます。 rrkhファイルはRarakuヘッダと呼ばれ、C言語におけるhヘッダファイルに相当します。

rrkhの読み方は「ララクエイチ」とかでいいんじゃないでしょうか(適当)。

例えば import std/str の場合、Rarakuのインストールディレクトリ内のrrk_pkg/std/str.rrkh ファイルを読み込みます。 これにより rrk_pkg/std/str.rrkh 内で宣言されている関数が使えるようになります。

拡張子「.rrkh」の指定は不要であることに注意してください。 またここでのディレクトリ区切り文字は必ず「/」を使います(たとえWindows上であってもです)。 「..」を用いて「Rarakuの所定の位置」からの相対パスを指定することもできます(ただし絶対パスでの指定はできません)。 「Rarakuの所定の位置」は変更することもできますが、これについては「ヘッダファイルとリンケージ」のセクションで詳しく説明します。

その後に実際に文字列のための標準関数を使います。 例えば以下のようになります。

import std/str

varia str1 string
varia str2 = "world"
varia leng1 uint = RrkStr_leng( str1 )
varia leng2 uint = RrkStr_leng( str2 )

上の例の「RrkStr_leng( str1 )」のように、「名前( 変数または定数名 )」という形式になっているものを関数呼び出し、 あるいは単に関数と言い、「( 変数また定数名 )」の内側の変数(または定数)のことを関数の引数と言います。 また、ある関数呼び出し「F(A)」のことを関数Fの引数としてAを指定すると言ったり、 関数FをAを引数として呼び出すなどと言うこともあります。

関数の引数は(関数ひとつにつき)複数あることもあり、その場合は「,」で区切って指定します。 また逆に一つもないこともあり、その場合は括弧の中味は空になります。

呼び出された関数の中ではユーザ側からは見えない様々な処理を行って一つの値を生成し、その値を呼び出し側へと提供します。 その値を関数の戻り値と呼び、結果的に関数が呼び出されたちょうどその位置に、あたかもその戻り値が指定されたのと同じ効果になります。

上の例では、RrkStr_leng関数の引数として str1 を指定しており、str1 の文字列長がこの関数の戻り値となります。 今回の場合、str1 はまだ空文字列なのでその文字列長は 0 となり、結果的にRrkStr_lengの戻り値は 0 となり、 leng1 には 0 が代入されます。

また、次の文ではRrkStr_leng関数の引数として str2 を指定しており、str2 の文字列長がこの関数の戻り値となります。 この場合、str2 は "world " という文字列なのでその文字列長は 5 となり、結果的にRrkStr_lengの戻り値は 5 となり、 leng2 には 5 が代入されます。

Rarakuの標準関数の名前にはすべてRrkというプレフィックスが先頭につきます。 例えば、RrkStr_leng などもそうです。 単にlengなどとせず(英単語としてはlengthが正しいですが、ここは少し短く後ろのthを省略しています)このようにしている理由は、 名前衝突を避けるため、またその関数の所属を一目でわかるようにするためです。

また副次的な効果として、Rrkと最初についていた方がGoogle検索などによりRarakuの関数のリファレンスやサンプルコードに辿りつく可能性が高くなると期待できます。

いまどきのテキストエディタならば入力補間が効きますので、少々冗長で長い名前でも記述の手間はあまり心配はないと思います (式中に書きたいならば少し短い名前の方がよいということはありますけども)。 またこれがどうしても手間な場合、関数のエイリアスを使うことができます。 関数のエイリアスについては、後の「functype文とコールバック関数」のセクションで詳しく述べます。

RrkStr_lengの場合、引数を一つだけ指定する決まりになっていました。 しかし関数の種類によっては引数を複数個指定する決まりになっているものもあり、その場合はそれぞれの引数を「,」区切りで指定します。 このとき最初の引数を第1引数、2番目の引数を第2引数のように呼びます。 例えば以下のようになります。

import std/str

varia str = "world"
varia ch0 int = RrkStr_at( str, 0 )
varia ch1 int = RrkStr_at( str, 1 )

上の例のRrkStr_atでは、第1引数には文字列、第2引数には整数を指定しなければなりません。 この取り決めは、rrk_pkg/std/str.rrkh ファイル内に関数宣言という形で記載されています。 この関数宣言の詳しい読み方についてはすぐ後で解説しますが、とりあえずここでは置いておきます。 またこのRrkStr_atの詳細については全般的な基本説明が終わった後で各論として改めて述べます。

関数定義


続いて標準関数RrkStr_findIdxLを例に関数定義について説明します。 この関数は rrk_pkg/std/str.rrkh ファイル内で以下のように定義されています。

function RrkStr_findIdxL( str conststr, begin uint, ptn conststr ) uint
{
	return RrkStr_findIdx( str, begin, ptn, RrkStrFindOrder_e_FromFront );
}

上記はこの関数の窓口の情報を与えるものであるというイメージをもってもらえれば今は十分です。 この関数ではこの後さらに { ... } で囲まれた行が続きますがこれは関数内の実装であり、これを関数定義と呼びます。 関数のユーザは関数定義の中身は無視して構いません。 通常の使用においては、窓口の情報さえあれば十分だからです。

この記述において最初の「function」はこれが関数定義の開始であることを示します。 続いて関数の名前(この例ではRrkStr_findIdxL)が来ます。 その直後に ( ... ) で囲まれた記述が来ますが、この中に関数の引数の羅列を記述します。 ( ... ) 内の記述が終わったら最後に関数の戻り値の型(この例ではuint)が来て窓口の情報は終了となります。

関数の引数の記述についてもう少し詳しく見てみます。 先ほどの関数定義(の第1行目)を以下にもう一度再掲します。

function RrkStr_findIdxL( str conststr, begin uint, ptn conststr ) uint

最初に「str conststr」とありますがこれは第1引数としてconststr型を指定できるということです。 strは単に引数につけられた名前(変数名)です。 以下の説明では第N引数という表記の替わりにこの名前を使ってこれを指し示すことがあります。

引数に付けられた名前は、関数定義においてその実装のコードの中で参照されます。

カンマ区切りで次の第2引数「begin uint」とありますが、これも同様に第2引数としてuint型を指定できるという意味になります。 第3引数の「ptn conststr」も同様です。

関数宣言


rrk_pkg/std/str.rrkh ファイル内を見ますと、ほとんどのケースでは「natvfunc」といったキーワードで始まり、 { ... } で囲まれた行が続かない(関数定義がない)記述となっています。 例えば以下の通りです。

natvfunc RrkStr_leng( str conststr ) uint

natvfunc RrkStr_at( str conststr, idx uint ) int 

つまり今度は本当に窓口としての情報しか記述されていないわけですが、これを関数宣言と呼びます。 これは関数定義が別のファイルに存在して隠蔽されていることを意味します。

この宣言において最初の「natvfunc」はこれがネイティブ関数の宣言であることを示します。 「natvfunc」以外にも「global」といったキーワードで始まることもあります。

「function」や「global」で始まる通常の関数はその実装をRaraku言語で行っていますが、 一方で「natvfunc」で始まるネイティブ関数は、その実装をC言語によるライブラリで行っています。

ただ今の段階ではとりあえずこれら3つはほぼ同じものと考えて結構です。 このあたりの詳細については「ヘッダファイルとリンケージ」のセクションで詳しく説明します。

関数宣言は必ずヘッダファイルに記述します(その他のファイルに書くことは許可されません)。 また関数宣言の文末に「;」を書く必要は通常ありません。 ただ関数宣言の直後に無名ブロックが続くような状況も一応想定はできます(滅多にないとは思いますが)。 その場合、それが関数定義と判断されないようにするため、関数宣言の文末の「;」は必須となります。

標準関数 RrkStr_leng


RrkStr_leng は、与えられた文字列の文字列長を取得する関数です (既に登場した関数ですが、ここで改めて紹介します)。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

natvfunc RrkStr_leng( str conststr ) uint

既に述べたとおり、この関数は引数として与えた文字列の文字列長を返します。

標準関数 RrkStr_at


RrkStr_at は、与えられた文字列のインデックス番目の文字を取得する関数です (既に登場した関数ですが、ここで改めて紹介します)。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

natvfunc RrkStr_at( str conststr, idx uint ) int 

第1引数には文字列を、第2引数にはインデックスを与えます。 例えばstrの値が"world"である場合、RrkStr_at( str, 0 ) では、str内の0番目の文字「w」の文字コード(整数値)がこの関数の戻り値になります。 またRrkStr_at( str, 1 ) でも同様に、str内の1番目の文字「o」の文字コード(整数値)がこの関数の戻り値になります。

この関数は文字列のインデックス表記、つまり str[idx] のように表記したのと基本的には同じ結果を得るものです。 ただしこの関数では idx に範囲外の値を与えたとしてもランタイムエラーとまではならず、null文字('\0')を返します。 その意味で str[idx] を使ったアクセスよりよく言えば安全、悪く言えば「お茶を濁した」ものになっています。

標準関数 RrkStr_set_c


RrkStr_set_c は、与えられた文字列のインデックス番目に文字を代入する関数です。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

natvfunc RrkStr_set_c( str string, idx uint, ch int ) bool

第1引数には文字列を、第2引数にはインデックスを、第3引数には代入元の文字(整数)を与えます。 例えばstrの値が"abc"である場合、RrkStr_set_c( str, 1, 'B' ) では、str内の1番目の文字「b」が「B」に置き換わり、 結果的にstrの値は"aBc"に変更されます。 この関数は文字列の値の変更に成功した場合はtrue、失敗した場合はfalseを返します。

文字列のインデックス表記では、str[1] = 'B' のような記述は禁止されていましたが、この関数を使えばこれに相当することが可能です。 この関数で idx に範囲外の値を与えた場合は何もせず、単にfalseを返します。

この関数の第3引数(ch)に0や'\0'を指定した場合、何もせずfalseを返します。 Rarakuの文字列は内部的にはC文字列(最後がnull文字('\0')で終わるような文字列)となっています。 そのため、途中に'\0'を代入するような処理を許すと文字列長の整合性がとれなくなってしまいます (RrkStr_set_cでは文字列長を自動的に変更するといったような処理は行わないものとしています)。

標準関数 RrkStr_findIdxL


RrkStr_findIdxL は、与えられた文字列からある文字列パターンが含まれる位置を取得する関数です。 (既に登場した関数ですが、ここで改めて紹介します)。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

function RrkStr_findIdxL( str conststr, begin uint, ptn conststr ) uint

この関数ではstr(第1引数)に与えた文字列内のbegin(第2引数)番目の文字から順番に探索して 最初にptn(第3引数)が現れる位置を返します。 例えば以下のような例の場合、RrkStr_findIdxLの戻り値は5になります。

import std/str

const idx = RrkStr_findIdxL( "abcdefghijk", 3, "fgh" )
if idx == Rrk_NPOS {
	Rrk_print( "Not found.\n" )
} else {
	Rrk_print( \=idx\n )
}

この例の場合、"abcdefghijk" の 3番目の文字 'd' から開始します( 0番目 を出発として数えますので 0, 1, 2, 3 で 'd' が3番目となります)。 そこから右へ探索して"fgh" の最初の文字 'f' が現れる位置(5)が戻り値となります。 もしもptnで指定された文字列がstr中に存在しない場合は、この関数は Rrk_NPOS を返します。 Rrk_NPOSとはRarakuでデフォルトで定義された整数定数であり、その値は (-1->uint) です。

標準関数 RrkStr_assign


RrkStr_assign は、与えられた文字列の任意の位置以降を、別の文字列で書き換える関数です。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

natvfunc RrkStr_assign( dst string, dst_pos uint, src conststr, src_pos uint, src_leng uint ) void

最後の戻り値の型 void は、戻り値がない関数であることを示します (よってこの関数を式中に埋め込むといった使い方はされませんし、できません)。

このvoidは(戻り値がない場合)省略できることもあります。 ただし「global」や「natvfunc」といったキーワードで始まる場合(つまり関数宣言の場合)はこれを省略できません。

この関数ではdstで与えた文字列変数の値をsrcで与えた文字列で書き換えます。 このときsrcで与えた文字列の任意の部分文字列を指定することもできます。 即ちsrc内において、src_pos番目からsrc_leng文字だけ進んだ範囲までに相当する文字列を指定できます。 さらにそれをdstの任意の位置(dst_pos番目)以降に書き込みます。

例えば以下をご覧ください。

import std/str

varia dst string   = "abcdefghijk"
varia src conststr = "HELLO"
RrkStr_assign( dst, 3, src, 0, 5 ) /* dst = "abcHELLO" */

この例では src における 0 番目から5文字分にあたる範囲、すなわち"HELLO"を dstにおける3番目、すなわち'd'のある位置以降に上書きしますので、 結果的にdstの値は "abcHELLO" となります。 このとき、dstに元々あった最後の"ijk"は、サイズとして余りますので削除され、dstの文字列長は短くなります。

逆に、srcで上書きした結果、dstの元々のサイズをオーバーする場合は自動的にサイズが拡張されて値が割り当てられます。 例えば以下のようにdst_posとして8を与えると、dstの文字列長は元々より長くなります。

import std/str

varia dst string   = "abcdefghijk"
varia src conststr = "HELLO"
RrkStr_assign( dst, 8, src, 0, 5 ) /* dst = "abcdefghHELLO" */

尚、この関数では src_leng にRrk_NPOSを指定すると、srcの一番最後尾までの範囲が与えられたのと同じ効果になります。 例えば以下のようになります。

import std/str

varia dst string   = "abcdefghijk"
varia src conststr = "HELLO"
RrkStr_assign( dst, 8, src, 0, Rrk_NPOS ) /* dst = "abcdefghHELLO" */

標準関数 RrkStr_set


RrkStr_set は、与えられた文字列を、別の文字列で完全に書き換える関数です。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

function RrkStr_set( dst string, src conststr ) void
{
	RrkStr_assign( dst, 0, src, 0, Rrk_NPOS )
}

関数定義部を見て頂くとわかる通り、これは実質的にはRrkStr_assignの引数をちょっと変えたものを呼び出しているだけのものです (このような関数をラッパーと呼びます)。 RrkStr_assignの書き換え先はdstの最初の位置、書き換え元はsrcの最初の位置から最後までです。 従って結局、dstの内容全体をsrcの内容全体に置き換える処理を行っていることになります。

これを使わなくとも「dst = src」と書けば済む話では?と思われるかもしれません。 しかし残念ながらRarakuでのこの代入は、その意図では機能しません。 この理由は後の「ユーザ定義関数」セクションで詳しく述べますが、 単に「dst = src」と書いた場合、それはdstの内容を書き換えているわけではなく指し示しているものを変更しているに過ぎず、 またその効果は関数の呼び出し元には反映されません。

標準関数 RrkStr_append


RrkStr_append は、与えられた文字列の後ろに、別の文字列の任意の範囲を連結する関数です。 rrk_pkg/std/str.rrkh ファイル内では以下のようになっています。

function RrkStr_append( str string, src conststr, src_pos uint, src_leng uint ) void
{
	RrkStr_assign( str, Rrk_NPOS, src, src_pos, src_leng )
}

関数定義部を見て頂くとわかる通り、これはRrkStr_assignのラッパーです。 RrkStr_assign では dst_pos にRrk_NPOSを指定すると、dstの一番最後の位置を指定したのと同じになります。 これにより、結果的に文字列が連結されるのと同じになります。 例えば以下のようになります。

import std/str

varia dst string   = "abcdefghijk"
varia src conststr = "HELLO"
RrkStr_append( dst, src, 1, 3 ) /* dst = "abcdefghijkELL" */

文字列連結の複合代入演算子「&=」との違いは、この関数では連結される文字列srcの任意の範囲を指定できるということです。 つまり「dst &= src」は「RrkStr_append( dst, src, 0, Rrk_NPOS )」と結果的には同じになります。

標準関数 RrkStr_replace


RrkStr_replace は、与えられた文字列の任意の範囲を、別の文字列の任意の範囲で書き換える関数です。 rrk_pkg/std/str.rrkh ファイル内での関数宣言は以下のようになっています。

function RrkStr_replace(
		dst string,   dst_pos uint, dst_leng uint,
		src conststr, src_pos uint, src_leng uint ) void
{
	RrkStr_replace_ex(
			dst,                   dst_pos, dst_leng,
			RrkStr_uint8_ary(src), src_pos, src_leng )
}

この関数ではdstで与えた文字列変数の値をsrcで与えた文字列で書き換えます。 その意味ではRrkStr_assignと似てはいますが、dstの任意の位置(dst_pos番目)以降からdst_lengの範囲にのみ書き込み、 尚且つ、その範囲より後ろのdstの内容は消滅せずそのまま残る点がRrkStr_assignとは異なります(RrkStr_replaceの方がより柔軟な処理に対応できます)。 まさに、dstのdst_pos番目からdst_lengの範囲だけをsrc側の部分文字列で置き換えている(replaceしている)わけです。 このsrc側の部分文字列は、src_pos番目からsrc_leng文字だけ進んだ範囲までとなります。

例えば以下をご覧ください。

import std/str

varia dst string   = "abcDEFGhijk"
varia src conststr = "HELLO"
RrkStr_replace( dst, 3, 4, src, 0, 5 ) /* dst = "abcHELLOhijk" */

この例では src における 0 番目から5文字分にあたる範囲、すなわち"HELLO"を dstにおける3番目から4文字分にあたる範囲、すなわち"DEFG"のある範囲に上書きしますので、 結果的にdstの値は "abcHELLOhijk" となります(RrkStr_assignとは異なり、dstに元々あった最後の"hijk"は、削除されずに残っています)。

尚、この関数ではRrkStr_assignと同様にRrk_NPOSを指定できます。すなわち以下の通りです。
  • src_pos にRrk_NPOSを指定した場合
  • srcの一番最後の位置を指定したのと同じになる.

  • src_leng にRrk_NPOSを指定した場合
  • srcの一番最後尾までの範囲が与えられたのと同じ効果になる.

  • dst_pos にRrk_NPOSを指定した場合
  • dstの一番最後の位置を指定したのと同じになる.

  • dst_leng にRrk_NPOSを指定した場合
  • dstの一番最後尾までの範囲が与えられたのと同じ効果になる.

標準関数 RrkStr_replacePtn


RrkStr_replacePtn は、与えられた文字列内に含まれる任意のパターン文字列を、別のパターン文字列に置換する関数です。 rrk_pkg/std/str.rrkh ファイル内での関数宣言は以下のようになっています。

natvfunc RrkStr_replacePtn( str string, begin uint,
		old_ptn    conststr, new_ptn       conststr,
		seek_depth uint,     delta_to_next uint ) uint

この関数ではstrで与えた文字列において、パターン文字列old_ptnを別のパターン文字列new_ptnに置き換える処理を行います。

今、文字列str内をスキャンするにあたり、そのスキャンの現在位置をカーソルと呼ぶことにします。 beginはstr内のカーソルの開始位置であり、この位置より前の部分では置換処理は行われません。通常これは 0 を指定すればよいでしょう。

seek_depth には置換処理を行う最大回数を指定します。たとえば 1 を指定すると最初に現れるパターンのみが置換処理され、 以降に同じパターンが現れてもすべて無視されます。seek_depthにRrk_NPOSを指定した場合、最大回数は無限大となり、 すべてのパターンにおいて置換処理が行われます。

delta_to_next には置き換え処理を行った直後、カーソルをどれだけ右へ移動させるかを指定します。 delta_to_next には通常 Rrk_NPOS を指定するとよいです。この場合、new_ptnの文字列長分だけカーソルが移動します。

例えば以下のようにして使用します。

import std/str

varia str string = "abcOLDdefOLDghi"
RrkStr_replacePtn( str, 0,
	"OLD", "NEW",
	Rrk_NPOS, Rrk_NPOS )
Rrk_print( "str=" str "\n" ) /* str = "abcNEWdefNEWghi" */

上の例では "abcOLDdefOLDghi" の"OLD"の部分がすべて"NEW"に置き換わりますから、 結果的にstrの値は"abcNEWdefNEWghi"となります。

標準関数 RrkStr_slice


RrkStr_slice は、与えられた文字列str を開始位置beginからleng文字分を残してそれ以外をカットする関数です。 rrk_pkg/std/str.rrkh ファイル内での関数宣言は以下のようになっています。

natvfunc RrkStr_slice(  str string, begin uint, leng uint ) void

例えば以下のようにして使用します。

import std/str

varia str string = "abcDEFghiJKL"
RrkStr_slice( str, 3, 6 )
Rrk_print( "str=" str "\n" ) /* str = "DEFghi" */

上の例では "abcDEFghiJKL" の3番目から開始して6文字分だけが残りますから、 結果的にstrの値は"DEFghi"となります。

標準関数 RrkChar_toStr


RrkChar_toStr は、与えられた文字(実体は整数)を文字列に変換します。 この関数はrrk_pkg/std/str.rrkh ではなく、rrk_pkg/std/char.rrkh ファイル内で関数宣言されており、 以下のようになっています。

natvfunc RrkChar_toStr( ch int ) conststr

戻り値の型が文字列conststrであり、Rrk_printなどに指定して使うこともできます。 例えば以下のようにして使用します。

import std/char

varia ch = 'a'
Rrk_print( "ch=" RrkChar_toStr(ch) "\n" ) /* ch=a */

上の例では ch に文字 'a' (実体は整数)を代入しています。 これをそのままRrk_printに渡すと'a'の文字コードを意味する「97」が表示されてしまいますが、 RrkChar_toStrを介することで a と表示させることができます。

この関数でnull文字('\0')をchとして指定した場合、"\0"という文字列に変換されます。 またその他の非印字可能文字を指定した場合、「\x16進数」という形の文字列に変換されます。

文字列に関する標準関数は他にも多く存在しますが、このチュートリアルでの紹介ではこのくらいに留めておきます。

関数の引数におけるstring型とconststr型の違い


ある関数における文字列型の引数がstring型である場合とconststr型である場合の違いについて、詳しく説明します。
  • 関数の引数がstring型である場合

  • これは例えば以下のような状況です。

    function func( s string ) void {
    	/* something imprementation */
    }
    varia str string = "hello"
    func( str )
    

    このstring型引数にはstring型変数/定数を指定することしかできません。 このときstring型変数の値(実体を指し示すアドレス)が渡されます。 そしてその変数/定数の指し示す実体は関数によって書き換えられる可能性があります (そのような書き換えがそもそもの目的である関数では、string型の引数となっています)。

    このstring型引数にconststr型変数/定数や文字列リテラルを直接指定することは許可されません。 (このような指定はconststr型変数(の指し示す値)を変更し得ることに繋がるためです)。 例えば以下はコンパイルエラーとなります

    function func( s string ) void {
    	/* something imprementation */
    }
    varia str conststr = "hello"
    func( str )     /* NG */
    func( "hello" ) /* NG */
    

    conststr型や文字列リテラルの場合でも以下のように「->string」と記述(これをキャストと呼びますが)することで、 ディープコピーを行うよう強引に指定することはできますが、 このとき指定したconststr型変数/定数とはまた別の(しかし値は同じであるstring型の)実体が作られ、 関数の引数はそれを参照する形になります(つまり元のconststr型変数には何の影響も及ぼしません)。

    function func( s string ) void {
    	/* something imprementation */
    }
    varia str conststr = "hello"
    func( str->string )
    func( "hello"->string )
    

  • 関数の引数がconststr型である場合

  • これは例えば以下のような状況です。

    function func( s conststr ) void {
    	/* something imprementation */
    }
    varia str string = "hello"
    func( str )
    

    このconststr型引数にはstring型変数/定数とconststr型変数/定数の指定が可能です。 また文字列リテラルを直接指定することも可能です。 このときconststr型変数/定数の値(実体を指し示すアドレス)が渡されます。 そしてその変数/定数が指し示す値は関数によって書き換えられることはありません

上記の仮引数「s」の前に「const」「immut」「tight」「varia」などが付加されている場合もあります。 これらはモディファイア(modifier)と呼ばれますが、これが付加された場合の意味については「ユーザ定義関数」のセクションで詳しく述べます。

文字列のための標準関数のまとめ


文字列のための標準関数を使うには最初に import std/str を実行しましょう。 その上で、例えば文字列変数 s の文字列長を取得したい場合、RrkStr_leng( s ) を式中に書きます。 文字列のための標準関数には他にもRrkStr_at、RrkStr_findIdxL、RrkStr_assign、RrkStr_replace、RrkStr_replacePtn、RrkStr_sliceなどがあります。 関数によっては文字列を上書きするようなタイプのものもあり、その場合conststr型ではなくstring型が必要になります。



目次に戻る

ユーザ定義関数

基本事項


Rarakuではユーザが自分で関数を作ることができます。 関数を作ることを関数を定義するとも呼びます。 また関数を定義した記述部分そのもの(専用のブロック「{ ... }」で囲まれたものですが)を関数定義関数の実装と呼びます。

関数を定義するには、まず「function 関数名( 引数の羅列 ) 戻り値の型」という書式で記述を開始します。 例えば以下では my_addition という名前の関数を定義しています。

varia st_ival int = 1
function my_addition( ival int ) int {
	return st_ival + ival
}
varia st_ans = my_addition( 1 ) /* st_ans = 2 */

my_additionでは1つのint型の引数を受け取り、またその戻り値はint型となります。 続けて { ... } で囲った内部に実際の関数定義を記述します。 { ... } 内では任意の文を任意の数だけ記述したり、別の関数を呼び出したりすることができます。

いかなる関数定義の内側でもない(言い換えればブロック階層の完全な外側の)部分をグローバルスコープと呼びますが、 Rarakuではまずグローバルスコープにある文が上の行から順に実行されます。 このとき、もしも途中で関数定義部が来た場合は、それをスキップしつつ実行します。

上記の例では、グローバルスコープにある文は1行目の「varia st_ival int = 1」と最後の行の「varia st_ans = my_addition( 1 )」ですから、 まずこれら2つの文が(関数定義をスキップしつつ)この順で実行されます。

最後の行でmy_addition関数が呼ばれたときに初めて関数定義部へと制御が移り、その内部が実行されます。 今回の例では、{ ... }内で足し算(st_ival + ival)をして一つの整数値を生成し、さらに戻り値としてその結果を返しています。 戻り値を返すには「return 戻り値」といったように記述します(これをreturn文と呼びます)。

グローバルスコープで変数が宣言されており、しかもそれが関数定義よりも前に位置する場合、 その変数を関数定義内部からアクセスすることができます。

例えば上記の例では st_ivalという変数がグローバルスコープで宣言されていますが、 これはmy_additionの関数定義よりも前にあります。 よってst_ivalを関数定義内からでも使用することができます。

ただしこの場合、st_ivalが既に初期化されている必要があります。 つまりグローバルスコープにある1行目の「varia st_ival int = 1」が既に実行された後で、 このmy_addition関数が呼び出されなければなりません。 それが実行されるより前の段階でこの関数を呼び出した場合、コンパイルエラーとなります (Rarakuのコンパイラは初期化チェックに関しては割と強力でこれを静的に検出します)。

おおよその指針ですが、このような場合まずアクセス対象となる外部の変数(上の例ではst_ival)を先に書き、 次に関数の定義を書き、(グローバルスコープからの)関数の呼び出しはなるべくそれらより下に記述することをお勧めします。 このようにするとこの種の問題はずっと発生しにくくなります。

一方、st_ansもグローバルスコープで宣言されてはいますが、これはmy_additionの関数定義よりも後ろにあります。 よってst_ansを関数定義内から使用することはできません。

戻り値のある関数(戻り値の型がvoidではない関数)では、{ ... } 内の最後の文は必ずreturn文でなければなりません。 また最後の文以外でもreturn文を記述することができ、その場合そのreturn文が実行された時点で関数が終了します。 例えば以下の例ではivalが3より大きい場合、「return 100」が実行され、その時点でmy_func関数は終了し、呼び出し元に戻ります。

function my_func( ival int ) int {
	if ival > 3 {
		return 100
	}
	return 200
}
my_func( 3 )

戻り値のない関数(戻り値の型がvoidの関数)では、最後のreturn文を省略できます。 戻り値のない関数にreturn文を記述する場合は単に「return」といったように戻り値を指定せずに記述します。 例えば以下のようになります。

function my_func( ival int ) void {
	if ival > 3 {
		return
	}
	return /* このreturn文は省略することもできる */
}
my_func( 3 )

最近のプログラミング言語では戻り値を返す場合でもキーワード「return」を記述しなくてもよいものが増えています。 そのような言語では、最後に評価された式の値を自動的に戻り値とみなす場合が多いようです。 あるいはブロック内の最後に単独の式を書くとそこでその式値を戻り値として返すといった具合です。

一方、returnに相当する処理は制御構造をそこで打ち切るものですから、それなりに目立つキーワードである方が安全という考え方もあります。 また例えば単独の関数の呼び出しで完結するような文があってその後にreturnすべき値を書き忘れた場合、 予期せぬreturnが何のエラーもなく行われてしまうかもしれません。 Rarakuでもこの考え方に準じて基本的には戻り値を返す場合はキーワード return を記述する必要があります。

ただ例えば単一の数学の式を返すような単純な関数の場合など、return のない記法の方が便利という考え方もわからなくはありません。 そこでRarakuではその一つの妥協点として「return」の替わりに「..」と書くことも可能になっています (returnを示す目印が完全に消えるわけではありませんがかなり簡素になります)。

これを使うと例えば次のようになります。

function my_func( ival int ) int {
	if ival > 3 {
		.. 100 /* return 100 と全く同じ */
	}
	..200 /* return 200 と全く同じ(..と200の間のスペースはなくてもよい) */
}
my_func( 3 )

戻り値がない場合の例ですと以下のようになります(途中でreturnするような場合です)。

function my_func( ival int ) void {
	if ival > 3 {
		.. /* return と全く同じ */
	}
	/* まだ処理が続く */
}
my_func( 3 )

ただ、ある程度中味が複雑な関数では、やはり「return」というキーワードを明示的に記述する方が、 後で読むときそこで何が起きているのか一瞬で判断しやすいと思います。

関数定義内において、以下のように自分自身を呼び出すこともできます。

function my_func( ival int ) int {
	if ival > 3 {
		return ival
	}
	return my_func( ival+1 ) /* 自分自身(my_func)を呼び出す */
}
my_func( 1 ) /* ival = 3 */

このような呼び出しを再帰呼び出しあるいはrecursive-callと呼びます。

ただしこのような呼び出しを考え無しに行うと無限に呼び出しが発生し、無限ループとなってしまいます。 ですからある条件でその呼び出しが終了するような分岐処理を関数内に記述しておく必要があります。 上記の例ではivalが3より大きくなった時点でそれ以上の呼び出しを終了するという分岐処理が含まれています。

極めて特異な例ですが「戻り値のないreturn文の直後に無名ブロックが来る」ようなケースも一応想定はできます。 その場合、returnの直後に「;」が必要となります。

function my_func( ival int ) void {
	return ; /* この「;」が必要 */
	{
		/* このブロックは通常どのようにしても実行できないが、一応記述できる */
		varia i = 0
	}
}
my_func( 3 )

この「;」がない場合、Rarakuのパーザは、後述する構造体の初期化子をreturn文で返す構文とみなしますので、 戻り値の型ないしはブロック内部の記述の矛盾により、コンパイルエラーとなります。

もっとも普通はこんな記述はしないとは思いますが、強いて言えば最初に以下のように無名ブロックを使ったコードがあり プログラミング中に(デバッグやテスト等の目的で)実験的にその直前にreturnを記述したような場合に起こり得るのかもしれません。

function my_func( ival int ) void {
	/* something */

	/* 例えばこの位置に後から実験的にreturnを入れるような場合 */
	{
		/* 無名ブロック */
	}
}
my_func( 3 )


引数がない関数


引数がない関数を定義する場合、引数の指定に何も指定せず単に「( )」と記述します。 あるいは void を使って「( void )」と記述しても構いません(これは完全に好みの問題です)。 例えば以下の通りです。

function my_func1() int /* 引数がない関数 */
{
	return 1
}
function my_func2( void ) int /* 引数がない関数 */
{
	return 2
}
function my_func3() void /* 引数がない関数 */
{
	return
}

my_func1()
my_func2()
my_func3()

上の例のmy_func1、my_func2、my_func3はすべて引数がない関数です。 これを呼び出すときも引数を指定せず単に「my_func1( )」などと記述します。

my_func3では引数も戻り値も両方ない関数の定義となっています。 戻り値がない場合も同様に void を省略することができます。 例えば以下のようになります。

function my_func3() /* 戻り値のvoidも省略. */
{
	return
}
my_func3()

ただしglobalやnatvfuncといったキーワードで始まる関数の書式の場合、(戻り値がない場合において)voidを省略することはできません。 globalキーワードについては「ヘッダファイルとリンケージ」のセクションで詳しく説明します。

関数の仮引数におけるモディファイア


二つの変数の値を入れ替える(このような処理は一般にswapと呼ばれますが)関数の定義を考えます。 まずは整数型変数の引数をとる以下のような関数を作ったとしましょう(ただしこれはコンパイルできません)。

function swap_test1( xx int, yy int )
{
	const tmp = xx
	xx = yy
	yy = tmp
}
varia x = 10
varia y = 20
Rrk_print( "before : x=" x ", y=" y "\n" )
swap_test1( x, y )
Rrk_print( "after  : x=" x ", y=" y "\n" )

上記を実行するとコンパイルエラーとなり、以下のようなエラーメッセージが表示されます。

You are trying to assign to the const type.
const : [xx]

Rarakuのデフォルトでは、仮引数はconst文で宣言されたのと同様なものとして扱われます。 そのため、このような仮引数に(「=」による)値の代入はできません。

これを代入可能なものとするには、仮引数の識別子(上の例では「xx」)の前にモディファイア(modifier)を指定する必要があります。 このようなモディファイアとして、「const」「immut」「tight」「varia」の4つがあります。

結論から言えば、これらのうちのどれを指定しても(これら単独では)今回の例では意図した結果にはなりませんが、 次の内容に進むための準備としてまずは代入の可否に関するモディファイアの意味を抑えておく必要はあります。

それぞれのモディファイアの概要について以下に簡単に述べます。

  • constモディファイア

  • 仮引数にこのモディファイアを付けると「=」による代入を行うことが禁止されます。 上記の例では仮引数「xx int」の部分を「const xx int」に変える形になります。 実は単に「xx int」と記述した場合、これは「const xx int」の「const」が省略されたものとして扱われます。 そのため、仮引数にモディファイアを何も指定しない場合、「=」による代入が禁止されるわけです。

    因みに(仮引数ではなく)戻り値の型にモディファイアを付けない場合、それは(「const」ではなく)「varia」が省略されたものとして扱われます。 ここはややこしいですが、仮引数と戻り値ではデフォルトのモディファイアが異なることに注意してください。

  • immutモディファイア

  • int型の仮引数にこのモディファイアを付けると「=」による代入を行うことが許可されます。 上記の例では仮引数「xx int」の部分を「immut xx int」に変える形になります。 このモディファイアはやや発展的であるため、このセクションではこれ以上は取上げません。 「配列とモディファイア」のセクションで詳しく述べることになると思います。

  • tightモディファイア

  • int型の仮引数にこのモディファイアを付けると「=」による代入を行うことが許可されます。 上記の例では仮引数「xx int」の部分を「tight xx int」に変える形になります。 このモディファイアはやや発展的であるため、このセクションではこれ以上は取上げません。 「配列とモディファイア」のセクションで詳しく述べることになると思います。

  • variaモディファイア

  • int型の仮引数にこのモディファイアを付けると「=」による代入を行うことが許可されます。 上記の例では仮引数「xx int」の部分を「varia xx int」に変える形になります。 これについては次項以降で詳しく述べます。

よって、上記の例では immut、tight、variaのいずれかを指定すれば、とりあえず「xx = yy」や「yy = tmp」といった代入を行うことは可能になります。 しかし、実はこれだけでは意図した結果にはなりません。 確かにこの代入によって関数呼び出しの内部ではxxとyyの更新が行われるのですが、 その結果は関数呼び出しの外までは反映されないからです。 次項からはそのことについてもっと詳しく説明します。

refer指定された整数引数(call by reference)


二つの変数の値を入れ替えるswap関数の定義をもう一度考えます。 整数型変数の引数をとる以下のような関数を作ったとします。 ただし今回は仮引数のモディファイアとしてvariaを指定してあります。

function swap_test1( varia xx int, varia yy int )
{
	const tmp = xx
	xx = yy
	yy = tmp
}
varia x = 10
varia y = 20
Rrk_print( "before : x=" x ", y=" y "\n" )
swap_test1( x, y )
Rrk_print( "after  : x=" x ", y=" y "\n" )

上記の実行結果は以下のようになります。

before : x=10, y=20
after  : x=10, y=20

今回はコンパイルエラーとまではなりません。 しかし実行結果をよく見ると、残念ながら二つの変数x, yの値は入れ替わってはいません。

関数の仮引数 xx は、実引数 x とは別に確保された変数です。 つまりこれら二つは実体が異なります。 関数swap_test1が呼び出されたとき、x から xx へ値がコピーされます(y と yy についても同様です)。 よって仮引数 xx は実引数 x と値としては同じになっています。 ただしここで重要なことは、xx と x は実体が異なるということです (このような指定をcall by valueと呼びます)。

関数swap_test1内では、xx と yy の指し示す先は確かに入れ替わっています。 ところがこれは、(呼び出し元の実引数である)変数 x と y とは異なる実体であり、 そのため x と y には何の効果も及ぼしません。

このチュートリアルでのこれまでの例(このswap処理の例より前の例)は、 すべてこのような呼び出し元に効果を与える必要のないものでした。 ですからたまたま問題にならなかったわけです。

では、呼び出し元の実引数 x と y にも結果を反映させるにはどうしたらよいでしょうか? そのためには関数定義の引数において(結果を反映させたい各変数の前に) refer を指定します。 例えば次のようになります。

function swap_test2( refer xx int, refer yy int )
{
	const tmp = xx
	xx = yy
	yy = tmp
}
varia x = 10
varia y = 20
Rrk_print( "before : x=" x ", y=" y "\n" )
swap_test2( x, y )
Rrk_print( "after  : x=" x ", y=" y "\n" )

上記の実行結果は以下のようになります。

before : x=10, y=20
after  : x=20, y=10

今回は値が入れ替わっており、これは望み通りの結果です。

referを指定することで、仮引数と実引数の実体を同一のものとすることができます (このような指定をcall by referenceと呼びます)。 今回の場合、swap_test2 での仮引数 xx の実体と、呼び出し元での実引数 x の実体は同一のものということです。 y と yy についても同様です。 これによりswap_test2 内での xx = yy という代入処理は、元の変数で x = y という代入処理を行っているのと全く同じになります。

それならば最初から全部referにすればよいのでは?と思われるかもしれませんが、 実際のプログラミングではreferを指定するのは必要最小限に留めるべきです。 というのも何でもかんでもreferにしてしまうと、今度は関数呼び出しの度に すべての引数について、その値が変わってしまう可能性を考慮しなければならなくなります。 これにより変数の値の追跡が不必要に困難になってしまいますし、 またデバッグ(プログラミングのミスを修正する作業)も難しくなってしまいます。

string型引数の値の変更を呼び出し元に反映させる


string型で指定した引数の値を関数内で変更し、その結果を呼び出し元にも反映させたいとします。 これには以下のように二つの方法があります(どちらでもいいとは思うのですが、前者の方がrefer指定がない分、引数の指定はすっきりします)。

  • (refer指定せず)RrkStr_set関数等を使う方法

  • refer指定されていないstringの場合、xが指し示す先を変えることはできませんが、 (指し示した実体は同じままにした上で)その実体の内容を関数内で書き換えることならば可能です。 例えば以下をご覧下さい。

    import std/str
    function setToStr( str string )
    {
    	RrkStr_set( str, "XX" ) /* str が指し示す領域の値を"XX"に書き換える */
    }
    
    varia x string = "aa"
    Rrk_print( "before : x=" x\n )
    setToStr( x )
    Rrk_print( "after  : x=" x\n )
    

    上記の実行結果は以下のようになります。

    before : x=aa
    after  : x=XX
    

    この場合、xが指し示す領域は同一のものですが、その領域の値が "XX" に書き換えられることになります。

    尚、上記では「varia str string」ではなく「str string」としていますが、これは間違いではありません。 というよりこの場合むしろ、「str string」にすべきで、 もっと言えば「varia str string」にすべきではありません。

    Rarakuではモディファイアを指定しない場合それはconstが指定されたのと同じ意味になりますから、 「str string」は「const str string」と同じ意味になります。 そしてまた、Rarakuでは const string 型の内容を RrkStr_set 関数等で書き換えたり、「&=」による文字列連結も許可されます。 const string型で禁止されるのは以下のように「str = "XX"」といった直接的な代入文のみです。

    function setToStr( str string )
    {
    	str = "XX" /* NG: strはconstなので「=」による代入はコンパイルエラー */
    }
    

    既に見たようにreferを使わない場合、「str = "XX"」といった代入文は(関数の呼び出し元から見れば)効果のない文ですが、 (値を書き換えるつもりで)うっかりこのように書いてしまうミスは十分に想定できます。 「varia str string」ではなく「str string」(「const str string」)とすることによって、 この種のミスを(コンパイルエラーとして)検出することができます。

    参考: 「varia str string」と指定した場合
    参考のため、仮に「varia str string」と指定した場合も見ておきましょう。 例えば以下の通りです。

    function setToStr( varia str string )
    {
    	str = "XX" /* str が指し示す先を"XX"の実体へ変更する */
    }
    
    varia x string = "aa"
    Rrk_print( "before : " \=x\n )
    setToStr( x )
    Rrk_print( "after  : " \=x\n )
    

    上記の実行結果は以下のようになります。

    before : x=aa
    after  : x=aa
    

    これはうまくいきません。 (呼び出し元である)実引数の x は "aa" の実体を指し示したままとなります。

    Close


  • refer指定する方法

  • 以下の例では、refer指定をした上でstrに文字列 "XX" を代入しています。 これはstrが指し示す先を "XX" の実体へ変更することを意味します。

    function setToStr( refer str string )
    {
    	str = "XX" /* str が指し示す先を"XX"の実体へ変更する */
    }
    
    varia x string = "aa"
    Rrk_print( "before : " \=x\n )
    setToStr( x )
    Rrk_print( "after  : " \=x\n )
    

    上記の実行結果は以下のようになります。

    before : x=aa
    after  : x=XX
    

    このように(呼び出し元である)実引数 x もまた "XX" の実体を指し示すようになります。

    参考: 「refer str conststr」と指定した場合
    referを指定する方法の場合、string型ではなくconststr型でも同様のことはできます。 例えば以下の通りです。

    function setToStr( refer str conststr )
    {
    	str = "XX" /* str が指し示す先を"XX"の実体へ変更する */
    }
    
    varia x conststr = "aa"
    Rrk_print( "before : " \=x\n )
    setToStr( x )
    Rrk_print( "after  : " \=x\n )
    

    上記の実行結果は以下のようになります。

    before : x=aa
    after  : x=XX
    

    conststrでもうまくいく理由は、ここで行っていることは値の書き換えではなく、 あくまで指し示す実体を切り替えているに過ぎないからです。 ただしconststr型の場合string型と違い、関数内で「&=」を使った文字列連結などはできません。

    Close

refer指定されたconststr文字列引数(call by reference)


練習問題として文字列をswapする関数の定義を今一度考えます。 今回はintの替わりにconststrを用います。 例えば次のようになります。

function swap_cstr( refer xx conststr, refer yy conststr )
{
	const tmp = xx
	xx = yy
	yy = tmp
}
varia x = "aa"
varia y = "bb"
Rrk_print( "before : x=" x ", y=" y "\n" )
swap_cstr( x, y )
Rrk_print( "after  : x=" x ", y=" y "\n" )

上記の実行結果は以下のようになります。

before : x=aa, y=bb
after  : x=bb, y=aa

望み通りの結果です。 ただしconststr/string型の場合、(int型とは異なり)"aa" と "bb" の値の実体が直接移動して入れ替わったのではなく、 指し示す先が入れ替わる形になります。

最初 x は "aa"、y は "bb" を指し示しています。 この "aa" と "bb" の実体はコンスタントプールと呼ばれる領域に別途確保され存在しています。 コンスタントプールは x と y が存在する領域とはまた別です。 x と y には、そのコンスタントプールに存在する "aa" と "bb" のアドレスが格納されているということです。 swap_cstrではそのアドレス値を入れ替えています。 これにより、x が "bb" を、y が "aa" をそれぞれ指し示すように変更されたことになります。 このようなアドレス値の移動は、データ量の大きい値の実体を直接移動して入れ替えるよりも一般には高速になります。

参考: C言語との比較
この「参考」はC言語をご存知の方のみお読みください。

これはC言語で言えば文字列ポインタの入れ替えそのものです。 同様の処理をC言語で書けば以下のようになります。

#include <stdio.h>

void
swap_cstr( const char** xx, const char** yy )
{
	const char* tmp = *xx;
	*xx = *yy;
	*yy = tmp;
}
int 
main( void )
{
	const char* x = "aa";
	const char* y = "bb";
	
	printf( "before : x=%s, y=%s\n", x, y );
	swap_cstr( &x, &y );
	printf( "after  : x=%s, y=%s\n", x, y );

	return 0;
}

Close


特に初心者にはわかり辛い箇所なのでもう少し別の例を見ておきます。

以下の例は別途変数 z を用意し、予め x を z へ代入しています。 その後 swap_cstr を実行し、z がどうなるかを観察します。

function swap_cstr( refer xx conststr, refer yy conststr )
{
	const tmp = xx
	xx = yy
	yy = tmp
}
varia x = "aa"
varia y = "bb"
varia z = x /* x を代入しておく */
Rrk_print( "before : " \=x \, \=y \, \=z \n )
swap_cstr( x, y )
Rrk_print( "after  : " \=x \, \=y \, \=z \n )

上記の実行結果は以下のようになります。

before : x=aa, y=bb, z=aa
after  : x=bb, y=aa, z=aa

zの値、すなわち z が指し示した先の値は変わっていません。 これはすなわちコンスタントプール上にある実体 "aa" の値は何も変更されていないことを意味します。 そもそも conststr とは「指し示した先の値を変更しない」ことを保証するための型ですが、それとも矛盾しないことが確認できます。

関数の仮引数が文字列の場合のまとめ


関数の仮引数が文字列の場合、conststrとstring、それにreferの有り無しで全部で4パターンの指定があり得ますが、ここまでの結果をまとめましょう (尚、関数の仮引数としてvariaの付いたconststrやstringの指定は通常すべきではありませんので、ここではそれは取上げません)。

  • conststr型引数

  • 単に変数が指し示す実体のアドレス値のコピーが渡されます(アドレス値に関するcall by value)。 関数の内部において、「この引数により指定した変数」が「元とは別の実体を指し示す」ように変更される可能性はありません。 というのもこの指定は「const conststr」と指定したのと同じ(constが省略された形)であり、 よって仮に関数内部において「=」による代入処理を行った場合、コンパイルエラーとなるためです。

    関数の内部において、「この引数により指定した変数が指し示している実体の値」が「直接書き換えられるような処理」が行われる可能性はありません (仮にそのような処理を行っていた場合、コンパイルエラーとなります)。

    最も制限が厳しい型ですので、関数宣言においては可能な限りこの型とすべきです。

  • string型引数

  • 単に変数が指し示す実体のアドレス値のコピーが渡されます(アドレス値に関するcall by value)。 関数の内部において、「この引数により指定した変数」が「元とは別の実体を指し示す」ように変更される可能性はありません。 というのもこの指定は「const string」と指定したのと同じ(constが省略された形)であり、 よって仮に関数内部において「=」による代入処理を行った場合、コンパイルエラーとなるためです。

    関数の内部において、「この引数により指定した変数が指し示している実体の値」が「直接書き換えられるような処理」が行われる可能性があります。 (仮にそのような処理を行っていた場合でも、コンパイルエラーとはなりません)。

    stringの値を直接加工するような関数では、(加工対象となる文字列に対して)これを指定するとよいでしょう。 例えばエラーログ文字列などで文字列をどんどん連結していくような処理をしたい場合、この指定が最適と思われます。

  • refer conststr型引数

  • 引数として指定した変数そのものが渡されます(call by reference)。 関数の内部において、「この引数により指定した変数」が「元とは別の実体を指し示す」ように変更される可能性があります。 これは例えば、関数内部において「=」による代入処理を行った場合、その指し示す先が右辺値に変更されることを意味します。

    関数の内部において、「この引数により指定した変数が指し示している実体の値」が「直接書き換えられるような処理」が行われる可能性はありません (仮にそのような処理を行っていた場合、コンパイルエラーとなります)。

    conststr型の戻り値が複数必要なケースではこれを使うとよいでしょう。 ただしこの指定では文字列連結を行うことはできません。

  • refer string型引数

  • 引数として指定した変数そのものが渡されます(call by reference)。 関数の内部において、「この引数により指定した変数」が「元とは別の実体を指し示す」ように変更される可能性があります。 これは例えば、関数内部において「=」による代入処理を行った場合、その指し示す先が右辺値に変更されることを意味します。

    関数の内部において、「この引数により指定した変数が指し示している実体の値」が「直接書き換えられるような処理」が行われる可能性があります。 (仮にそのような処理を行っていた場合でも、コンパイルエラーとはなりません)。

    ここで取上げた4つの中では最も制限が緩い型です。 殆どの場合、単なる「string型引数」でも同じことができますので、この指定は別に必要ないといえばそうです。 ただ、関数定義においては「=」による代入でも呼び出し元に効果が及ぶような値の変更ができますので、 内部でRrkStr_set関数等を使わずに済むといった違いはあります。 その他、指定された引数が元々nullを指し示しており、関数内部で新たに非nullなstringを指し示す というような特殊なケースに対応しなければならない場合、この指定(正確には「refer string^?」)を使うことになると思います。

Rarakuでは関数の戻り値の型にもreferを指定することができますが、それについては「tight文とimmut文とrefer文」のセクションで詳しく説明します。

staticローカル変数


通常、const/varia文で宣言された関数内のローカル変数は、その関数が終了した時点で寿命を終えます。 しかし、このconst/varia文の前に「static」キーワードを記述して宣言した変数はグローバルな寿命を持ち、 さらにその実体は(関数の呼び出し回数に関わらず)一つとなります。 このような変数をstaticローカル変数と呼びます。 staticローカル変数は、その関数内だけからアクセスが可能です(この点は通常のローカル変数と同じです)。

これはC言語における、staticローカル変数とほぼ同じです。 ただしRarakuでは初期化時の右辺として定数以外のもの(一般的な式や変数、関数など)を指定することもできます。 その意味ではC++におけるmeyers-singletonと呼ばれる手法の挙動にも似ています。

例えば以下をご覧ください。

function func()
{
	static varia st_var = 10
	Rrk_print( \=st_var\n )
	++st_var;
}

func() /* 1回目の実行 */
func() /* 2回目の実行 */
func() /* 3回目の実行 */

上の例では関数内でstatic varia文により st_var というstaticローカル変数が宣言されており、 これはこの関数を終了しても破棄されずに残ります。 またstatic const/static varia文での初期化(上の例では「=10」の部分)は、初めてその文に到達したタイミングで一度だけ行われます。 2度目以降に同文に到達した場合、その文はスキップされます(このとき右辺値も一切評価されません)。

上の例の場合、まずfuncの1回目の実行において、st_var が 10 で初期化されます。 次に st_var の値をインクリメントして関数を終了していますので、st_varには値11が格納されたまま残ります。

続いてfuncの2回目の実行においては、初期化(つまり「static varia st_var = 10」の実行)が丸ごとスキップされます。 st_varの値は前回の値のまま、つまりRrk_printにおいては「11」と表示され、インクリメントして関数を終了していますので st_varには値12が格納されたまま残ります。

最後のfuncの3回目の実行においても同様に、「static varia st_var = 10」の実行が丸ごとスキップされ、 st_varの値は前回の値のまま、つまりRrk_printにおいては「12」と表示され、インクリメントして関数を終了していますので st_varには値13が格納されたまま残ります。

上の例の実行結果は以下のようになります。

st_var=10
st_var=11
st_var=12

通常のローカル変数とは異なり、staticローカル変数の実体は一つです。 これによりstaticローカル変数を更新した場合は、グローバルスコープで宣言された変数と同様なふるまいを見せます。 例えば以下のように再帰呼び出しを行ってそれを変更した場合でも、常に一つの実体への変更となります。

function func( i int )
{
	static varia st_var = 10
	Rrk_print( \=st_var \, \=i\n )
	if i == 0 {
		return
	}
	--i;
	++st_var;
	func( i )
}
func(3)

上の例の実行結果は以下のようになります。

st_var=10, i=3
st_var=11, i=2
st_var=12, i=1
st_var=13, i=0

関数のデフォルト引数


関数の仮引数(parameter)xにおいて、「x 型 = 式」のように記述した場合、その引数(argument)のデフォルト値を指定することができ、これをデフォルト引数(default arguments)と呼びます。 例えば以下の通りです。

function func( x int = 10 )
{
	Rrk_print( \=x\n )
}
func()
func( 20 )

上記ではfunc関数は仮引数が1つで、呼び出し側も本来は引数の指定が1つ必要ですが、 x はデフォルト引数となっているため、関数呼び出し側でその引数を指定しなかった場合は自動的に10が指定されたものとみなされます。 上の例の実行結果は以下のようになります。

x=10
x=20

通常の仮引数とデフォルト引数を混ぜた引数宣言も可能です。 例えば以下の通りです。

function func( x int, y int = 20 )
{
	Rrk_print( \=x \, \=y \n )
}
func( 10 )
func( 30, 40 )

上記では第1引数は通常の引数、第2引数はデフォルト値が付属したデフォルト引数となっています。 x は通常の引数ですので、この部分は必ず指定する必要があります。 一方、y はデフォルト引き数ですので、この部分は指定を省略することもでき、その場合は自動的に20が指定されたものとみなされます。 上の例の実行結果は以下のようになります。

x=10, y=20
x=30, y=40

上記はデフォルト引数が通常の引数より後に来る例でしたが、これが通常の引数より前に来るような指定も可能です。 しかし第2引数の指定が必須である以上、普通に引数指定をすると第1引数を省略できません。 このような場合、以下のように第1引数にdefaultを指定することで第1引数を省略したとみなすことができます。

function func( x int=10, y int )
{
	Rrk_print( \=x \, \=y \n )
}
func( default, 20 )

この例の実行結果は以下のようになります。

x=10, y=20

尚、すぐ後に述べる「名前付き引数」を使うことでも、上記の第1引数の指定を省略することができます。

名前付き引数による関数呼び出し


関数呼び出し時、「引数名 : 値」のように記述することで、どの引数に何の値を設定するかを明示的に指定することができます。 これを名前付き引数(named arguments)と呼びます。 これに対し、これまで行ってきた通常の引数指定では、その引数の順番と位置を正確に指定しなければなりませんが、 これを位置引数(positional arguments)と呼びます。

名前付き引数指定の場合、(位置引数指定とは異なり)それを好きな順序や位置で指定することができます。 例えば以下の通りです。

function func( x int, y int, z int )
{
	Rrk_print( \=x \, \=y \, \=z \n )
}
func( 10, 20, 30 )       /* A */
func( x:10, y:20, z:30 ) /* B */
func( y:20, z:30, x:10 ) /* C */

まず上記のAの指定「func( 10, 20, 30 )」は、これまで見て来た位置引数による指定です。

その次のBの指定「func( x:10, y:20, z:30 )」では、 第1引数を「x:10」(xに10に指定する名前付き引数)、 第2引数を「y:20」(yに20に指定する名前付き引数)、 第3引数を「z:30」(zに30に指定する名前付き引数) を指定しており、結局Aの指定と結果は同じになります。

つまりBの指定では完全にx, y, zと本来の順番通りに与えているわけですが、 このような場合であっても名前付き引数の使用が無駄というわけではありません。 名前付き引数の場合、どの引数に何を指定しているかがより明確になる(つまりドキュメント性が(コメントを使うことなく)向上している)点が メリットとして挙げられます。

最後に上記のCの指定「func( y:20, z:30, x:10 )」では、 Bの指定とは順番が入れ替わってはいますが、どの引数に何の値を設定するかの対応はBと全く同じなので、 結果もBと全く同じになります。

このような性質から、将来なんらかの理由でfunc関数の仮引数x, y, zの順番が入れ替わったとしても、 名前付き引数の場合(少なくともこの3つの引数に関しては)呼び出し側を修正する必要がないことになります。

結局、指定A,B,Cはどれも結果は同じであり、この例の実行結果は以下のようになります。

x=10, y=20, z=30
x=10, y=20, z=30
x=10, y=20, z=30

名前付き引数の指定において、存在しない仮引数名を指定した場合はコンパイルエラーとなります。 例えば以下の通りです。

function func( x int, y int, z int )
{
	/* something */
}
func( x:10, w:20, z:30 )

上記の2番目において、「w:20」と指定していますが、w という名前の仮引数はもちろん関数funcには存在しないため、 これは許可されずコンパイルエラーとなります。

関数の仮引数にデフォルト引数がある場合でも名前付き引数を指定することができます。 例えば以下の通りです。

function func( x int=10, y int, z int=30 )
{
	Rrk_print( \=x \, \=y \, \=z \n )
}
func( y:20 )

上記では、xとzはデフォルト引数が付属していますので、指定を省略することもできます。 一方でyはデフォルト引数が付属していませんので、位置引数(通常の引数)または名前付き引数での指定が必須となります。 この例の実行結果は以下のようになります。

x=10, y=20, z=30

名前付き引数の指定において、特に仮引数名(「:」の左側)と指定する変数名(「:」の右側)が全く同じ場合、つまり「x:x」のような指定となる場合、 簡易記法として値の方(右のx)を省略して単に「x:」と書くこともできます(xを2回冗長に書かなくても済むということです)。 例えば以下の通りです。

function func( x int )
{
	Rrk_print( \=x \n )
}
varia x = 10
func( x:x )
func( x: )

上記で「x:」と指定していますが、これは「x:x」と指定したのと全く同じです。 この例の実行結果は以下のようになります。

x=10
x=10

尚、念のために補足しますと、値として「x+1」のように何か付加された式を指定する場合は、 この簡易記法は使えず「x:x+1」のように完全な形で指定しなければなりません。

位置引数と名前付き引数の混合


位置引数(通常の引数)と名前付き引数は混ぜて指定することもできますが、全く自由に指定できるわけではありません。 面倒であれば(多くの言語でそうであるように)まず位置引数の指定を行った後に名前付き引数の指定をすると覚えてもらっても構いませんが、 Rarakuの場合、正確には次に説明する規則により各々の引数指定の可否を決定しており、 この規則を満たすならば名前付き引数の指定の後でも位置引数指定が行えます。

Rarakuでは名前付き引数を使ったときにそれが位置引数(通常の引数)において本来指定すべき位置とズレている場合、 「名前付き引数必須モード」へ移行します。 そしてひとたび「名前付き引数必須モード」へ移行した後は、位置引数を指定することはもう許可されず、 残りの引数は全て名前付き引数で指定しなければなりません。 これは具体的に以下の例を見た方がわかりやすいでしょう。

function func( x int, y int, z int )
{
	/* something */
}
func( 10, y:20, 30   ) /* A:OK */
func( 10, z:20, y:30 ) /* B:OK */
func( y:20, x:10, 30   ) /* C:NG Compile error */

上記のAの指定「func( 10, y:20, 30 )」では、名前付き引数「y:20」が本来 y が来るべき位置(2番目の位置)で 指定されていますので「名前付き引数必須モード」へは移行しません。 その次に位置引数として30が指定されていますが、これは許可されます(コンパイルエラーとはなりません)。 この30は(名前付き引数の指定も含めて)3番目の位置に指定されていますので、仮引数における3番目、すなわちzの引数として指定されたものとみなされます。

次のBの指定「func( 10, z:20, y:30 )」では、名前付き引数「z:20」が本来 z が来るべき位置(3番目の位置)で 指定されておらず、2番目の位置で指定されています。 そのため、この2番目の位置の時点で「名前付き引数必須モード」へ移行し、それ以降の指定もすべて名前付き引数で指定しなければなりません。 Bの指定ではその後の指定(「y:30」)も名前付き引数であり、(2番目以降は)最後まで位置引数は1つもありませんので、 これは許可されます(コンパイルエラーとはなりません)。

次のCの指定「func( 10, z:20, 30 )」では、名前付き引数「y:20」が本来 y が来るべき位置(2番目の位置)で 指定されておらず、1番目の位置で指定されています。 そのため、この1番目の位置の時点で「名前付き引数必須モード」へ移行し、それ以降の指定もすべて名前付き引数で指定しなければなりません。 しかしCの指定ではその後の指定 30 が位置引数であり、これは許可されません(コンパイルエラーとなります)。

位置引数と名前付き引数の指定が競合するような指定はコンパイルエラーとなります。 例えば以下の通りです。

function func( x int, y int, z int )
{
	/* something */
}
func( 10, x:10, z:30 ) /* Compile error */

上記では、まず1番目で位置引数により引数 x の値を10で指定しており、 次に2番目でも名前付き引数により引数 x の値を10で指定しています。 今回はたまたま同じ値10を指定している形にはなっていますがそこは問題ではなく、 そもそも2箇所から同一の引数xを重複して指定するような形になりますのでこれは許可されずコンパイルエラーとなります。

言語仕様の背景: 名前付き引数必須モード
Rarakuにおける名前付き引数指定の制限(すなわち「名前付き引数必須モード」へ移行するといった制限)は、 若干ややこしく見えるかもしれません。

このような制限を設けている理由は、名前付き引数の後に再び位置引数を指定をした場合、 その関数の仮引数の仕様が変更になったときに起きる無用な混乱を(通常の位置引数指定においても元々発生し得るタイプの混乱は仕方ないにしても) なるべく小さく抑えたいからです。

そもそも名前付き引数の後に再び位置引数を指定をすることを一切許可しない言語も多いですが (実際、そちらの方がさらに強い制限ではありますが)、Rarakuは条件付きでそれを部分的に認めている形になります。

もっと極端になると、Swiftのようにデフォルトでは名前付き引数を強制するほどの言語もありますが(確かにそれが関数の仕様変更に対する一番安全な対策ではありますが)、 名前付き引数ばかりとなりますと今度は関数呼び出しの記述がその分煩雑になりますし、 関数の仮引数名を単に別名に変更しただけでも、その関数を呼び出している全ての箇所で修正が必要となります。 その修正作業が(名前付き引数を使っている以上、ある程度は仕方ないにしても)あまりに大変になるようだと それはそれで問題ですので、Rarakuではそこまで極端な仕様にはしていません。

例えば以下をご覧下さい。

function transfer( dst string, dst_pos uint=0,
	src conststr, src_pos uint=0, src_leng uint=Rrk_NPOS ) /* version 1 */
{
	/* something */
}
varia v string
transfer( v,    src:"hello", src_pos:1 ) /* A */
transfer( v, 0, src:"hello", 1 ) /* B */
transfer( v,    src:"hello", 1 ) /* C */

Aの指定では特に何も問題ありません。 Bの指定で4番目に、またCの指定で3番目に指定している 1 は、(引数srcを指定の直後に整数を指定しているこの流れからしますと) 多分プログラマがsrc_posの値のつもりで指定している可能性が高いのではないでしょうか? この意図での指定であるとしますと、Bの指定も特に問題ありません。

現状のRarakuの仕様では、上記のAとBの指定は許可されますがCの指定は最初から許可されません (Cではsrcの名前付き引数の指定が3番目となっていませんので、それ以降の位置引数の指定 1 が許可されません)。 今仮に、Rarakuがそれを許可したとしても、いずれにせよCの指定では 3番目に来るべき型はconststr型であり、型の不一致でコンパイルエラーとなります。

さて、ここで関数transferをversion 2として次のように変更したとしましょう。

function transfer( dst string, dst_pos uint=0, dst_end=Rrk_NPOS, dst_size uint=Rrk_NPOS
	src conststr, src_pos uint=0, src_leng uint=Rrk_NPOS ) /* version 2 */
{
	/* something */
}
varia v string
transfer( v,    src:"hello", src_pos:1 ) /* A */
transfer( v, 0, src:"hello", 1 ) /* B */
transfer( v,    src:"hello", 1 ) /* C */

この変更でも、Aの指定では特に修正しなくても何も問題ありません(このあたりは名前付き引数そのものの効果です)。

Bの指定において、4番目に指定している 1 は、やはりプログラマがsrc_posの値として指定しているものと思われますが、 version 2への修正の結果、これがdst_sizeに指定される形になってしまいます。 これは修正しなければなりません。

また、src:"hello"の指定位置が(修正後のversion 2 のtransferにおいて)本来指定すべき位置からズレた(5番目ではなく3番目になった)ため、 現状のRarakuの仕様ではこの時点で「名前付き引数必須モード」へ移行し、次の指定 1 でコンパイルエラーとなります。 そのため、何か異常が発生していることにプログラマは気づくことができます。 逆にもしも「名前付き引数必須モード」に入らない仕様であったとしますと、この例の場合、ここでコンパイルエラーが発生せず、 誤りを見逃してしまう恐れがあります。

Cの指定においても同様で、3番目に指定している 1 は、やはりプログラマがsrc_posの値として指定しているものと思われますが、 version 2への修正の結果、これがdst_sizeに指定される形になってしまいます。 これは修正しなければなりません。

また、src:"hello"の指定位置もまた同様に(修正後のversion 2 のtransferにおいて)本来指定すべき位置からズレた(5番目ではなく2番目になった)ため、 現状のRarakuの仕様ではこの時点で「名前付き引数必須モード」へ移行し、次の指定 1 でコンパイルエラーとなります。 そのため、何か異常が発生していることにプログラマは気づくことができます(もっともCの指定ではversion 1の段階で既にコンパイルエラーにはなっていましたので、既になんらかの変更を加えている可能性はあります)。 逆にもしも「名前付き引数必須モード」に入らない仕様であったとしますと、この例の場合、ここでコンパイルエラーが発生せず、 誤りを見逃してしまう恐れがあります。

もう一つ全く別の例も見てみましょう。 以下をご覧下さい。

function copy( dst_id uint, dst_pos uint=0, src_id uint, src_pos uint=0 ) /* version 1 */
{
	/* something */
}
varia my_id   = 1000u
varia your_id = 1001u
copy( my_id, 0, your_id, 1 ) /* A */
copy( my_id, 0, src_id:your_id, 1 ) /* B */

あまりあることではありませんが、今度の例では全ての引数が同じ型(uint型)になっています。 またAの指定ではすべて位置引数、Bの指定では3番目の本来src_idを指定すべき位置においてsrc_idの名前付き引数を指定しています。 AとBのどちらの指定も許可されますが、このような場合、むしろ名前付き引数を混ぜたBの指定の方が関数の仕様変更における誤りの発見という意味では安全になります。 ここで関数copyをversion 2として次のように変更したとします。

function copy(
	dst_id uint, dst_pos uint=0, dst_size uint=Rrk_NPOS,
	src_id uint, src_pos uint=0, src_size uint=Rrk_NPOS ) /* version 2 */
{
	/* something */
}
varia my_id   = 1000u
varia your_id = 1001u
copy( my_id, 0, your_id, 1 ) /* A */
copy( my_id, 0, src_id:your_id, 1 ) /* B */

Aの指定において、4番目に指定している 1 は、元々プログラマがsrc_posの値として指定していたものですが、 version 2への修正の結果、これがsrc_idに指定される形になってしまいます。 これは修正しなければなりませんが、型的にはすべての引数でマッチしてしまい、残りもデフォルト引数で処理されてしまう関係でコンパイルエラーが発生しません。 この意味ではデフォルト引数を使い過ぎると位置引数による指定で危険性が増大します

一方、Bの指定においても同様に、4番目に指定している 1 は、元々プログラマがsrc_posの値として指定していたものですが、 version 2への修正の結果、これがsrc_idに指定される形になってしまいます。 これは修正しなければなりませんが、 version 2への修正の結果、「src_id:your_id」の指定が本来指定されるべき位置からズレた(4番目ではなく3番目になった)ため、 「名前付き引数必須モード」に入り、次の指定 1 でうまくコンパイルエラーを発生させることができます。

これが「名前付き引数必須モード」に入ることの恩恵となります。 尚、この仕様はC#における名前付き引数の指定の制限とほぼ同様であり、実際Rarakuでの仕様もそれを参考にしています。

Close


ユーザ定義関数のまとめ


関数を定義するには、まずfunction、関数の名前、仮引数の羅列、その関数の戻り値の型の順で記述します。 次に「{」と「}」の内部に好きな文を書いて具体的な処理を実装しましょう。

戻り値を返して関数を終了するにはreturnを使います。 戻り値がない場合で、関数を途中で中断したい場合は、単にreturnと記述します。 戻り値がない場合であれば、関数の最後のreturnは省略できます。

引数がない関数を定義する場合、引数の指定に何も指定せず単に「( )」と記述することもできます。 refer指定された引数(call by reference)では、その引数の大元の実体を関数内から直接更新することができます。 conststr型の引数は関数内でその文字列の値を変更することは許可されませんが、string型の引数ではそれが許可されます。 「仮引数の羅列」においては、必要に応じて適切な引数を宣言しましょう。

関数内部であってもstatic指定されたローカル変数は特別です。 この実体は一つであり、初期化は最初の1回目のみ行われます。 またこの変数は関数を抜けてもその寿命は維持されます。



目次に戻る

比較演算子と論理演算子と三項演算子

数値の比較演算子


数値の比較演算子は、二項演算子の一種です。 二つの数値型オペランドの大小関係を比較します。 この演算子を使った式は条件式となり、その値はbool型となります。

ここまでにもいくつか比較演算子を使用してきましたが、このセクションでは改めてそれらを紹介し整理しておきます。 Rarakuにおける数値の比較演算子は以下の6つです(この例では整数aとbを比較し、その結果をbool型変数rに格納しています)。

varia r bool
varia a = 1
varia b = 2
/* 演算子 > */
r = ( a > b )  /* a が b より大きい場合 true さもなくば false */

/* 演算子 >= */
r = ( a >= b ) /* a が b より大きいか等しい場合 true さもなくば false */

/* 演算子 < */
r = ( a < b )  /* a が b より小さい場合 true さもなくば false */

/* 演算子 <= */
r = ( a <= b ) /* a が b より小さいか等しい場合 true さもなくば false */

/* 演算子 == */
r = ( a == b ) /* a と b が等しい場合 true さもなくば false */

/* 演算子 != */
r = ( a != b ) /* a と b が等しくない場合 true さもなくば false */

上の例では処理が行われる順番をわかりやすくするため敢えて比較演算子の方を括弧で括っていますが、 実際はこの括弧がなくともこの順で評価されます。 「=」と「==」が似ていて紛らわしいと感じられるかもしれません。 「=」は代入を意味する演算子ですが、一方で「==」は等しいか否かの比較をしその結果をbool値として得るための演算子であることに注意してください。

文字列の比較演算子


文字列の比較演算子は、二項演算子の一種です。 二つの文字列型オペランドの辞書における順序関係(辞書順)で比較します。 この演算子を使った式は条件式となり、その値はbool型となります。

Rarakuにおける文字列の比較演算子は以下の6つです(この例では文字列aとbを比較し、その結果をbool型変数rに格納しています)。

varia r bool
varia a conststr = "hello"
varia b conststr = "world"
/* 演算子 > */
r = a > b  /* a が b より辞書順で後なら true さもなくば false */

/* 演算子 >= */
r = a >= b /* a が b より辞書順で後か等しいなら true さもなくば false */

/* 演算子 < */
r = a < b  /* a が b より辞書順で前なら true さもなくば false */

/* 演算子 <= */
r = a <= b /* a が b より辞書順で前か等しいなら true さもなくば false */

/* 演算子 == */
r = a == b /* a と b が等しい場合 true さもなくば false */

/* 演算子 != */
r = a != b /* a と b が等しくない場合 true さもなくば false */

今回は比較演算子の方を括弧で括っていませんが、評価の順番は一つ前の例と同様です。

空文字「""」は辞書順で最初の文字列(順序関係において最小の文字列)とみなされます。

また特殊なケースとして、Rarakuには無効値を表すnullを文字列型に格納することができる場合があります。 詳しくは「nullableとnotnull」のセクションで説明しますが、このnullを格納可能な文字列(nullable文字列)を宣言するには 「conststr/string」の替わりに「conststr^?/string^?」というように後ろに「^?」を付けて宣言します。

Rarakuではnullが格納された文字列変数/定数も普通に比較することができ、 その場合nullは辞書順で最後の文字列(順序関係において最大の文字列)とみなされます。 また変数/定数ではなく直接コード上に「null」と表記されたものをnullリテラルと呼びます。 nullリテラルは比較相手がconststr/string型変数や文字列リテラルの場合のみ比較でき、 同じく辞書順で最後の文字列とみなされます。

空文字「""」が(辞書順における)最小の文字列という仕様は、例えば(辞書順における)最大の文字列を求める場合に便利です。 最初に初期値を「""」としたstring変数maxを用意し、それより大きい文字列が現れたらmaxの値を更新していく形になるでしょう。

nullが(辞書順における)最大の文字列という仕様は、例えば(辞書順における)最小の文字列を求める場合に便利です。 最初に初期値をnullとしたstring変数minを用意し、それより小さい文字列が現れたらminの値を更新していく形になるでしょう。
C言語をご存知の方は、この順序関係はstrcmp標準関数を使った場合の結果と合致すると考えて構いません (実際RarakuVM内部でもこれを呼び出しています)。 つまりA>Bがtrueになるのは strcmp(A,B)>0 となる場合、 A==Bがtrueになるのは strcmp(A,B)==0 となる場合、 A<Bがtrueになるのは strcmp(A,B)<0 となる場合にそれぞれ相当します。

ただしstrcmp関数においては、文字列とNULLの比較は不可能です。 よってnullとの比較に関しては、strcmp関数とはまた別にRarakuで独自に処理しています。

論理演算子「&&」


条件式を複数組み合わせて、複雑な条件式を記述することができます。 そのためには論理演算子と呼ばれるものを使います。 論理演算子では通常、条件式がオペランドになります。

演算子「&&」は論理積と呼ばれ、論理演算子の一種です(&ではなく&&です)。 これは二つの条件式AとBがあったとき、「A && B」といった書式で記述し、 「AかつB」といった意味合いの新しい条件式を作ります。 この新しい条件式はAとBが同時にtrueである場合にのみ全体としての値が true になります。 さもなければ(全体としての値が)false になります (C言語における「&&」と同じです)。

例えば以下をご覧ください。

varia ival = 3
if ival >= 0 && ival < 10 {
	Rrk_print( "one-digit number\n" )
}

上の例では条件式「ival >= 0」と「ival < 10」を演算子「&&」で連結して 一つの条件式「ival >= 0 && ival < 10」を作っています。

演算子が3つもあり複雑な式ですが、 これは括弧を使って書くと「(ival >= 0) && (ival < 10)」と同じことです。 比較演算子と演算子「&&」をいっしょに使った場合はこのように比較演算子の方がより基本的な構成単位になると考えてください。 これを比較演算子の方が優先順位が高いと言うこともあります。

この条件式は「ival >= 0」と「ival < 10」が両方同時に成り立つ場合のみtrueとなります。 この条件式により、ival が 0以上10未満の場合のみ、Rrk_print( "one-digit number\n" ) が実行される形になります。

参考: &&演算子における短絡評価(short-circuit evaluation)
演算子「&&」においては、まずその左側の条件式が最初に評価され、 その左側の条件式の値が true である場合のみ、その右側の条件式が評価されます。 これは逆に、左側の条件式の値が false であった場合、右側の条件式は一切評価されないことを意味します。 このような条件式の評価方式を短絡評価(short-circuit evaluation)と呼びます。

このことは通常の条件式の比較においてはあまり気にする必要がありませんが、 右側で何か特殊な副作用を生じるような関数等を実行する場合は意識しなければならない点になります。

例えば以下の例をご覧ください。

function func( void ) bool
{
	Rrk_print( "in func.\n" )
	return true
}
varia ival = 3
if ival < 0 && func() {
	Rrk_print( "in if-block.\n" )
}

上の例のif文では、まず「ival<0」が評価されますが、これは false となります。 このとき短絡評価によって右側の「func()」は評価されません。 言い換えれば、関数 func は実行されず、その呼び出しが行われません。 結果的に関数 func 内の Rrk_print( "in func.\n" ) の実行も行われません。

もしも(ivalの値に関わらず)関数funcを確実に実行したいのであれば、 if文の外で予め「func()」を実行し、その戻り値をbool型変数で保存しておき、 演算子「&&」の右側のオペランドにはそのbool型変数を指定するなどすればよいでしょう。

function func( void ) bool
{
	Rrk_print( "in func.\n" )
	return true
}
varia ival = 3
const result = func() /* 予めfuncを実行し、戻り値(bool型)も保存しておく */
if ival < 0 && result {
	Rrk_print( "in if-block.\n" )
}

あるいは以下のように演算子「&&」の右側と左側のオペランドの指定を逆にしてもよいでしょう (左側ならば確実に実行されるからです)。

function func( void ) bool
{
	Rrk_print( "in func.\n" )
	return true
}
varia ival = 3
if func() && ival < 0 { /* 先にfunc()を左側で実行しておく */
	Rrk_print( "in if-block.\n" )
}

Close


参考: 演算子「&&」のオペランドに整数を指定した場合
基本的に演算子「&&」にはオペランドとして条件式を指定しますが、条件式の替わりに整数値を指定できる場合もあります。 例えば以下をご覧ください。

varia ival = 3
varia jval = 4
if ival && jval {
	Rrk_print( "in if-block.\n" )
}

この例のように 演算子「&&」にint型のオペランドivalを与えた場合、これが特別な条件式として機能します。 この場合、ivalが非ゼロであれば true、ゼロであれば false となる条件式とみなされます。 jvalの場合も全く同様です。 つまり上記は以下のように書いたのと全く同じです。

varia ival = 3
varia jval = 4
if ival != 0 && jval != 0 {
	Rrk_print( "in if-block.\n" )
}

整数値以外にも構造体変数やnatvobj型をオペランドとして指定することもでき、その場合も全く同様に機能します。 構造体変数については「構造体」のセクションで、 natvobj型については「ファイル入出力とネイティブオブジェクト型」のセクションで詳しく述べます。

Close


論理演算子「||」


演算子「||」は論理和と呼ばれ、論理演算子の一種です(|ではなく||です)。 これは二つの条件式AとBがあったとき、「A || B」といった書式で記述し、 「AまたはB」といった意味合いの新しい条件式を作ります。 この新しい条件式はAとBが同時にfalseである場合にのみ全体としての値が false になります。 さもなければ(全体としての値が)true になります (C言語における「||」と同じです)。

例えば以下をご覧ください。

varia ival = 3
if ival <= -10 || ival >= 10 {
	Rrk_print( "The absolute value is greater than 10.\n" )
}

上の例では条件式「ival <= -10」と「ival >= 10」を演算子「||」で連結して 一つの条件式「ival <= -10 || ival >= 10」を作っています。

演算子が3つもあり複雑な式ですが、 これは括弧を使って書くと「(ival <= -10) || (ival >= 10)」と同じことです。 比較演算子と演算子「||」をいっしょに使った場合はこのように比較演算子の方が優先順位が高くなります。

この条件式は「ival <= -10」と「ival >= 10」のいずれか一方、または両方成り立つ場合にtrueとなります (今回の場合は両方成り立つということはあり得ませんが)。 この条件式により、ival の絶対値が10以上の場合のみ、Rrk_print( "The absolute value is greater than 10.\n" ) が実行される形になります。

参考: ||演算子における短絡評価(short-circuit evaluation)
演算子「||」においては、まずその左側の条件式が最初に評価され、 その左側の条件式の値が false である場合のみ、その右側の条件式が評価されます。 これは逆に、左側の条件式の値が true であった場合、右側の条件式は一切評価されないことを意味します。 このような条件式の評価方式も短絡評価(short-circuit evaluation)と呼びます (これは演算子「&&」における短絡評価とちょうど逆の対応になっています)。

このことは通常の条件式の比較においてはあまり気にする必要がありませんが、 右側で何か特殊な副作用を生じるような関数等を実行する場合は意識しなければならない点になります。 これは演算子「&&」における注意点と全く同様です。

Close


参考: 演算子「||」のオペランドに整数を指定した場合
基本的に演算子「||」にはオペランドとして条件式を指定しますが、条件式の替わりに整数値を指定できる場合もあります。 これは演算子「&&」のオペランドに整数値を指定した場合と全く同様に評価されます。 例えば以下をご覧ください。

varia ival = 0
varia jval = 4
if ival || jval {
	Rrk_print( "in if-block.\n" )
}

この例のように 演算子「||」にint型のオペランドivalを与えた場合、これが特別な条件式として機能します。 この場合、ivalが非ゼロであれば true、ゼロであれば false となる条件式とみなされます。 jvalの場合も全く同様です。 つまり上記は以下のように書いたのと全く同じです。

varia ival = 3
varia jval = 4
if ival != 0 || jval != 0 {
	Rrk_print( "in if-block.\n" )
}

整数値以外にもnatvobj型をオペランドとして指定することもでき、その場合も全く同様に機能します。 構造体変数については「構造体」のセクションで、 natvobj型については「ファイル入出力とネイティブオブジェクト型」のセクションで詳しく述べます。

Close


論理演算子「!」


演算子「!」は論理否定と呼ばれ、論理演算子の一種です。 これは条件式Aがあったとき、「!A」といった書式で記述し、 「Aではない場合」といった意味合いの新しい条件式を作ります。 この新しい条件式はAの真偽を逆転させます。 すなわちAがfalseである場合に値が true に、Aが true である場合に値が false になります (C言語における「!」と同じです)。

演算子「!」のオペランドは一つだけです。 このような演算子を単項演算子と呼びます。

例えば以下をご覧ください。

varia ival = 3

if !(ival >= 0) {
	Rrk_print( "negative number.\n" )
}

この例のif文では条件式「ival >= 0」を「!」で論理否定していますので、 結局if文の条件式全体としては「ival < 0」が指定されたのと同じになります。

論理演算子「!」の優先順位は(論理演算子の中では)特別で比較演算子よりも高いことに注意してください。 つまり今回の「!(ival >= 0)」という条件式では括弧は必須です。 括弧を使わずに「!ival >= 0」と書いてしまうと「(!ival) >= 0」といった意味になってしまいます。

参考: 演算子「!」のオペランドに整数を指定した場合
基本的に演算子「!」にはオペランドとして条件式を指定しますが、条件式の替わりに整数値を指定できる場合もあります。 例えば以下をご覧ください。

varia ival = 0
varia ans int
if !ival {
	ans = 10 / ival
}

この例のように 演算子「!」にint型のオペランドivalを与えた場合、これが特別な条件式として機能します。 この場合、ivalが非ゼロであれば true、ゼロであれば false となる条件式とみなされます。 つまり上記は以下のように書いたのと全く同じです。

varia ival = 0
varia ans int
if ival != 0 {
	ans = 10 / ival
}

整数値以外にもnatvobj型をオペランドとして指定することもでき、その場合も全く同様に機能します。 構造体変数については「構造体」のセクションで、 natvobj型については「ファイル入出力とネイティブオブジェクト型」のセクションで詳しく述べます。

Close


論理演算子の組み合わせ


以下の例のように複数の論理演算子を組み合わせることもできます。

varia x = -5
varia y = -6
if !( x >= 0 || y >= 0 ) {
	Rrk_print( "Fourth quadrant.\n" )
}

この例ではまず「x >= 0 || y >= 0」という条件式が評価され、その結果に対し演算子「!」を施して真偽を逆にします。

この条件式「!( x >= 0 || y >= 0)」はド・モルガンの法則より結局「x < 0 && y < 0」と等価な条件式になります。 (x, y)を数学のxy座標上の点と見れば、第4象限にある(境界を含まない)場合に条件式がtrueになります。

論理演算子が複数あるような場合、Rarakuでは一応「!」「&&」「||」の順で評価されますが、 この優先順位のルールは算術演算子と比べるとあまりわかりやすいものではありません。 このルールを覚えるよりも、優先的に評価したい論理演算の方を常に括弧で明示的に囲い、 コード上で優先順位が見える形にしておくことを強くお勧めします。

例えば以下の例の3つのif文の条件式はすべて同じものです。

varia x = -5
varia y = -6

/* 括弧を使わずに書くこともできるが、ぱっと見での構造がわかりにくい */
if x >= 0 && y >= 0 || x < 0 && y < 0 {
	/* something */
}

/* 括弧を使って明示的に書いた場合 */
if ( x >= 0 && y >= 0 ) || ( x < 0 && y < 0 ) {
	/* something */
}

/* 括弧を使って明示的に書き、さらに改行した場合 */
if   ( x >= 0 && y >= 0 )
  || ( x <  0 && y <  0 )
{
	/* something */
}

この条件式は、論理演算子の優先順位のルールから本来は1番目のように括弧をまったく使わずに書くことができます。 しかし1番目のように括弧を使わずに書くと、ざっくりとした全体の構造を捉えるだけでも細部の演算子の意味を考えなければなりません。 2番目のように括弧を使うと、とりあえず括弧を追うだけで構造を捉えることができ、かなりすっきりして見えます。 場合によっては3番目のように途中で改行して書いた方がさらにわかりやすくなります。

とはいえ以下のように比較演算子にまで括弧で囲うのは(比較演算子の式がとてつもなく複雑な場合はまた別ですが)さすがに冗長かと思います。

varia x = -5
varia y = -6

if ( (x >= 0) && (y >= 0) ) || ( (x < 0) && (y < 0) ) {
	/* something */
}

「!」を除く論理演算子(つまり「&&」と「||」)と比較演算子をいっしょに使った場合は、比較演算子の方が優先順位が高いことは覚えておいた方がいいでしょう。 ただし論理否定演算子「!」が頭に付く場合は例外です。 この場合は(何か特別な意図がない限りは)以下のように比較演算子の側に必ず括弧をつけましょう

varia x = -5
varia y = -6

if !(x >= 0) && !(y >= 0) {
	/* something */
}

三項演算子


条件式の値が真か偽かによって初期化する値を切り替えたい場合、一つの方法としては一旦varia文で初期化なしで宣言し、 その後に if文を使って初期値を代入するいう方法があります。 例えば以下のようになります。

varia x = 10
varia y = 20

varia z int
if x < y { /* 条件式の結果に応じて z を二通りで初期化 */
	z = 100
} else {
	z = 200
}

ただしこのようにif文を使った初期化は const 文との相性がよくありません。 const 文の場合、初期化後は値の代入が許可されませんが、if文による初期化ではその代入が(const文以降に)発生するからです。 そしてこのようなif文を const 文の右辺に記述することもできません。

一方、三項演算子は式の一種で const 文と相性のよい記法になります。 これは「条件式 ? A : B」という書式になり、条件式が真の場合はこの式全体の値が A、偽の場合は B となります。 これによりif文と同等の処理が可能になるわけです。 例えば以下のようになります。

varia x = 10
varia y = 20

const z int = x < y ? 100 : 200

上の例では「x < y」が真となりますので、const文の右辺の値は 100 となり、それが z の初期値となります。 三項演算子「条件式 ? A : B」において、AとBの型は同じでなければなりません。 また三項演算子全体としてもAとBと同じ型を持ちます。

varia/const文の右辺として三項演算子を指定した場合、その型指定を省略することができます。 このとき宣言された変数/定数の型は、三項演算子の型より自動的に型推論されます。 例えば以下の通りです。

varia x = 10
varia y = 20

const z = x < y ? 100 : 200

上記では三項演算子内の100と200がint型ですので、zの型もint型と型推論されることになります。

三項演算子を連鎖させることで2つ以上の条件式を使って3つ以上の値を切り替えて初期化することもできます。 例えば以下のようになります。

varia x = 10

const w = x > 0 ? 100 : x < 0 ? -100 : 0

上の例では「x > 0」が真ならば式全体の値は100、そうではなく「x < 0」が真ならばその値は-100、それらのいずれでもなければその値は 0 となります。 つまりこれはあたかもif-elif-else文の組み合わせと同じように機能します。 ただ、三項演算子をこのように連鎖させる場合1行で一気に書くとやや見辛いので、 以下のように少し工夫して記述した方がいいでしょう。

varia x = 10

const w =
	x > 0 ?  100 : /* if   x > 0 */
	x < 0 ? -100 : /* elif x < 0 */
	0              /* else */

上の例はインデントや改行を入れただけで一つ前の例と全く同じものですが、 条件式が始まる部分を行頭に置き、行末に「:」を置いてそこで改行しています。 これによりこの三項演算子全体としての if-elif-else の構造が見やすくなります。

値が文字列の場合でも同様に記述できます。 例えば以下のようになります。

varia x = 10

const name =
	x >=  10 ? "positive big number" :
	x >    0 ? "positive one-digit" :
	x <= -10 ? "negative small number" :
	x <    0 ? "negative one-digit" :
	"zero"

上の例は x が「10以上か」、そうではないが「0より大きいか」、そうではないが「-10以下か」、そうではないが「0より小さいか」、それらのどれでもない(つまり0)かで条件を置き、 その結果に応じて異なる文字列をnameに代入しています(この例ではnameの型は文字列リテラルと同じ型、すなわちconststrとなります)。

三項演算子と文字列の連結


文字列連結演算子「&」の方が三項演算子よりも優先順位は高いです。 従って文字列の連結処理の中へ三項演算子を埋め込みたい場合、以下のように三項演算子全体を括弧で囲わなければなりません

varia str string^? = null
const statement = "str is " & ( str ? str : "This is null!" ) & ".\n"
Rrk_print( statement )

上記ではstrはnullで初期化されていますが、このような場合「string」ではなく「string^?」と記述する必要があります。 詳しくは「nullableとnotnull」のセクションで説明しますが、今の段階ではとりあえずそういうものだと考えてください。 尚、Rarakuではnullと通常の文字列を連結することができ、その場合nullの方は"null"といった文字列に変換されます。
上記の「str ? str : "This is null!"」の部分が三項演算子となりますが、ここではそれを括弧で囲っており、 その条件部では文字列型の識別子strを与えています。 この場合、strの値がnullの時は条件部はfalse、非nullの場合はtrueと評価されます。

さて、これでこの話を終わりにしてもよいのですが、もしもこれを括弧で囲い忘れた場合どうなるかが気になる人向けに、 以下ではその補足説明しましょう。 「&」と三項演算子を混ぜて使っており、かつその三項演算子を括弧で囲うのを忘れた場合、Rarakuは多分コンパイルエラーを出すはずです。 例えば以下の通りです。

varia str string^? = null
const statement = "str is " & str ? str : "This is null!" & ".\n"
Rrk_print( statement )

上記の例では、「"str is " & str」の塊が三項演算子の条件部とみなされ、またこれは文字列の連結表現となっています。 Rarakuでは三項演算子の条件部に文字列の連結表現を「直に」与えた場合、コンパイルエラーとなる少し変わった仕様があります (文字列の連結表現も文字列型ではあるのですが、このように連結された式で与えた場合は特例として問答無用でコンパイルエラーとなります)。

言語仕様の背景: これがコンパイルエラーとなる理由
上記のような例では、プログラマは「"str is " & str」の塊を三項演算子の条件部としてみなして欲しいのではなく、 「str ? str : "This is null!"」を一つの塊とし、「str」を条件部としてみなして欲しいと思って記述していると考えられます。 そしてプログラマがそのような意図であるにも関わらず、上記のように括弧を囲い忘れている状況は十分想定されます。 この囲い忘れを検出するため、Rarakuで三項演算子の条件部に文字列の連結表現を与えた場合、コンパイルエラーとなる仕様にしました。

括弧がなくとも「str ? str : "This is null!"」をRarakuが自動的に一つの塊と解釈できれば一番よいのですが、 そのためには三項演算子の優先順位を「&」演算子よりも高くする必要があります。 しかし仮に三項演算子の優先順位を「&」をより高く設定すると、同時にその優先順位が論理演算子よりも高くなることになってしまいます。 三項演算子の条件部には論理演算子を頻繁に書くことになりますが、今度はそこに括弧が必須になり、より深刻な別の問題が発生してしまうことになります。

もう一つの方法として「&」演算子の優先順位をずっと低くする方法も考えられますが、 「&」は文字列連結のための演算子ですので(つまりそれによって生成されるものが文字列ですので)、 その優先順位を文字列と比べてあまりにかけ離れて低くしてしまうのもまた問題になります。 例えば三項演算子の第3番目の引数に「&」で連結された文字列を書きたい場合、今度はそこに括弧が必須になってしまいます。

結局どのような仕様にしてもこのような状況の場合、なんらかの形で括弧を明記する必要性は避けられないように思います。

そもそも「"str is " & str」のような文字列連結表現を条件式として評価した場合、 (単なる文字列型の識別子が与えられた場合とは異なり)これは明らかに(文字列として)非nullですから、 その条件式は常にtrueとなり、(副作用がある場合を除けば)比較すること自体無意味となります。 そういうわけでこのような条件式に意味はないのですが、何か理由があってどうしても「"str is " & str」の塊を 三項演算子の条件部とみなして欲しいならば、以下のように記述すれば一応可能です。

varia str string^? = null
const statement = ("str is " & str)->string ? str : "This is null!" & ".\n"
Rrk_print( statement )

三項演算子の条件部を単に括弧で括るだけでは、依然としてRarakuパーザはこれを文字列連結式とみなします。 そのため、これをさらにstring型でキャストし、パーザのチェックを回避している形になります。

ただ繰り返しますがこのような記述に意味はない上、 上記の例では一番最後の「"This is null!" & ".\n"」の塊もまるごと三項演算子の第3番目のオペランドとみなされてしまうことに注意してください。

尚、Rrk_print関数にはnullを指定することもでき、その場合は"null"と表示されます。

Close


実はこの仕様をもってしても括弧の付け忘れを検出できないような厄介なケースもあります。 例えば以下の例をご覧下さい。

varia x = 1
const statement = "x is " & ( x == 0 ? "zero" : "not zero" ) & ".\n"
Rrk_print( statement )

上記の例では、「x == 0 ? "zero" : "not zero"」の部分が三項演算子となりますが、それを括弧で囲っています。 これは正しい記述ですが、仮にこの括弧を付け忘れたとしましょう。 以下をご覧下さい。

varia x = 1
const statement = "x is " & x == 0 ? "zero" : "not zero" & ".\n" /* 三項演算子を括弧で囲っていない(コンパイルエラーとならない!) */
Rrk_print( statement )

上記の例では、「"x is " & x == 0」の塊が三項演算子の条件部とみなされます。 ところが残念ながらこれはコンパイルエラーになりません。 この例では(おそらくこれを記述したプログラマの期待に反して)まず「"x is " & x」の塊が文字列と解釈され、 その後に「"x is " & x == 0」の塊が(文字列と0を「==」で比較した)論理演算式と解釈され、内部の文字列連結演算は(木構造の)下層へ覆い隠されることになります。 つまりRarakuのパーザは三項演算子条件部内に潜む「&」演算子を検出できず、期待に反してコンパイルエラーとはならないわけです。

ちなみに以下のように「x == 0」の部分に括弧がある状況であれば、今度は(三項演算子条件部内に潜む「&」演算子を検出し)無事(?)コンパイルエラーとなりますが、 このようなケースの場合、そもそもそのような位置に括弧は付けないことの方が多いでしょう。

varia x = 1
const statement = "x is " & (x == 0) ? "zero" : "not zero" & ".\n" /* 三項演算子を括弧で囲っていない(コンパイルエラーとなる) */
Rrk_print( statement )

というわけで、括弧付け忘れの自動検出にも限界があります。 繰り返しになりますが、やはり文字列連結処理の中へ三項演算子を埋め込みたい場合は細心の注意を払い、不安があれば括弧を適宜使い、特に三項演算子全体を括弧で囲うのを忘れないようにしましょう。

そもそも文字列連結処理の中へ三項演算子を埋め込むなどといった入り組んだ記述をしないことも勿論できるわけです。 例えば上記の例は以下のように書くこともできます。

varia x = 1
const tmp = x == 0 ? "zero" : "not zero" /* 三項演算子の結果を一旦別の文で確保 */
const statement = "x is " & tmp & ".\n"
Rrk_print( statement )

そして大抵の場合、このように書いた方が可読性もよいでしょう。

あるいは「@['」と「']」で囲まれた文字列リテラルであれば、 変数埋め込み機能を有効にしますと、実は以下のように内部に三項演算子を書くこともできます。

varia x = 1
/* 三項演算子を${...}の中に埋め込む */
const statement = @$['x is ${ x == 0 ? "zero" : "not zero" }.']\n
Rrk_print( statement )

ただこの場合でも結局「${」と「}」で囲わなければならないわけで、括弧と囲う場合と大差ないような気もします。

文字列連結処理の中へ三項演算子を埋め込むような入り組んだ記述はどうしても必要な場合に留めるべきでしょう。 しかしあまり使わない記述だからこそ、そこに潜む問題を忘れやすいといった側面もありますので注意が必要です。

比較演算子と論理演算子と三項演算子のまとめ


比較演算子と論理演算子を組み合わせることでより複雑な条件式をif文等に指定できます。 三項演算子は const 文の初期化において、if-elif-elseのような処理で値を切り分けたい場合に便利です。 三項演算子を使う場合は適切に改行とインデントを行うことで見やすくなります。

文字列連結処理の中へ三項演算子を埋め込みたい場合は、三項演算子全体を括弧で囲いましょう。



目次に戻る

配列とモディファイア

配列とは


配列は同じ型の複数の変数を連続して並べたものです。 このとき、それぞれの変数に別々の名前をつけるのではなく、共通した同じ名前とインデックス番号の組み合わせでそれぞれを区分します。 区分されたそれぞれの変数を配列の要素と呼びます。

Rarakuにおける配列の型は、要素の型の後ろに [ ] または [ 正の整数 ] をつけたものになります。 そしてこのような指定はconst文やvaria文などの型として指定できます。 まず例を見てこれを概観しましょう。

varia a1 int[3]^?      = null /* 要素がint     型の固定長配列 */
varia a2 real[3]^?     = null /* 要素がreal    型の固定長配列 */
varia a3 conststr[3]^? = null /* 要素がconststr型の固定長配列 */
varia a4 string[3]^?   = null /* 要素がstring  型の固定長配列 */
varia a5 int[]^?       = null /* 要素がint     型の可変長配列 */
varia a6 real[]^?      = null /* 要素がreal    型の可変長配列 */
varia a7 conststr[]^?  = null /* 要素がconststr型の可変長配列 */
varia a8 string[]^?    = null /* 要素がstring  型の可変長配列 */

要素の型の後ろに [ 正の整数 ] が続く場合、サイズが固定の配列を意味し、これを固定長配列と呼びます。 例えば以下がこれに該当します。

varia a1 int[3]^?      = null /* 要素がint     型の固定長配列 */
varia a2 real[3]^?     = null /* 要素がreal    型の固定長配列 */
varia a3 conststr[3]^? = null /* 要素がconststr型の固定長配列 */
varia a4 string[3]^?   = null /* 要素がstring  型の固定長配列 */

また要素の型の後ろに [ ] が続く場合、サイズが可変な配列を意味し、これを可変長配列と呼びます。 例えば以下がこれに該当します。

varia a5 int[]^?       = null /* 要素がint     型の可変長配列 */
varia a6 real[]^?      = null /* 要素がreal    型の可変長配列 */
varia a7 conststr[]^?  = null /* 要素がconststr型の可変長配列 */
varia a8 string[]^?    = null /* 要素がstring  型の可変長配列 */

上記の例ではvaria文の右辺値として敢えて null を代入しています。

Rarakuではnullを代入可能な配列を宣言する場合、その型の最後に「^?」を付記する必要があります。

配列の値がnullの場合、その配列はまだ何も実体が確保されていない状態であり、各要素にアクセスもできません。

仮にこの状態で各要素にアクセスするような記述をした場合、Rarakuではコンパイルエラーとなります。 というより、そもそも上記のように後ろに「^?」が付いている配列型を宣言した場合、 そのままでは各要素にアクセスするような記述をすることが許可されません。 これをアクセス可能にするにはnotnull化する必要がありますが、 これに関しては「nullableとnotnull」のセクションで詳しく説明します。

配列の実体を確保するには(nullではなく)[ ] を代入する形になります。 例えば以下の通りです。

varia a1 int[3]      = [] /* 要素がint     型の固定長配列 */
varia a2 real[3]     = [] /* 要素がreal    型の固定長配列 */
varia a3 conststr[3] = [] /* 要素がconststr型の固定長配列 */
varia a4 string[3]   = [] /* 要素がstring  型の固定長配列 */
varia a5 int[]       = [] /* 要素がint     型の可変長配列 */
varia a6 real[]      = [] /* 要素がreal    型の可変長配列 */
varia a7 conststr[]  = [] /* 要素がconststr型の可変長配列 */
varia a8 string[]    = [] /* 要素がstring  型の可変長配列 */

しかしvaria文においては配列の右辺値として何も指定されていない場合は、そのデフォルト値は [ ] となります。 つまりこの例では、以下のように単に右辺値がない記述でOKです。

varia a1 int[3]      /* 要素がint     型の固定長配列 */
varia a2 real[3]     /* 要素がreal    型の固定長配列 */
varia a3 conststr[3] /* 要素がconststr型の固定長配列 */
varia a4 string[3]   /* 要素がstring  型の固定長配列 */
varia a5 int[]       /* 要素がint     型の可変長配列 */
varia a6 real[]      /* 要素がreal    型の可変長配列 */
varia a7 conststr[]  /* 要素がconststr型の可変長配列 */
varia a8 string[]    /* 要素がstring  型の可変長配列 */

ただし少し細かい注意点もありますので、次の項目ではそれについて詳しく述べます。

Rarakuでは配列が空(配列の要素数が0)であることと配列がnullであることは同じではありません。

内部実装の観点ではこの二つには違いがあり、配列が空の場合は配列自体の管理領域がメモリ上に確保されるのに対し、 配列がnullの場合は本当に何も確保されていない状態となり、初期化の効率は後者の方が若干上になります。 例えば配列を要素とするような配列がありかつその要素数が膨大である場合、 この初期化の速度差は次第に明確になります。

論理的には配列が空(配列の要素数が0)であることでもって、その配列の無効性をも示すといった考え方もあるとは思いますが、 Rarakuではこのような効率上の理由から、配列そのものがnullになることもできる仕様にしてあります (ただしその場合、配列型名の最後に「^?」を付記する必要はあります)。

尚、Rarakuではif文の条件式として配列を与えた場合、 配列が空(配列の要素数が0)の場合はtrue、配列がnullである場合はfalseと評価されます。

固定長配列と初期化子


まずは固定長配列のケースを考えます。 例えば以下の通りです。

varia a1 int[3]      /* 要素がint     型の固定長配列. 配列の実体を確保 */
varia a2 real[3]     /* 要素がreal    型の固定長配列. 配列の実体を確保 */
varia a3 conststr[3] /* 要素がconststr型の固定長配列. 配列の実体を確保 */
varia a4 string[3]   /* 要素がstring  型の固定長配列. 配列の実体を確保 */

上記ではすべて要素数3の配列の実体が確保されます。 実体が確保された状態であれば、この要素数の範囲内で各要素にアクセスすることができます。 Rarakuでは、配列変数名 [ インデックス ] という書式を式中に書くことにより、その配列の各要素にアクセスできます(この書式はC言語等と同じです)。 最初の要素のインデックスは0、次の要素のインデックスは1、その次の要素のインデックスは2、 このように0から始めて1ずつインデックスが増加して行く形になります。 例えば上記の配列a1の場合、a1[0]、a1[1]、a1[2] のように各要素にアクセスします。

以下のようにして各要素へ値を代入できます。

varia ary int[3]
ary[2] = 10 /* インデックス2の要素へアクセス(そこへ10を代入) */
Rrk_print( "ary[2]=" ary[2] "\n" )

要素数がNの配列の場合、配列の最終要素のインデックスは、その要素数から1を引いたN-1となることに注意しましょう。 N以上の値をインデックスとして与えるとランタイムエラーとなります。 例えば上記の配列aryの場合、その型は int[3] ですから要素数は3であり、インデックスは3-1で2まで、 つまり ary[0]、ary[1]、ary[2] までのアクセスが可能です。 これを超えたary[3]といったアクセスはランタイムエラーとなります。

配列の初期化子とは「[」で始まり、「]」で終わる範囲に、初期値となる要素をカンマで区切って右辺値として指定したものです。 例えば以下では配列 ary の各要素を 10, 20, 30 で初期化しています。

varia ary int[3] = [ 10, 20, 30, ]

初期化子の最後の要素(この例では30)の直後にあるカンマはあってもなくても構いません。

配列の型に指定する(要素数を表す)「[ ]」は初期化子ではありません。 例えば上記の例では「int[3]」における「[3]」の部分は初期化子ではなく型指定の一部であり、単に要素数を示しているだけです。 同じ「[」で始まり、「]」で終わる表記のものではありますが、両者は全く異なるものである点に注意してください。

この場合、( [ ] で代入したときと同様)まず要素数3の配列の実体が確保され、 また同時に 10, 20, 30 という値が各要素にそれぞれ代入されます。 つまり上記は以下を実行したのと結果的に同じです。

varia ary int[3] /* 要素数3の配列を確保 */
ary[0] = 10
ary[1] = 20
ary[2] = 30

初期化子はvaria/const文以外の通常の代入においても使用できます。 例えば以下のようになります。

varia ary int[3]
ary = [ 10, 20, 30, ]

初期化子は上記のような代入文の右辺値の他、関数の引数、return文の指定等でも使用することができます。 一方、通常の右辺値のように使用できない場合もあります。 例えば(後のセクションで登場する)foreach文の配列部等へ初期化子を直接指定することはできません。 また初期化子全体に対して算術演算子やキャスト演算子など他の演算子を適用することも通常できません。

整数以外、例えば文字列の配列の場合の初期化子の例も見ておきましょう。 次のようになります。

varia ary string[3] = [
	"apple",
	"orange",
	"banana",
]

初期化子の「[」と「]」の間に、その配列の型の要素数よりも少ない値を指定することもできます。 例えば以下をご覧ください。

varia ary int[3] /* 要素数3の固定長配列 */
ary = [ 10, 20, ] /* 初期化子で与えた値の個数は2個 */

上記の場合、ary[0]とary[1]についてはそれぞれ10、20が代入されますが、 ary[2]については、要素の型のデフォルト値が自動的に代入されます。 この場合、配列aryの要素の型はint型ですので、結局ary[2]の値は 0(int型のデフォルト値)になります。

またほとんど同じことですが、varia/const文の右辺に以下のような指定した場合も同様な挙動になります。

varia ary int[3] = [ 10, 20, ] /* 初期化子で与えた値の個数は2個(ary[2]については0で初期化される) */

初期化子で与える値の個数が1個の場合も同様です。 例えば以下の通りです。

varia ary conststr[3] = [ "apple", ] /* 初期化子で与えた値の個数は1個(ary[1], ary[2]については""で初期化される) */

初期化子で与える値の個数は0個でも構いません。 このとき「[」と「]」の間の記述は空となります。 そして実のところ、これが一番最初に配列の実体の確保のために右辺値として指定した「[ ]」に他なりません。 改めて初期化子「[ ]」を使った以下の例を確認しておきましょう。

varia ary int[3] = [] /* 初期化子で与えた値の個数は0個(ary[0], ary[1], ary[2]については0で初期化される) */

初期化子「[ ]」を指定した場合、まず配列の実体を確保し、次にその全要素を要素型のデフォルト値で初期化するというわけです。 初期化子「[ ]」はvaria/const文以外の通常の代入においても使用できます。 例えば以下のようになります。

varia ary int[3] = [ 10, 20, 30 ]
ary = [] /* この代入によって [ 0, 0, 0, ] に変更される */

varia文で初期化した時点での値 [ 10, 20, 30, ] はすべて消去される点に注意してください。 念のため他の例も見ておきます。 例えば以下の例では最後の30は残らず、0 に上書きされます。

varia ary int[3] = [ 10, 20, 30 ]
ary = [ 100, 200, ] /* この代入によって [ 100, 200, 0, ] に変更される */

varia文において配列をnullで初期化した場合、繰り返しになりますがまだ配列の実体は確保されません。 この場合、その後の通常の代入文において初期化子を代入した時点で、 初めてその実体が確保されることになります。 例えば次の通りです。

varia ary int[3]^? = null /* 敢えてnullで初期化: このタイミングでの無駄な確保はされなくなる */
ary = [] /* この代入によって初めて配列の実体が確保され、 [ 0, 0, 0, ] に初期化される */

可変長配列と初期化子


ここまでは固定長配列についての初期化子を見てきました。 次に可変長配列に初期化子を指定した場合の挙動を見ます。 例えば以下の通りです。

varia a5 int[]      /* 要素がint     型の可変長配列 */
varia a6 real[]     /* 要素がreal    型の可変長配列 */
varia a7 conststr[] /* 要素がconststr型の可変長配列 */
varia a8 string[]   /* 要素がstring  型の可変長配列 */

上記ではすべて要素数0の配列の実体が確保されます(varia文において配列の初期値を省略した場合、そのデフォルト値は [ ] となります)。

要素数0なのにその実体が確保されたというは少し妙な話に聞こえるかもしれませんが、 Rarakuの配列ではその要素そのものの領域の他に、それに付随する管理領域のようなものも存在していて、 後者だけがとりあえず確保されたのだというイメージで構いません。

固定長配列では型で指定された自然数分の要素数が確保されましたが、可変長ではこれが0となる点に注意してください。

比較のため、例も挙げておきます。

varia a1 int[3] = [] /* 要素数が3の配列が実体として確保される */
varia a5 int[]  = [] /* 要素数が0の配列が実体として確保される */

この例でのvaria文では敢えて [ ] で初期化していますが、これがなくとも結果は同じです。 このように並べて比較すると、固定長(int[3])の場合、初期化子 [ ] の中身は空なので要素数0が確保されるように錯覚するかもしれませんが、 実際にはint[3]という型の情報が優先され、要素数3の配列が確保されます。

確保された要素数が0ということは、ary[0]といったアクセスですら認められません(ランタイムエラーとなります)。 ただし可変長配列の場合、この要素数を実行時に動的に変更することができます(一方で固定長配列の場合は、このような変更はできません)。 つまりまずは要素数を増やし、その上で各要素にアクセスすればよいことになります。

可変長配列で要素数を増やすにはRrk_resizeWithInitVal関数を使います。 第1引数に対象の配列を、第2引数に新しく変更したい要素数(uint型)をそれぞれ指定します。 例えば以下の通りです。

varia ary int[]

Rrk_resizeWithInitVal( ary, 3, 0 ) /* 要素数を3に変更、要素数が増える場合は増加分の要素は0で初期化 */
ary[0] = 10
ary[1] = 20

Rrk_print( "ary[0]=" ary[0] "\n" ) /* ary[0]の値は10 */
Rrk_print( "ary[1]=" ary[1] "\n" ) /* ary[1]の値は20 */
Rrk_print( "ary[2]=" ary[2] "\n" ) /* ary[2]の値は0  */

Rrk_resizeWithInitValの第1引数は特殊なものとなっていて(正確にはジェネリクスと呼びますが)、 通常の可変長配列であればいかなる型の変数/定数を与えても構いません。 ただし固定長配列を与えた場合はコンパイルエラーとなります。 さらにその第3引数も特殊なものとなっており、第1引数で与えた配列の要素型に相当する変数/定数を与えることができます。

この関数の戻り値はbool型となります。 戻り値をチェックする必要は通常ありませんが、この関数が成功した場合はtrue、 (引数にnull値を与えるなどして)失敗した場合はfalseを返します。

組み込み関数Rrk_numofによって、今現在の配列のサイズを得ることができます。 この関数の引数として配列を与えると、戻り値として配列のサイズを返します。 例えば以下の通りです。

varia v_ary int[]
varia size uint

size = Rrk_numof( v_ary ) /* sizeは0 */
Rrk_print( \=size \n )    /* sizeの値が0となっていることを確認  */

Rrk_resizeWithInitVal( v_ary, 3, 0 )    /* 要素数を3に変更 */
size = Rrk_numof( v_ary ) /* sizeは3 */
Rrk_print( \=size \n )    /* sizeの値が3となっていることを確認  */

varia v_ary_nullable int[]^? = null  /* v_ary_nullable に nullを代入 */
size = Rrk_numof( v_ary_nullable )   /* sizeは0 */
Rrk_print( \=size \n )               /* sizeの値が0となっていることを確認  */

Rrk_numof の引数は特殊なものとなっていて(正確にはジェネリクスと呼びますが)、配列であればいかなる型の変数/定数を与えても構いません。 またその戻り値はuint型となります。 また値がnullとなっている配列をこの関数に与えた場合、ランタイムエラーとはならず単に 0 を返します。
上の例では v_ary は可変長配列ですので宣言時の初期要素数は 0 であり、Rrk_numof( v_ary ) も 0 を返します。 次に Rrk_resizeWithInitVal( v_ary, 3, 0 )で要素数を3に変更しますと、Rrk_numof( v_ary ) も 3 を返します。

Rrk_numof の引数には固定長配列を与えることもできます。 例えば以下の通りです。

varia f_ary int[3]
varia size uint

size = Rrk_numof( f_ary ) /* sizeは3 */
Rrk_print( \=size \n )    /* sizeの値が3となっていることを確認  */

f_ary = null              /* f_ary に nullを代入 */
size = Rrk_numof( f_ary ) /* sizeは0 */
Rrk_print( \=size \n )    /* sizeの値が0となっていることを確認  */

上の例では f_ary は固定長配列であり、int[3] で宣言されていますので初期要素数は 3 であり、Rrk_numof( v_ary ) も 3 を返します。 ただし値がnullとなっている場合は、f_ary が固定長配列であっても Rrk_numof( f_ary ) は 0 を返します

可変長配列においても、初期化子[...]を用いて配列を初期化したり、代入することができます。 例えば以下のようになります。

varia ary int[] = [ 10, 20, 30, ] /* 初期化子により初期化 : 要素数3の配列が確保される */
varia size uint

size = Rrk_numof(ary) /* sizeは3 */

ary = [ 100, 200, 300, 400, ] /* 初期化子により代入 : 要素数4の配列が確保され、古い配列は破棄される */
size = Rrk_numof(ary) /* sizeは4 */

ary = [ 1000, 2000, ] /* 初期化子により代入 : 要素数2の配列が確保され、古い配列は破棄される */
size = Rrk_numof(ary) /* sizeは2 */

固定長配列の時とは異なり、可変長では初期化子に指定した値の個数分だけの要素数が、その配列の実体として確保されます。

配列をスタックのようにして使いたい場合、組み込み関数Rrk_push_bkが便利です。 この関数によって、配列の後ろに新しい要素を追加(プッシュ)できます。 例えば以下のようにして使用します。

varia a1 int[]
varia size uint

Rrk_push_bk( a1, 10 ) /* 10を一番後ろに新規追加 */
Rrk_push_bk( a1, 20 ) /* 20を一番後ろに新規追加 */
Rrk_push_bk( a1, 30 ) /* 30を一番後ろに新規追加 */

size = Rrk_numof(a1) /* sizeは3 */

Rrk_print( "a1[0]=" a1[0] "\n" ) /* a1[0]の値は10 */
Rrk_print( "a1[1]=" a1[1] "\n" ) /* a1[1]の値は20 */
Rrk_print( "a1[2]=" a1[2] "\n" ) /* a1[2]の値は30 */

また逆に組み込み関数Rrk_pop_bkによって、配列の後ろの要素を削除(ポップ)できます。 例えば以下のようにして使用します。

varia a1 int[]
varia size uint

Rrk_push_bk( a1, 10 ) /* 10を一番後ろに新規追加 */
Rrk_push_bk( a1, 20 ) /* 20を一番後ろに新規追加 */
Rrk_push_bk( a1, 30 ) /* 30を一番後ろに新規追加 */

size = Rrk_numof(a1) /* sizeは3 */

Rrk_pop_bk( a1 ) /* 一番後ろ30を削除 */
Rrk_pop_bk( a1 ) /* 一番後ろ20を削除 */
Rrk_pop_bk( a1 ) /* 一番後ろ10を削除 */

size = Rrk_numof(a1) /* sizeは0 */

Rrk_push_bk/Rrk_pop_bkの第1引数は特殊なものとなっていて(正確にはジェネリクスと呼びますが)、可変長配列であればいかなる型の変数/定数を与えても構いません (ただし固定長配列を与えた場合はコンパイルエラーとなります)。 また値がnullとなっている配列をこの関数に与えた場合、何もせずfalseを返します。 さらにRrk_push_bkについては、その第2引数も特殊なものとなっており、第1引数で与えた配列の要素型に相当する変数/定数を与えることができます。

戻り値はbool型となります。 戻り値をチェックする必要は通常ありませんが、この関数が成功した場合はtrue、 (引数にnull値を与えるなどして)失敗した場合はfalseを返します。

さて、ここまで固定長配列と可変長配列を説明してきましたが、 可変長配列の方が柔軟であり、わざわざ固定長配列を使う意味はあるのかと思われるかもしれません。 固定長配列の使いどころは、例えば次元が固定されたベクトルやそれを処理する行列など、 配列の要素数が絶対に変更されることがあってはならないようなデータ構造に限定されるかと思います。

後の項目(配列のアサイナビリティ)で詳しく説明しますが、固定長配列と可変長配列では(一部の例外的な状況を除き)一般に相互に代入は許可されません。 これは固定長配列の要素数が(可変長配列とみなされるなどして)うっかり変更されてしまうようなプログラミングミスを防止するためです。

const配列


これまでvaria文により配列を宣言する例を見てきました。 次にconst文で宣言した配列(以下これをconst配列と呼びます)について説明します。

const文ですので宣言時の初期化は必須となります。 また const配列の場合、その配列自体への通常の代入は勿論、その要素への代入も禁じられます。 例えば以下は要素への代入なのでコンパイルエラーとなります。

const c3 int[] = [ 10, 20, 30, ] /* const配列 : 初期化は必須. c3自体の値はこれ以降変更不可 */

c3[1] = 99 /* コンパイルエラー : const配列の要素に値を代入しようとしている */

ただし、単に要素の中身をリードオンリーで参照するだけなら勿論可能です。

const c3 int[] = [ 10, 20, 30, ] /* const配列 */

Rrk_print( "c3[1]=" c3[1] \n ) /* OK. 値は20 */

const配列ではさらに要素数の変更も禁じられます。 要素数を変更するような関数(Rrk_resizeWithInitVal、Rrk_push_bk、Rrk_pop_bkなど)にconst配列を指定することはできません。 例えば以下はconst配列においてRrk_push_bkを呼び出しているのでコンパイルエラーとなります。

const c3 int[] = [ 10, 20, 30, ] /* const配列 */

Rrk_push_bk( c3, 99 ) /* コンパイルエラー : const配列の要素数を変更しようとしている */

上記はすべてconstで宣言された可変長配列(要素数を明示的に指定していない)ですが、これを固定長配列で宣言する意味はあまりありません。 なぜなら宣言時の右辺値によりその要素数は自動的に決まり、しかもconst文で宣言しているその時点で、その要素数は以降ずっと固定だからです。 const配列の場合は普通は可変長配列にする(constなのに可変長という言葉も変ですが)と考えてもらって差し支えないと思います。

あまり使いどころを思いつきませんが、一応const文で固定長配列を宣言することも可能ではあります。

const zero_vector3D int[3] = [] /* const配列: この場合、要素は 0, 0, 0 で初期化される */

上記のように初期化子を伴ってしかも残りの要素の指定を省略したい場合、便利なのかもしれません。 また、宣言時の右辺が初期化子ではなく他の配列変数/定数であり、 その右辺の要素数が指定した要素数と相性の悪いものである場合、コンパイルエラーにできます。 これについてはすぐ後の項目(配列のアサイナビリティ)で詳しく述べます。

配列型のアサイナビリティ(varia文だけの場合)


配列の代入では、代入先と代入元の配列型の相性を考えなければならない場合があります。 すなわち「代入先の型」は、「代入元の型」を受け入れられるような型でなければなりません。 わざわざこのような言い回しをするのは、双方の配列型が完全に同じな場合は勿論、そうでなくても代入可能なケースがあるからです。 これを配列のアサイナビリティ(assignability)と呼びます。

日本語訳では「代入可能性」や「譲渡可能性」と訳されることもあるようですが、 いずれにせよあまり一般的な用語でない上、これだとひょっとするとちょっとニュアンスの違う意味にとられるような気もするので、 この記事ではGo言語のドキュメントでも使用されている「アサイナビリティ」という用語を使うことにします。

とりあえずconst配列はまだ考えず、varia文で宣言した配列のみを考えます。 まず大前提として、代入する場合はこれらの配列の要素の型が一致していなければなりません。 例えば以下の例では、int型配列をreal型配列へ代入しようとしてるため、コンパイルエラーとなります。

varia i_ary int[] = [ 10, 11, 12, ]
varia r_ary real[]

r_ary = i_ary /* コンパイルエラー: 要素の型が異なる(int型配列からreal型配列へ) */

配列の代入ではディープコピーは行われません。 代入先には新たな配列の要素は確保されず、代入元が元々確保していた領域を参照するだけになります。 一方、代入先が元々確保していた旧領域は(もうどれからも参照されていないならば)破棄されます。

これを確認するため、例えば以下のようにia1をia2に代入後、ia2の要素を変更したとします。 この場合、(一見すると要素を変更していない)ia1の要素も変更されたように見えることになります。 これは ia1, ia2双方とも同一の実体を参照しているためです。

varia ia1 int[] = [ 10, 11, 12, ]
varia ia2 int[] = [ 20, 21, 22, ]

/***
 * この代入により、ia1, ia2 双方が同一の実体 [ 10, 11, 12 ]を参照することになる.
 */
ia2 = ia1

ia2[1] = 99
Rrk_print( "ia2[1]=" ia2[1] \n ) /* 勿論99と表示される */
Rrk_print( "ia1[1]=" ia1[1] \n ) /* こちらも99と表示される */

固定長配列と可変長配列では(一部の例外的な状況を除き)一般に相互に代入は許可されません。 例えば以下の通りです。

/* 固定長配列と可変長配列が混在する場合 */
varia f1 int[3]
varia f2 int[3]
varia v1 int[]
varia v2 int[]

f2 = v1 /* コンパイルエラー: 代入先が固定長配列、代入元が可変長配列 */
v2 = f1 /* コンパイルエラー: 代入先が可変長配列、代入元が固定長配列 */

上記では 固定長配列 f2 へ可変長配列 v1 を代入しようとしていますがこれはコンパイルエラーとなります。 また逆に可変長配列 v2 へ固定長配列 f1 を代入しようとしていますがこれもコンパイルエラーとなります。

一方、固定長配列同士の代入や、可変長配列同士の代入では(一部の例外的な状況を除き)一般に相互の代入が可能です。 例えば以下の通りです。

/* 固定長配列と可変長配列が混在する場合 */
varia f1 int[3]
varia f2 int[3]
varia v1 int[]
varia v2 int[]

f2 = f1 /* OK: 代入先/代入元が両方とも固定長配列(しかも要素数まで同じ) */
v2 = v1 /* OK: 代入先/代入元が両方とも可変長配列 */

ただし固定長配列同士であってもそれらの要素数が異なる場合、代入が許可される場合と許可されない場合があります。 このとき代入先の要素数は代入元の要素数以下でなければなりません。 例えば以下の通りです。

/* すべて固定長だが要素数が異なる場合 */
varia f2 int[2]
varia g2 int[2]
varia f3 int[3]
varia f4 int[4]

g2 = f2 /* OK: 代入先/代入元双方が固定長配列 ( 要素数が同じ) */
f2 = f3 /* OK: 代入先/代入元双方が固定長配列 ( 代入先の要素数(2) <= 代入元の要素数(3) ) */
f4 = f3 /* NG: 代入先/代入元双方が固定長配列 ( 代入先の要素数(4) >  代入元の要素数(3) ) */

上記の例で f2 と g2 は要素数も同じなのでこれらは相互に代入が可能です。 f2 と f3 は要素数が異なりますが、代入先である f2 の要素数(2)が代入元である f3 の要素数(3)以下であるため、この代入は認められます。 一方、f4 と f3 は、代入先である f4 の要素数(4)の方が代入元である f3 の要素数(3)より大きいため、この代入はコンパイルエラーとなります。

配列型のアサイナビリティ(const文も含む場合)


次に「variaで宣言した配列」と「constで宣言した配列」が両方ある場合を考えます。 まず全て可変長配列である場合、「constで宣言した配列」へ「variaで宣言した配列」を代入が可能です。 (constの場合、代入は初期化時限定ですが)。 ただしその逆は認められません。 例えば以下の通りです。

varia v_v1 int[]
const c_v1 int[] = v_v1 /* OK: variaからconstへ(どちらも可変長配列) */

const c_v2 int[] = []
varia v_v2 int[] = c_v2 /* コンパイルエラー: constからvariaへ(どちらも可変長配列) */

一般に可変長配列と固定長配列では相互に代入できないことを既に述べました。 しかし特例として、代入先がconstで宣言した可変長配列である場合、代入元として固定長配列を指定することができます。 例えば以下の通りです。

const c_f1 int[3]
const c_v1 int[] = c_f1 /* OK: 左辺値がconst可変長配列 */

varia v_f2 int[3]
const c_v2 int[] = v_f2 /* OK: 左辺値がconst可変長配列 */

つまりこの意味ではconstの可変長配列は割と万能なアサイナビリティを持ちます。 varia文との違いに注意しましょう。 varia文で宣言した可変長配列の場合はこれは認められません。 例えば以下はコンパイルエラーです。

varia v_f int[3]
varia v_v int[] = v_f /* コンパイルエラー: 左辺値がvaria可変長配列 */

これがコンパイルエラーになる理由はvariaでは要素数変更の可能性を認めてしまうからです。

すぐ後で述べますがRarakuではimmutやtightで宣言された可変長配列というものもあります。 そして代入先がimmutやtightで宣言した可変長配列である場合、constの時と同様に代入元として固定長配列を指定することができます。 ただし代入先がimmutの場合は、代入元はimmutかtightかvariaでなければなりません。 また代入先がtightの場合は、代入元はtightかvariaでなければなりません。

この特例について少しまとめますと、一般に可変長配列と固定長配列では相互に代入できませんが、 代入先がconstまたはimmutまたはtightで宣言した可変長配列である場合に限り、 代入元として固定長配列を指定することができる場合があるということです。 尚、代入先が固定長であり、代入元が可変長の場合はこのような特例は存在せず、すべてのケースで代入は禁止されます。

配列型のアサイナビリティ(関数の引数型への指定)


まずこれまでに学んだ、可変長配列における「初期化」と「再代入」のすべての組み合わせとその可否をまとめて表にしたものを以下に示します。
  • 初期化の場合

  • 左辺値右辺値OK/NG
    varia varia OK
    varia const NG
    const varia OK
    const const OK


  • 再代入の場合

  • 左辺値右辺値OK/NG
    varia varia OK
    varia const NG
    const varia NG
    const const NG
特に「再代入」の場合、左辺値がconstであれば問答無用でNGとなることに注意してください。

尚、これは配列の場合の規則であって、int型などではまた規則が変わってきます。 int型の場合、「初期化」についてはすべての組み合わせでOKであり、 また「再代入」については左辺値がvariaの場合は全てOK、左辺値がconstの場合は全てNGとなります。

さて、関数の引数に配列を与える場合、(一部の例外を除き)基本的にはvaria/const文での初期化と同じアサイナビリティの規則が成り立ちます。 すなわち仮引数(parameter)を初期化時における左辺式、実引数(argument)を初期化時における右辺式と考えて判断します。

「再代入と同じ」ではなく「初期化と同じ」です。 この二つは規則が違うので注意しましょう。
関数の仮引数がstring型の場合、それにconststr型の実引数を直接与えることはできません。 これについては例外的に初期化とは異なるアサイナビリティになっています。

また仮引数の型にモディファイアを何も付けなかった場合、それはデフォルトでconstになります。

まずは実引数が可変長配列である場合を見ましょう。 また仮引数の配列にvariaモディファイアが付加された場合を考えます。 例えば以下の例をご覧ください。

function func( varia p int[] ){
	/* この関数内で要素の値や要素数が変更される可能性がある */
}
varia a int[] /* 可変長配列 */
func( a ) /* OK: 仮引数は(非constな)可変長配列であり、実引数も(非constな)可変長配列である */

上記の例では仮引数 p は可変長配列であり、実引数 a もまた可変長配列です。 この組み合わせの指定は問題ありません。

実引数が固定長配列である場合も見てみます。 例えば以下の通りです。

function func( varia v int[] ){
	/* この関数内で要素の値や要素数が変更される可能性がある */
}
varia f int[3] /* 固定長配列 */
func( f ) /* コンパイルエラー: 仮引数は(非constな)可変長配列なのに実引数に固定長配列を与えている */

上記の例では仮引数 v は(非constな)可変長配列であるにも関わらず、実引数 f は固定長配列です。 この場合、これまで見てきた通常の代入規則と同様、コンパイルエラーとなります。

関数func内で v の要素数が変化する可能性がありますから、仮にこれを認めると f の要素数まで変化してしまう恐れがあります。

仮引数の配列にconstモディファイアが付加された場合(あるいは全く同じことですがモディファイアを何も付加しなかった場合)、 その関数内で配列の要素の値や要素数の変更を禁止できます。 例えば以下の例をご覧ください。

function func( const v int[] ){
	/* この関数内で要素の値や要素数が変更禁止 */
}
varia f int[3]
func( f ) /* OK: 仮引数はconst可変長配列であるので、実引数に固定長配列を与えることは許可される */

上記の例では仮引数 v はconst可変長配列であり、実引数 f は固定長配列です。 この指定は許可されます。

関数func内で v の要素数が変化する恐れはないので、この場合 f の要素数が変化する恐れもありません。

仮引数の配列にconstモディファイアを付加すれば(あるいはモディファイアを何も付加しなければ)、 固定長配列を実引数として渡すことができ、配列の要素数の変更と要素の値の変更の両方を関数内で禁止できることを見ました。 しかし場合によってはconstモディファイアでは制限が強すぎることがあります。 例えば要素数の変更のみ禁じ、要素の値の変更は許可したい場合どうすればよいでしょうか?

仮引数の配列にtightモディファイアを付加すると、このようなことが可能になります。 constモディファイアではすべてが禁止されたわけですが、 tightモディファイアではすべて禁止とまではいかなくとも、制限が少しきつい、すなわち要素数の変更のみを禁止する形になります。 例えば以下の例をご覧ください。

function initVector3D( tight v int[] ){
	/* この関数内では、配列 v の要素数の変更は禁止、要素の値の変更は許可 */

	Rrk_push_bk( v, 9 ) /* コンパイルエラー: 配列 v の要素数の変更 */

	v[0] = 8 /* OK: 配列 v の要素の値の変更 */
}
varia f int[3]
initVector3D( f ) /* OK: 仮引数はtight可変長配列であるので、実引数に固定長配列を与えることは許可される */

上記では、仮引数 v にtightモディファイアが指定されています。 またこの場合においても、実引数側において固定長配列(上記では f )を指定することができます。 この v の要素数を変更するような処理(例えばRrk_push_bkなどの関数呼び出し)は禁止されますが、 v の要素の値を変更する(例えば単なる代入)は許可されます。

constモディファイアを指定した場合と同じく、 関数func内で v の要素数が変化する恐れはないので、この場合 f の要素数が変化する恐れもありません。
その他、immutモディファイアというものもあります。 これは配列の要素がint型の場合は、上記のtightモディファイアの例と同じく、 v の要素数を変更するような処理(例えばRrk_push_bkなどの関数呼び出し)は禁止されますが、 v の要素の値を変更する(例えば単なる代入)は許可されます。 配列の要素がint型ではなく配列型や(後のセクションで述べる)構造体型の場合、 immutモディファイアとtightモディファイアの違いが現れますが、このセクションではこれ以上深入りしないことにします。

ここまですべて仮引数が可変長配列でしたが、念のためこれが固定長配列である場合も見ておきます。 例えば以下の例をご覧ください。

function func2D( p int[2] ){
}
varia a int[3] /* 固定長配列 */
func2D( a ) /* OK: 双方とも固定長配列であり、しかも実引数の要素数(3) >= 仮引数の要素数(2) */

上記の例では仮引数と実引数はどちらも固定長配列です。 さらに要素数の関係性も問題なく、この指定は問題ありません。

次の例は仮引数は同じく固定長配列ですが、実引数が可変長配列になっています。

function func2D( p int[2] ){
}
varia a int[] /* 可変長配列 */
func2D( a ) /* コンパイルエラー: 仮引数は確実な要素数として2を要求する。しかし実引数は要素数不定な可変長配列 */

このような場合は許可されません。 上記の場合、例えば要素数が1の配列が実引数として指定されるかもしれませんが、 その場合関数内で(p[1]といったアクセスを行うと)問題が発生する恐れがあるからです。

固定長配列とした時点で要素数の変更禁止の意味も含まれます。 従って仮引数の固定長配列にimmutやtightモディファイアを付加したとしてもそれはあまり意味を持ちません (非constにはなりますから(関数内における配列要素への)代入は可能になるといった意味合いだけが付与されます)。

その他、(配列ではない)単なる整数や文字列にimmutやtightモディファイアをつけたとしても、 それは通常、variaモディファイアと同じとみなすことができます。

配列型のアサイナビリティ(関数の戻り値型への指定)


関数の戻り値に配列を与える場合も全く同様のアサイナビリティの規則が成り立ちます。 すなわち戻り値の型を左辺値、return文で指定する側を右辺値と考えた初期化の規則として判断します。 例えば以下の例をご覧ください。

function func() int[3] {
	varia a int[]
	return a /* コンパイルエラー: 可変長配列から固定長配列のパターン */
}

上記の例ではreturn文に可変長配列を指定しており、戻り値の型は固定長配列です。 この場合、関係性に問題がありコンパイルエラーとなります。

仮引数の場合とは異なり、戻り値の型にモディファイアを何も付けなかった場合、それはデフォルトでvariaになることに注意しましょう。

これは配列等(あるいは後のセクションで述べる構造体もそうですが)を作って戻り値として返す関数を定義するといった、 かなりよくある状況に配慮したものです (このような「何か」を最初に生成する関数を「ファクトリ関数」と呼ぶこともあります)。 例えば以下をご覧下さい。

function makeIntAry( size uint ) int[] {
	varia a int[] = []
	Rrk_resizeWithInitVal( a, size, 0 )
	/* something process... */
	return a
}
varia vary = makeIntAry( 3 )
vary[0] = 10

const cary = makeIntAry( 3 )

上記の例のような場合、makeIntAryで受け取ったvaryの要素へさらに再代入したり、vary自体の要素数の変更をすることも普通にあると考えられます。 実際、上記の例でもそのためにvaryをvaria文で受け取っています。 仮に、関数の戻り値の型のデフォルトをconstにしてしまうと、このような関数を定義する度に (関数の戻り値の型に)variaを明示的に記述しなくてはならなくなり、さすがに煩わしくなります。

一方、makeIntAryで受け取ったcaryの要素を読み込み専用にしたければ、上記のようにcaryをconst文で受け取ることもできます。 これは関数の定義側の戻り値の型がvaria指定されていたとしても可能であり、つまり受け手側でいくらでも柔軟に対応できるということです。

勿論、makeIntAryの戻り値を絶対にconst文で受け取るように(受け手側に)強制したい場合もそれなりにあるとは思います(すぐ後でそのような例を見ます)。 例えば受け手側が配列の値を変更する手段を完全に封じたいが、一方でその値を読み込み専用で参照することだけは許可したいといった状況です。 しかし経験上、makeIntAryのようなファクトリ関数の登場の頻度と比べると、そのような関数が必要になる頻度は少ないのではないかと思います。 そのため、戻り値についてはvariaの方を優先してそちらをデフォルトとしたわけです。

戻り値の型にconstモディファイアが付いている場合の例も見ておきます。 例えば以下の通りです。

function getIntAry() const int[] {
	varia a int[]
	return a /* OK */
}
const b int[] = getIntAry() /* OK */

上記の例ではreturn文に可変長配列、戻り値の型はconst可変長配列で、ここは問題ありません。 また、呼び出し側においてgetIntAry()の値をconst可変長配列の初期値として代入していますが、これも問題ありません。

戻り値の型にtightモディファイアが付いている場合の例も見ておきます。 例えば以下の通りです。

function getIntAry() tight int[] {
	varia a int[3]
	return a /* OK */
}
varia b int[] = getIntAry() /* コンパイルエラー: */

上記の例ではreturn文に固定長配列、戻り値の型はtight可変長配列で、ここは問題ありません。 しかし、呼び出し側においてgetIntAry()の値をvaria可変長配列の初期値として受け取っていますが、 これはコンパイルエラーとなります。 varia可変長配列の初期値としてではなく、const可変長配列の初期値として受け取れば問題ありません。 例えば以下の通りです。

function getIntAry() tight int[] {
	varia a int[3]
	return a /* OK */
}
const b = getIntAry() /* OK */

上記の例ではconst文の型指定を省略していますが、これは型推論により自動的にint[]と判断されます。

配列型のアサイナビリティ(まとめ)


配列の初期化時/関数への引数指定時/関数のreturn文への値指定時のアサイナビリティについて、これまでの内容を模式的にまとめると次のようになります (allとある部分は任意のモディファイアになります)。

  • 両方とも可変長の場合

  • 左辺値右辺値OK/NG
    const dst[] all src[] OK
    tight dst[] tight/varia src[] OK
    tight dst[] const src[] NG
    varia dst[] varia src[] OK
    varia dst[] const/tight src[] NG


  • 片方が可変長、もう片方が固定長の場合

  • 左辺値右辺値OK/NG
    const dst[] all src[3] OK(特例)
    tight dst[] tight/varia src[3]OK(特例)
    tight dst[] const src[3] NG
    varia dst[] all src[3] NG
    all dst[3] all src[] NG


  • 両方とも固定長の場合

  • 左辺値右辺値OK/NG
    const dst[2]all src[3] OK
    tight dst[2]tight/varia src[3]OK
    tight dst[2]const src[3]NG
    varia dst[2]varia src[3]OK
    varia dst[2]const/tight src[3]NG
    all dst[4] all src[3] NG


  • その他(右辺値がnullな場合)

  • 左辺値右辺値OK/NG
    all dst[] null NG
    all dst[3] null NG
    all dst[]^? null OK
    all dst[3]^?null OK

  • 関数の引数に配列を与える場合
  • 仮引数(parameter)を左辺値、実引数(argument)を右辺値と考えて判断します。

  • 関数の戻り値に配列を与える場合
  • 戻り値の型を左辺値、return文で指定する側を右辺値と考えて判断します。

  • 再代入の場合
  • 上記で左辺値がconst/tightの場合はすべてNGに読み替えてください。

擬似定数と完全定数


これまで見てきたようにconst配列はその要素の値の変更や要素数の変更を禁止します。 ただしconst配列はあくまで宣言された配列の識別子を介した変更を禁じるものであることに注意しましょう。 例えば以下をご覧ください。

varia v3 int[] = [ 10, 20, 30, ] /* varia配列 */
const c3 int[] = v3              /* const配列(v3で初期化) */

v3[0] = 100 /* c3[0]の値も100になる */

上記でc3はconst配列として宣言していますが、その初期値はv3でありこれはvaria配列です。 つまりc3が指し示している実体はv3です。 これで確かにc3を介した値の変更は禁止されますが、v3を介した変更は依然として許可されるため、 上記の例ではc3[0]の値も変わっています。 このようにconst型の右辺をvaria型で初期化するような場合は注意が必要です。

一方、以下のような指定では、c3の値はもはや決して変わることはありません。

const c3 int[] = [ 10, 20, 30, ] /* const配列(完全なリテラルで初期化) */

上記ではconst配列に完全なリテラルを指定しており、これは(内部の要素も含め)c3以外のどれからも参照されていません。

このようにconst文で宣言された定数において、(その定数以外の)他のどの識別子からもその初期値が参照されていない場合、 その定数をRarakuでは完全定数と呼びます。 またそれ以外のケース、つまりその初期値が他の変数からも参照されているような定数を擬似定数と呼びます。

ただしこれは一般的な用語ではありません。 Rarakuでは定数は完全定数と擬似定数の二つに分類されると考えます。 ほとんどのケースでは擬似定数でも特に問題ないと思いますが、 これを明確に区別する必要がある文脈において、完全定数と擬似定数という用語を使うことにします。

ではvaria配列を初期値に指定し、かつそのconst配列を完全定数にしたい場合どうすればよいでしょうか? その場合、初期値としてvaria配列の完全なクローンを与えなければなりません。 例えば以下の通りです。

varia v3 int[] = [ 10, 20, 30, ] /* varia配列 */
const c3 int[] = [ v3[0], v3[1], v3[2] ] /* const配列(v3の完全なクローンとして初期化しているので完全定数) */

v3[0] = 100 /* c3[0]の値は10のまま */

上記でのc3は、v3と全く同じ値を持ちますが実体の異なる配列を新たに作り、それを初期値として与えている形になります。 このような場合、const配列c3は完全定数となり、v3の変更の影響を受けません。

上記の例では状況をわかりやすく強調するため、初期化子の中に逐一v3の要素を直接指定していますが、 実際のコーディングではRrk_clone_ary関数を使う方がよいでしょう。 例えば以下のようになります。

varia v3 int[] = [ 10, 20, 30, ] /* varia配列 */
const c3 int[] = Rrk_clone_ary( v3 ) /* const配列(v3の完全なクローンとして初期化しているので完全定数) */

v3[0] = 100 /* c3[0]の値は10のまま */

Rrk_clone_aryの引数は配列ならばいかなる引数を指定してもかまいません。 ただし後で述べる多次元配列の場合、一番外側の配列のみがコピーされる形となり、 この関数では完全なディープコピーとはなりません。

似たような状況は、const配列以外にconststr型文字列でも起こります。 例えば以下をご覧ください。

import std/str
varia s string   = "hello"
varia c conststr = s /* sで初期化 */

RrkStr_set( s, "world" ) /* cの値も"world"になる */

上記でcが指し示している実体はsです。 また識別子 c を介した文字列の値の変更はconststr型であるため確かに禁止されますが、 sを介した値の変更は依然として許可されるため、結果的に c の値が変化します。

上記はvaria文で宣言していますが、constで宣言しても同じです。 例えば以下の通りです。

import std/str
const s string   = "hello"
const c conststr = s /* sで初期化 */

RrkStr_set( s, "world" ) /* cの値も"world"になる */

この場合のconstは文字列変数自体への代入を禁じるのみの働きになります。 つまり「s = 他の文字列」や「c = 他の文字列」といった文の記述は確かに禁止されますが、 s はあくまで string 型であるため、s の内部の値を変える関数の呼び出しはこの場合でも許可されます。 sを介した値の変更は依然として許可されるため、結果的に c の値が変化します。

このような文字列 c を完全定数にしたい場合、配列の場合と同様、初期値としてstringの完全なクローンを与えなければなりません。 文字列のクローンを作るにはRrkStr_clone関数を使います。 例えば以下の通りです。

import std/str
const s string   = "hello"
const c conststr = RrkStr_clone( s ) /* sの完全なクローンとして初期化しているので完全定数 */

RrkStr_set( s, "world" ) /* cの値は"hello"のまま */

上記でのcは、sと全く同じ値を持ちますが実体の異なる文字列を新たに作り、それを初期値として与えている形になります。 このような場合、文字列cは完全定数となり、sの変更の影響を受けません。

一方、(配列や文字列ではなく)単に整数型や実数型、bool型をconst文で宣言する場合は、初期値が何であろうと常に完全定数となります。 例えば以下の通りです。

varia v = 1
const c = v /* vで初期化. int型であるため完全定数となる */

v = 2 /* cはvとは独立した完全定数である. 従ってcの値は1のまま */

array文と多次元配列


Rarakuではarray文を使うことにより、一つの配列型に別名をつけることができます(C言語におけるtypedefに相当します)。 これは「array 別名 配列型」という書式になります。

一度別名をつけると、それ以降のvaria/const文においてその別名を型名として使うことで配列を宣言できます。 以下の例をご覧ください。

array IntAry int[3]
varia a IntAry
varia b int[3]
a = b /* OK */
b = a /* OK */

上記の例ではIntAryがint[3]の別名となります。 またvaria文によりIntAry型の変数 a が宣言されていますが、この a は結局 int[3] 型の配列になります。 一方、varia文によりint[3]型の変数 b が宣言されていますが、この b と a とは互換性があります。 即ち a を b に代入したり、逆に b を a に代入することも可能です。

varia/const文における配列の宣言では、varia a int[2][3] といったように [ ] を二つ続けて記述することはできません。 しかしarray文の別名と組み合わせることにより、多次元配列を宣言することが可能です。 この時、宣言された多次元配列に単にアクセスする場合に限り a[i][j] のように [ ] を二つ続けて記述します。 例えば以下のようになります。

array AryInt3 int[3]
varia ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
] 
Rrk_print( "ary2d[0][0]=" ary2d[0][0] "\n" ) /* 1 */
Rrk_print( "ary2d[0][1]=" ary2d[0][1] "\n" ) /* 2 */
Rrk_print( "ary2d[0][2]=" ary2d[0][2] "\n" ) /* 3 */
Rrk_print( "ary2d[1][0]=" ary2d[1][0] "\n" ) /* 4 */
Rrk_print( "ary2d[1][1]=" ary2d[1][1] "\n" ) /* 5 */
Rrk_print( "ary2d[1][2]=" ary2d[1][2] "\n" ) /* 6 */

上記の例ではAryInt3を要素の型とするサイズ2の配列 ary2d を宣言しています。 要素の型も配列なので多次元配列となるわけです。 また初期化子の中に配列の初期化子を記述する必要がありますから、上記のようにこれをネストして記述します。

要素にアクセスする場合はary2d[i][j]という表記で書きますが、この場合最初のiの方が0から1まで、 2番目のjの方が0から2までの範囲になります。

このような繰り返し処理は、(特別な理由がない限り)通常は専用の制御文を使います。 例えばfor文をネストして書けば上の記述はもっとすっきり書くこともできます。 for文については後の「for文」のセクションで詳しく説明します。
このような多次元配列を用いなくても、一次元配列を工夫して利用すれば同様のアクセスは可能ではあります。 例えば替わりに以下のような一次元配列を用意した場合、上の例での「ary2d[i][j]」という表記は 「pseudo_ary2d[ 3*i + j ]」で代用できます。

varia pseudo_ary2d int[3*2] = [
	1, 2, 3,
	4, 5, 6,
] 
varia i,j int
pseudo_ary2d[ 3*i + j ]


多次元配列のアサイナビリティ


多次元配列でconst/tightを指定した場合、その一番外側の配列は勿論、内側の要素の配列にまでその効果が及びます。 例えば以下をご覧ください。

array AryInt3 int[3]
const ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
] 
ary2d[0] = [ 7, 8, 9 ] /* コンパイルエラー: ary2dの要素ary2d[0]もconst */
ary2d[0][0] = 10       /* コンパイルエラー: ary2d[0]の要素ary2d[0][0]もconst */

上記でary2dはconstで宣言されているため、その要素もまたconstとなり、 結果的にその要素の要素もまたconst、といったようにconstの効果が再帰的に及び、 結局コンパイルエラーとなります。

tightの場合も全く同様で、例えば以下の通りです。

array AryInt3 int[3]
tight ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
] 
Rrk_push_bk( ary2d[0],  7 ) /* コンパイルエラー: ary2dの要素ary2d[0]もtight */

上記でary2dはtightで宣言されているため、その要素もまたtightとなります。 つまり多次元配列でのtightはすべての次元の要素数が固定された状況であると言うことです。

配列とモディファイアのまとめ


実体が確保された状態であれば、要素数の範囲内で配列の各要素にアクセスすることができます。 要素数がNの配列の場合、配列の最終要素のインデックスは、その要素数から1を引いたN-1となることに注意しましょう。 N以上の値をインデックスとして与えるとランタイムエラーとなります。

配列の初期化子を使って配列の初期化や代入を行うことができます。 初期化子は「[」で始まり、「]」で終わる範囲に、要素をカンマで区切って指定します。 初期化子の最後の要素の直後にあるカンマはあってもなくても構いません。

通常は可変長配列を使うのが便利です。 可変長配列の型は例えばint[ ] というように要素数が空となっており、デフォルトの要素数は0です。 組み込み関数Rrk_resizeWithInitVal, Rrk_push_bk, Rrk_pop_bk 等によって要素数の変更、追加、削除が行えます。 組み込み関数Rrk_numofによって、今現在の配列のサイズを得ることができます。

配列をconstで宣言するとその配列自身への代入は勿論、その要素の値の変更が禁止され、 また要素数の変更も禁止されます。 配列をtightで宣言すると要素数の変更のみが禁止され、 配列自身への代入やその要素の値の変更は許可されます。

配列を他の配列へ代入する場合はアサイナビリティに注意しましょう。 一般に制限の弱い型から強い型へ代入ができます。 例えばvaria配列からtight配列への代入が可能ですし、 tight配列からconst配列への代入が可能です。 これに逆行することはできません。

Rarakuでは多次元配列はarray文を使い、一旦別名をつけてから再度配列を宣言する形で実現できます。



目次に戻る

while文とdowhile文

基本事項


プログラミングにおいて、ある特定の条件が成立する限り同じ処理を繰り返したい場合があります。 このような処理をループと呼び、ループを実現するにはループ文を使います。 while文はループ文の一種で「while 条件式 ブロック」という書式になります。 これは条件式が true である限り、ブロック内の実行を繰り返すといったものになります。

例えば以下の例をご覧ください。

varia idx uint = 4

while idx > 0 {
	Rrk_print( "idx=[" idx "]\n" )
	--idx;
}
Rrk_print( "after while.\n" )

上記の例では、idxの初期値は4で、ブロック内を一度実行するたびに「--idx」が実行され値が1つずつ減っていきます。 条件式「idx > 0」がtrueとなるのは idxの値が4,3,2,1の4回分ですので、ブロック内が実行される回数も4回となります。 この結果、Rrk_print も idx の値を減じながら4回実行されるのでコンソール上では以下のように表示されるはずです。

idx=[4]
idx=[3]
idx=[2]
idx=[1]
after while.

Rarakuのwhile文はC言語のwhile文と異なり、ブロックにおける「{」と「}」を省略することはできません。 一方、条件式を囲う「(」と「)」は記述する必要がありません。 (この括弧を記述しても一応構いませんが、それは結局条件式として括弧つきのものを与えていることになります)。

参考: while文の条件式に整数型を与えた場合
一般にbool型変数に整数型の値を代入することはできませんが、 while文(並びに後述するdowhile文)の条件式では、例外的に整数型を指定できます (これはif文の条件式の指定の場合と同様です)。 例えば以下の例をご覧ください。

varia idx uint = 4
while idx {
	Rrk_print( "idx=[" idx "]\n" )
	--idx;
}

ここでidxはbool型ではなく整数型ですが、while文の条件式に単独で整数型を与えた場合、これが特別な条件式として機能します。 この場合、idxが非ゼロであれば true、ゼロであれば false となる条件式とみなされます。 つまり上記は以下のように書いたのと全く同じです。

varia idx int = 4
while idx != 0 {
	Rrk_print( "idx=[" idx "]\n" )
	--idx;
}

この例の場合でも結局while文のブロック内は4回実行されることになります。

整数型以外にも構造体型やnatvobj型を指定することもでき、その場合も全く同様に機能します。 構造体型については「構造体」のセクションで、 natvobj型については「ファイル入出力とネイティブオブジェクト型」のセクションで詳しく述べます。

Close


break文


while文を終了するには通常は、whileキーワードの直後に指定した条件式がfalseにならなければなりません。 しかしその条件式が false になる前の段階でなんらかの特別な状況が発生し、while内のループを途中で強制終了させたい場合があります。 そのような場合にbreak文を使います(break文を実行することを「(ループ文から)breakする」と言う場合があります)。

break文はループ文におけるブロック内でのみ記述できます。 それ以外の場所でこの文を記述した場合はコンパイルエラーとなります。

C言語と異なり、Rarakuではswitch文にbreak文は存在しません。

例えば以下の例をご覧ください。

varia idx uint = 8

while idx > 0 {
	Rrk_print( "idx=[" idx "]\n" )
	if idx == 5 {
		break
	}
	--idx;
}
Rrk_print( "after while.\n" )

上記の例では、idxの初期値は8で、ブロック内を一度実行するたびに「--idx」が実行され値が1つずつ減っていきます。 条件式「idx > 0」がtrueとなるのは idxの値が8,7,6,5,4,3,2,1の8回分ですので、本来ならばブロック内が実行される回数は8回となるはずです。 しかし、whileブロックの内部にif文があり、「idx == 5」の場合において break文が実行されるようになっています。 この結果、idx の値が 5 になった段階でループが強制終了し、コンソール上では以下のように表示されるはずです。

idx=[8]
idx=[7]
idx=[6]
idx=[5]
after while.

continue文


while文におけるループでは、通常はブロック内の開始から終わりまでの一通りの実行を1回分とし、次の回へ進みます。 しかしある特別な状況下では、ブロック内の終わりに至る前に強制的に次の回へ進みたい場合があります。 そのような場合にcontinue文を使います。(continue文を実行することを「(ループ文内で)continueする」と言う場合があります)。

continue文はループ文におけるブロック内でのみ記述できます。 それ以外の場所でこの文を記述した場合はコンパイルエラーとなります。

例えば以下の例をご覧ください。

varia idx uint = 8

while idx > 0 {
	if idx >= 5 {
		--idx;
		continue
	}
	Rrk_print( "idx=[" idx "]\n" )
	--idx;
}
Rrk_print( "after while.\n" )

上記の例では、idxの初期値は8で、ブロック内を一度実行するたびに「--idx」が実行され値が1つずつ減っていきます。 しかし、whileブロックの内部にif文があり、「idx >= 5」が成り立つ間は continue文が実行されるようになっています。 この結果、idx の値が 5 以上の間はRrk_printが実行されず、コンソール上では以下のように表示されるはずです。

idx=[4]
idx=[3]
idx=[2]
idx=[1]
after while.

dowhile文


dowhile文もループ文の一種で「dowhile ブロック while 条件式」または「dowhile ブロック while( 条件式 )」という書式になります。

C言語をご存知の方は、これは要するに「do-while文」です。 キーワードは「do」ではなく「dowhile」となっていることに注意してください。

なぜこんな変なキーワードになっているのかと言うと「do」という文字列(しかも一応動詞)を変数名や関数名で使用可能にしたかったためです。 ただ、それを言うなら他のキーワードについても同様のことが言えてキリがないわけですが、 さすがに一般的に制御文として使用頻度が高いものについてはそのままキーワードとしてあります。 一方「do」は例えば「if」などと比べれば使用頻度が格段に低いためこのように冗長な名前にしても問題ないと判断しました。

というわけで、ほとんど「どー」でもいい理由でした(「do」だけに)。

whileという文字列が開始と終わりにどちらも含まれているのがやや気持ち悪いと感じられるかもしれません。 気休め程度ではありますがそうならないように一応最後の「while」を「:」と書くこともできます(余計気持ち悪いかもしれませんが…)。

while文ではまずブロック内の開始時に条件式の判定を行いました。 dowhile文では逆にブロック内の終了時に条件式の判定を行うことができます。 このとき最初の1回目は無条件でブロックが実行されます。

例えば以下の例をご覧ください。

varia idx uint = 4

dowhile {
	Rrk_print( "idx=[" idx "]\n" )
	--idx;
}: idx > 0
Rrk_print( "after while.\n" )

上記の例では、idxの初期値は4で、まず無条件にdowhileブロック内の1回目は実行されます。 ブロックの最後に条件式「idx > 0」があり、これが毎回評価されます。 最初の1回目では、この条件式でのidxはデクリメントされていますから 3 となっているはずです。 これ以降「--idx」が実行され値が1つずつ減っていきますが、最後の回の実行が完了した時点で「idx > 0」の値は false となり、 このときの idx は 0 です。 結局 idx の値は 3, 2, 1, 0 と遷移しますから Rrk_print は4回実行され、コンソール上では以下のように表示されるはずです。

idx=[4]
idx=[3]
idx=[2]
idx=[1]
after while.

Rarakuのdowhile文はC言語のdo-while文と異なり、ブロックにおける「{」と「}」を省略することはできません。

一方、最後のwhile(または「:」)の直後にある条件式を囲う括弧は省略できます。

dowhile文のブロック内においても break文、continue文を使うことができ、while文の場合と同様に機能します。

無限ループ


while文やdowhile文の条件式に true や 1 といった定数を指定すると、ブロック内でbreak文を実行しない限りループ処理が永遠に続くことになります。 これを無限ループと呼びます。

例えば以下をご覧ください。

varia idx = 0

while true {
	++idx;
	if idx >= 10 {
		break
	}
	Rrk_print( "idx=[" idx "]\n" )
}

この例ではwhileの条件式に true を指定していますので自然には終了しないwhile文となります。 一応「idx >= 10」が成立する段階で break 文を実行して終了するようにしていますが、 仮にこれがない場合は無限ループとなります。

プログラムをコンソール上で実行中に無限ループに陥ってしまった場合、 Ctrl+Cを入力することでプログラムを強制終了できます。

while文とdowhile文のまとめ


プログラミングにおいて、ある特定の条件下で同じ処理を繰り返したい場合はループ文を使いましょう。 ループ文にはいくつかありますが、このセクションではそのうち中でwhile文とdowhile文を紹介しました。 いずれも条件式はwhileの後ろに指定します。

ループ文の内部で break文を使うことで途中で強制的にそのループを終了できます。 またcontinue文を使うことで、強制的に次の周回へ進むこともできます。

無限ループを使う場合には注意しましょう。 通常そこから脱出する何らかの手段が提供されている必要があります。



目次に戻る

for文

for文の基本


for文もループ文の一種で、基本的には「for 初期化文 ; 条件式 ; 後処理文 ブロック」という書式になります。

ただしfor文の場合、括弧を付けて「for( 初期化式 ; 条件式 ; 後処理文 )ブロック」と書いても構いません。 意味は括弧がない場合と全く同じです。

for文ではまずすべての実行に先立って実行させるべき初期化文の指定ができます(これは省略することも可能です)。 典型的には、ここでループのインデックスとなる整数変数に 0 などを代入します。

2番目に while 文と同様、ループ継続を判定するための条件式がきます(これは省略することも可能です)。 典型的には、ここでループのインデックスとなる整数変数がループの最大回数に至ったかどうかの判定などを行います。

3番目にブロック内の処理が一通り終了した時に実行させるべき後処理文の指定ができます(これは省略することも可能です)。 典型的には、ここでループのインデックスとなる整数変数をインクリメントします。

例えば以下の例をご覧ください。

varia idx uint

for idx=0; idx<4; ++idx {
	Rrk_print( "idx=[" idx "]\n" )
}

上記の例では、まず for 文のすべての実行に先立って、初期化文「idx=0」が実行されます。 次に条件式「idx<4」を評価し、ブロック内の処理を実行すべきか否かを判定します。 今回の場合、idxの値はまだ0ですのでこの条件式は true となり、ブロック内の処理が実行されます。 ブロック内の実行が一通り終わったら、後処理文「++idx」が実行されます。 ここまでがループの1回目です。

後処理文内の「++idx」では後ろに「;」をつけてはいけません。 ずっと前のセクションで「Rarakuのインクリメント・デクリメントが単独の文である場合、その文末の「;」が必須である」と述べましたが、 このfor文の後処理文内における「++idx」だけは例外的に後ろに「;」が不要となります。

次に2回目以降は初期化文「idx=0」の実行をスキップします。 条件式「idx<4」を再度評価し、ブロック内の処理を実行すべきか否かを判定します。 idxの値は1になっていますのでこの条件式は true となり、ブロック内の処理が実行されます。 ブロック内の実行が一通り終わったら、後処理文「++idx」が実行されます。 ここまでがループの2回目です。

以降同様に実行していきます。

4回目のループの後処理文の実行直後、条件式「idx<4」を再度評価し、ブロック内の処理を実行すべきか否かを判定しますが idxは4になっていますのでこの条件式は false となり、ここでブロック内の処理を実行することなくfor文を終了します (このとき後処理文も実行されませんので idx は4のままブロックから抜けます)。

結局、ブロック内での idx の値は 0, 1, 2, 3 と遷移しますから Rrk_print は4回実行され、コンソール上では以下のように表示されるはずです。

idx=[0]
idx=[1]
idx=[2]
idx=[3]

参考: C言語のfor文との違い
この「参考」はC言語をご存知の方のみお読みください。

Rarakuのfor文はC言語のfor文と異なり、ブロックにおける「{」と「}」を省略することはできません。 一方、「初期化文 ; 条件式 ; 後処理文」を囲う「(」と「)」は省略できます。 ただしこの括弧を省略せず書くことも問題なくできます(単に括弧の有無の違いで、意味は変わりません)。 例えば以下の通りです。

varia idx uint

for( idx=0; idx<4; ++idx ){
	Rrk_print( "idx=[" idx "]\n" )
}

C言語のソースコードからRarakuのコードへ移植する場合等で、 この括弧の除去は面倒なのでとりあえず付けたままにしておくようなこともできます (そもそもこのように括弧付きの表記も認める仕様にしたのはその状況を想定したためです)。

Close


Go言語のように「:=」を使って、for文の前にあるidxの宣言「varia idx uint」を省略できる記法も存在しますが、それについては後ほど述べます。 尚、この場合でも「初期化文 ; 条件式 ; 後処理文」を囲う「(」と「)」は付けても構いませんし、省略しても構いません。

for文におけるbreak文とcontinue文


for文のブロック内においても break文、continue文を使うことができます。 これは基本的には while文と同様に機能しますが、若干の補足事項もあります。

break文では、これを使ってfor文のループを途中で強制終了する(breakする)ことができます。 ただし、break文を実行した直後に後処理文が実行されることはなく、直ちにfor文のブロック外へ脱出します。

以下の例をご覧ください。

varia idx uint
for idx=0; idx<4; ++idx {
	if idx == 2 {
		 break
	}
}
Rrk_print( "after for : idx=[" idx "].\n" )

上の例では idx の値が2のときにfor文からbreakしていますが、このとき後処理文にあたる「++idx」は実行されません。 よってfor文から抜け出た直後のidxは2となります。 結果は以下のように表示されるでしょう。

after for : idx=[2].

一方 continue文では、これを使ってfor文のブロックの途中でループの次の回へ強制的に進める(continueする)ことができます。 ただしcontinue文では、これを実行した直後に後処理文が実行されます

以下の例をご覧ください。

varia idx uint

for idx=0; idx<4; ++idx {
	if idx >= 2 {
		 continue
	}
}
Rrk_print( "after for : idx=[" idx "].\n" )

上の例では idx の値が2以上になるとcontinueしていますが、このとき後処理文にあたる「++idx」も実行されています。 よって idx の値はやがて 4 になり、「idx<4」の判定が false となってfor文は自然に終了します。 結果は以下のように表示されるでしょう。

after for : idx=[4].

初期化文、条件式、後処理文の省略


for文における初期化文、条件式、後処理文はいずれも省略可能で、その場合特別な挙動をします。

まず初期化文を省略した場合を見てみます。 以下をご覧ください。

varia idx = 3

for ; idx<4; ++idx {
	Rrk_print( "idx=[" idx "]\n" )
}

上の例では初期化文が何も指定されていません(ただしその場合でも初期化文の後ろにある「;」だけは必ず記述する必要があります)。 よって idx に特に何も施すことなくいきなり条件式「idx<4」を評価します。 idxの値は元々3ですので、この条件式はtrueになります。 実行結果は以下のようになるでしょう。

idx=[3]

次に条件式を省略した場合を見てみます。 以下をご覧ください。

varia idx uint

for idx=0; ; ++idx {
	Rrk_print( "idx=[" idx "]\n" )
	if idx >= 4 {
		break
	}
}

上の例では条件式が何も指定されていません(ただしその場合でも条件式の後ろにある「;」だけは必ず記述する必要があります)。 今回は初期化文はありますので、まず for 文のすべての実行に先立って、初期化文「idx=0」が実行されます。 次に条件式が何も指定されていませんが、この場合条件式としてあたかも「true」が指定されているかのような挙動になります。 つまりブロック内の処理は常に実行すべきと判定されます。 後処理文は「++idx」と指定されていますので、各回のブロックの実行終了後にインクリメントは行われます。

一応この例では、ブロック内で「idx >= 4」が成立した時点で break していますが、 仮にこれがない場合は無限ループとなります。

次に後処理文を省略した場合を見てみます。 以下をご覧ください。

varia idx uint

for idx=0; idx<4; {
	Rrk_print( "idx=[" idx "]\n" );
	idx = 4
}

上の例では後処理文が何も指定されていません(ただしその場合でも条件式の後ろにある「;」だけは必ず記述する必要があります)。 今回は初期化文はありますので、まず for 文のすべての実行に先立って、初期化文「idx=0」が実行されます。 次に条件式「idx<4」があり、ブロック内の処理は実行すべきと判定されます。 今回は後処理文は何も指定されていませんので、各回のブロックの実行終了後にインクリメントは行われません。

一応この例では、ブロック内で「idx = 4」が実行され、条件式「idx<4」が false となるようにしてありますが、 仮にこれがない場合、idxはずっと0のままですから条件式「idx<4」は常に成立し続け、無限ループとなります。

最後に初期化文、条件式、後処理文を3つとも省略した場合を見てみます。 以下をご覧ください。

varia idx int = 0

for ;; {
	Rrk_print( "idx=[" idx "]\n" )
	if idx >= 4 {
		break
	}
	++idx;
}

上の例では初期化文、条件式、後処理文の全てが指定されていません(ただしその場合でもそれぞれを区切る「;」だけは必ず記述する必要があります)。 この場合、初期化文や後処理文の実行も一切ありません。 また条件式が何も指定されていませんので条件式としてあたかも「true」が指定されているかのような挙動になります。 これは(細かいことを考えなければ)while文において条件式にtrueを指定したのと同じ状態と考えて結構です。

厳密に言えば、for文で条件式を省略した記述では、Raraku仮想マシンの無条件ジャンプ命令を使って無限ループを実現しますので 条件式として本当にtrueが指定されている状態ではありませんが、上記では説明のわかりやすさのため、そのように記述しています。

無条件ジャンプ命令の方が(条件式trueの評価が発生しない分)ほんのわずかに効率のよいRaraku仮想マシンのコードを出力することにはなりますが、 これはほとんど誤差の範囲です。 プログラミングではどちらを使って記述しても特に問題はないでしょう。

初期化文でインデックスの宣言も同時に行う


ここまでの例ではfor文に入る手前でvaria文によりidxを宣言していました。 しかしfor文は非常に頻繁に使う類の文でありますから、これを使う度にこのvaria文を毎回宣言するのも面倒です。 またそもそもこのidxは大抵の場合、for文だけで使う補助的な変数ですから、そのスコープもfor文の中だけで有効であるべきでしょう (もちろんそうはいかないケースもありますが)。 スコープを限定したいだけならば以下のように記述することもできますが、ますます面倒ですし余計なインデントまで入ってしまいます。

{
	varia idx uint
	for idx=0; idx<4; ++idx {
		Rrk_print( "idx=[" idx "]\n" )
	}
}

大抵のプログラミング言語のfor文では上記と実質的に同じことを実現する簡易記法が存在します。 これは idx の宣言をfor文の「初期化文」の部分で同時に行うというものです。 Rarakuでもそれは可能で、「初期化文」で「:=」演算子を使うことによってそれが実現できます(これはGo言語の記法に似ています)。 例えば以下のようになります。

for idx:=0; idx<4; ++idx {
	Rrk_print( "idx=[" idx "]\n" )
}

これは「idx:=0」の部分で「varia idx int=0」が実行されていると考えることもできます。 型は指定されていませんが「:=」より右側の値から自動的に型推論されます(今回の場合intです)。 またidxのスコープはfor文のブロック内だけとなります(ブロックを終了した後は無効となります)。 この記法は事前にvaira文によるidxの宣言が不要になるため、ほとんどの場合で便利です。 このチュートリアルでも以降は特別な理由がない限りこの記法を使用することにします。

int型ではなくuint型として型推論して欲しい場合は以下のようにキャスト演算子「->」を用います。

for idx:=0->uint; idx<4; ++idx {
	const u uint = idx
	Rrk_print( "u=[" u "]\n" )
}

あるいは、以下のようにuint型リテラル「0u」等で指定することもできます。

for idx:=0u; idx<4; ++idx {
	const u uint = idx
	Rrk_print( "u=[" u "]\n" )
}

上記の例でidxをuint型としましたが、条件式「idx<4」において「4」は依然としてint型リテラルのままであり、 ここにuint型とint型の比較が発生してしまいます。 これを回避するため、この「4」を「4u」などとして双方を明示的にuint型に揃えてもよいですが、 このケースではそこまでしなくとも「4」が非負であることが表面的に明らかであるため、特に問題ないでしょう。

しかし実際の用途では、このケースのようにマジックナンバー(数字の直書き)で指定するのではなく、何らかの変数/定数を与えることの方が多いでしょう。 そしてその場合、中身の値は(表面的には)わかりませんので問題になることがあります。 そのような場合、その変数/定数の型もidxの型と極力合わせるべきです。

idxがuint型であるにも関わらずこれを負の数と比較した場合、恐らくプログラマが予期しない結果になります。 例えば(あまりない状況だと思いますが)以下のような状況を考えます (注意:このプログラムは(無限ループとまではなりませんが)結果的に 18446744073709551615 回もループすることになりますので実際に動作させない方がよいでしょう)。

for idx:=0->uint; idx<-1; ++idx {
 	const u uint = idx
	Rrk_print( "u=[" u "]\n" )
}

上記では条件式は「idx<-1」ですのでこれがtrueになることはなく即座にループを終了するように見えるかもしれません。 確かにidxがint型であればそうなのですが、今回はidxはuint型となっています。 この場合、「idx<-1」の「-1」は暗黙のうちに uint 型に変換され、 負の数のアンダーフローにより、uint型での値 18446744073709551615 に変換されます。 つまりこの「idx<-1」は「idx<18446744073709551615」と同じになるということです。

idxがuint型の場合、これは必ず0以上です。そして「idx<-1」の「-1」が本当に文字通り「-1」の意味なら そもそもこのように書く意義自体がありません。そこから逆算して、この「-1」はuint型での値 18446744073709551615 のつもりで書いたのではないかと 予測することはできるかもしれません。 しかしそれでも「-1」だけからこのような結果になるというのはかなりわかりにくいでしょう。 せめて明示的に「(-1)->uint」のように記述されていた方がまだマシかもしれません。

このような状況は本来コンパイルエラーとしたいところですが(go言語などではそのような仕様になっていますが)、 そのためには比較演算子において、片方をint型、もう一方をuint型といったように指定した場合は問答無用でコンパイルエラーとなる仕様にする必要があります (それでも比較したい場合はどちらか一方の型を他方の型へキャストする必要があるものとします)。

しかし現状のRarakuではこれはデフォルトではコンパイルエラーとなりません(純粋なC言語などではそのような仕様(Warningは出るかもしれませんが)になっていますが、それに合わせてあります)。 これは実際の場面では、int型とuint型を直接比較したい状況(尚且つ、上記のような問題の恐れがない状況)はそれなりに多くあり、 その度にその部分を全部キャストをするのも煩わしいと考えるためです。

あまり考えなくキャストを連発して適当に取り繕ったとしても、完全な正確性が担保されるとは限りません。 キャストを行った側になんらかの情報の欠落が生じてしまうからです。 万全を期してこれを可能な限り正確に行いたいのであれば、 まずint型のオペランドの正負を判定してからキャストをするか否かを切り分ける必要があるでしょう。 例えば以下のようになります。

function cmpIntGtUInt( i int, u uint ) bool { return i >= 0 ? i->uint > u : false }
function cmpIntLtUInt( i int, u uint ) bool { return i >= 0 ? i->uint < u : true  }
function cmpUIntGtInt( u uint, i int ) bool { return cmpIntLtUInt( i, u ) }
function cmpUIntLtInt( i uint, i int ) bool { return cmpIntGtUInt( i, u ) }

const size int=-1
for idx:=0u; cmpUIntLtInt( idx, size ); ++idx {
	const u uint = idx
	Rrk_print( "u=[" u "]\n" )
}

ただ実際は、すべてのケースでこれを徹底してしまうとかえってコードが煩雑になる恐れもありますし、 ここまで徹底しなくても適当なキャストで実質的に十分な場合も多いでしょう。 そして適当にキャストするなら結局(それをやってもやらなくても)安全性に大差ないのではとも思えるわけです。

しかし一方、このようなケースを厳しくチェックしたい状況もあります。 そこで現状のRarakuではpragma文でこのチェックを有効にできるオプションがあります。 そのためにはソースコードの始めの方で次のように記述します。

pragma is_strict_cmp_int_uint:true

この指定を行った場合、Rarakuは(それが指定されたrrksファイルに限ってですが)比較演算子における二つのオペランドがそれぞれint型とuint型であった場合、 問答無用でコンパイルエラーを出します。この指定下ではプログラマはint型uint型の区別を厳しく管理しつつプログラミングを行うことが要求されるでしょう。

ただしこの指定を行った場合でも、比較演算子の一方のオペランドが非負のint型リテラル(「-」符号を伴わないint型リテラル)であり、 もう一方がuint型である場合はコンパイルエラーとなりません。 その場合は(非負のint型リテラルの方をuint型とみなせばよく)明らかに問題ないからです。

一方で、両方のオペランドが識別子であったり、一方のオペランドが負のint型リテラル (これは「-」符号を伴うint型リテラルであり、そのままint型であるとみなされます)で もう一方がuint型であるような場合はコンパイルエラーとなります。

for文の応用例


例えば配列のすべての要素を出力表示したいとしましょう。 for文(やその他のループ文)を使わない場合、以下のように全要素について直接記述しなければなりません。

varia iary int[4] = [
	0, 10, 20, 30,
]

Rrk_print( "iary[0]=[" iary[0] "]\n" )
Rrk_print( "iary[1]=[" iary[1] "]\n" )
Rrk_print( "iary[2]=[" iary[2] "]\n" )
Rrk_print( "iary[3]=[" iary[3] "]\n" )

これは配列の要素数が少ない場合ならまだなんとかなりますが、要素数が多い場合は大変な手間になります。 さらに配列の要素数が不変なもの(例えば2次元ベクトルや3次元ベクトルを配列で実現した場合など)であれば問題ありませんが、 将来の修正で要素数が変わり得るものの場合、配列そのものの修正に連動して、 それへアクセスする文を追加/削除する手間が発生してしまいます。

そこでこれを以下のようにfor文を使って書き直します。

varia iary int[4] = [
	0, 10, 20, 30,
]

for idx:=0u; idx<Rrk_arynum(iary); ++idx {
	Rrk_print( "iary[" idx "]=[" iary[idx] "]\n" )
}

配列の要素を指し示すためのインデックスが必要になるため、整数型変数idxを宣言する必要がありますが、 ここでは「:=」演算子を使い、forの初期化文においてこれを行っています (初期値として「0u」を指定していますのでこのidxはuint型です)。

for文では配列の要素数に依存しない抽象化が可能になる効果もあります。 例えば「Rrk_arynum(iary)」では配列の要素数(uint型)を自動的に取得できます。 for文ブロック内では「iary[idx]」という記述で要素のアクセスを(要素数に寄らず)抽象化しています。

これにより、将来の修正で要素数が変わっても(配列の各要素値の初期化部分の修正などは仕方ないにしても)、 少なくともそれへアクセスする部分を修正する必要はなくなります。

次に多次元配列へアクセスする例についても見てみます。 以下をご覧ください。

array AryInt3 int[3]
varia ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
] 
for i:=0u; i < Rrk_arynum(ary2d); ++i {
	for j:=0u; j < Rrk_arynum(ary2d[0]); ++j {
		Rrk_print( "ary2d[" i "][" j "]=" ary2d[i][j] "\n" )
	}
}

上記の例ではAryInt3を要素の型とするサイズ2の配列 ary2d を宣言しています (「配列」のセクションで取り挙げた多次元配列の例とまったく同じです)。

今回は二つのfor文をネストしており、外側がiに関するfor文、内側がjに関するfor文です。 ary2d[i][j]とあった場合、インデックスjの方が内側のループとなることに注意してください。 またRrk_arynum(ary2d) の値はこの場合 2 となり、Rrk_arynum(ary2d[0])の値がこの場合 3 となります。

これの実行結果は以下のようになります。

ary2d[0][0]=1
ary2d[0][1]=2
ary2d[0][2]=3
ary2d[1][0]=4
ary2d[1][1]=5
ary2d[1][2]=6

尚、今回のように単純にfor文を2重にネストさせ、尚且つ両for文の間に他に何も処理が挟まれないのであれば、 敢えてインデントのルールから一部逸脱し、以下のように二つのfor文の列の位置を合わせた方が見やすいかもしれません (あくまで好みの問題ですが)。

array AryInt3 int[3]
varia ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
] 
for i:=0u; i < Rrk_arynum(ary2d);    ++i {
for j:=0u; j < Rrk_arynum(ary2d[0]); ++j {
	Rrk_print( "ary2d[" i "][" j "]=" ary2d[i][j] "\n" )
}}



for文のまとめ


for文はwhile文と同じくループ文の一種ですが、idx変数を伴う記述をforキーワードとforブロックの間に凝縮して記述することができます。 これによりidx変数のスコープをforブロック内に限定したり、 forブロック実行後やcontinue文実行時のインクリメントを自動的に行わせたりすることができます。

実のところ実際のプログラミングではfor文よりも、次のセクションで述べるforeach文を使用することの方が多いと思われますが、 for文を理解することはforeach文の挙動を理解するための基本ともなります。



目次に戻る

foreach文

foreach文の基本


foreachは配列のすべての要素にアクセスするための特殊なループ文です。 このようなことはfor文を使用してももちろん可能ですが、for文の場合インデックス用に整数変数を別途宣言し、 それを使いながらアクセスする必要がありました。

foreach文を使うとこの手間を少し軽減することができます。 foreach文は「foreach 要素参照用変数 : 配列指定部 ブロック」という書式になります (あるいは括弧を付けて、「foreach ( 要素参照用変数 : 配列指定部 ) ブロック」と記述しても構いません)。

大元の配列を「配列指定部」の部分に指定し、0番目の要素から一つずつ「要素参照用変数」で宣言した変数に格納し、 ブロック内でそれを使うといった形になります。配列の最終要素での実行が終わるとforeach文自体を終了します。

foreachはforとeachを合体させたようなキーワードになっています。 他の言語でも登場するキーワードですが「フォーイーチ」と発音することが多いと思います。

例を見た方がわかりやすいかもしれません。 以下をご覧ください。

varia ary int[] = [
	10, 20, 30, 40,
]
varia sum int = 0
foreach e : ary {
	Rrk_print( "e=" e "\n" )
	sum += e
}
Rrk_print( "sum=" sum "\n" )

この例は、配列の要素の総和を計算するものです。 まず始めに、配列aryの0番目の要素(この例では10)を変数 e に格納します。

変数 e はforeach文の外で予め宣言しておく必要はありません。 この e はforeach文のブロック内で宣言された変数とみなされます。 従ってそのスコープはこのブロック内のみとなり、このブロックを抜けた後は e は無効になります。

ブロック内ではこの e (値は10)を用いて「sum += e」を実行します。 これでループの1回目は終了です。

ループ2回目においては、配列aryの1番目の要素(この例では20)を変数 e に格納します。 ブロック内ではこの e (値は20)を用いて「sum += e」を実行します。

以下同様に処理され、最後の要素(この例では40)についても同様に処理されたら その時点でforeach文は終了です。

最終的に sum には 10 + 20 + 30 + 40 = 100 が格納されているはずです。 この例の実行結果は以下のようになります。

e=10
e=20
e=30
e=40
sum=100

Rarakuのforeach文では、ブロックにおける「{」と「}」を省略することはできません。

foreach文のブロック内においても break文とcontinue文を使うことができます。 その働きはwhile文におけるbreakとcontinueと同様です。

foreach文の「:」の後ろの「配列指定部」の指定において、以下のように配列の初期化子などを直接指定した書き方はできません。

foreach e : [ 10, 20, 30, 40, ] { /* NG: コンパイルエラー */
	Rrk_print( "e=" e "\n" )
}

これは単独の初期化子だけからではそれが何型の配列なのか、あるいはそのモディファイアは何なのかがはっきりしないからです (この例の場合ならまだ型はなんとなくint[]であるような気もしますが、モディファイアははっきりしないでしょう)。 初期化子を変数宣言の右辺や関数の引数に与える場合であれば、その代入先からそのような情報を取得することができましたが、 foreachの場合それもできません。

他の言語のforeach文の説明等ではなぜかこのような書き方がよく取り上げられるのでここでも取り上げましたが、 そもそもこのように初期化子をforeach文の中に直に書くこと自体、個人的にはあまりメリットはないと思います (予め名前付き配列変数に一旦格納し、それを指定した方が多分見やすいでしょう)。 しかし一応、標準の関数Rrk_asを使えば、以下のような形で似たようなことはできます。

foreach e : Rrk_as^< int[], const >( [ 10, 20, 30, 40, ] ) {
	Rrk_print( "e=" e "\n" )
}

Rrk_asはジェネリクス関数と呼ばれるものでここでは簡単な説明に留めますが、 関数名Rrk_asとその引数を指定する括弧の間に「^<」と「>」で囲まれた記述があり、 この部分には型名(上記ではint[])やモディファイア(上記ではconst)などを指定します。 これにより、関数の引数に与える初期化子の情報を明示化することができます。 Rrk_asで行われることは引数で与えた初期化子をそのまま返すだけです(ただし明示的な型とモディファイアが付加された一時変数の状態で返します)。

特にRrk_asで指定するモディファイアがvariaである場合、代わりにRrk_asvを使うこともできます。 Rrk_asvのモディファイアは常にvariaであるため、モディファイアを明示的に指定する必要がなく、その分短く書けます。

/* foreach e : Rrk_as^< int[], varia >( [ 10, 20, 30, 40, ] ) と指定したのと同じ */
foreach e : Rrk_asv^< int[] >( [ 10, 20, 30, 40, ] ) {
	Rrk_print( "e=" e "\n" )
}


foreach文による要素の値の更新


foreach文における要素参照用変数の値を変更すると、それはそのまま配列本体における該当要素の値が変更されることになります。 例えば以下の通りです。

varia ary int[3] = [ 1, 2, 3 ]
foreach e : ary {
	e *= 10 /* 各要素を10倍 */
}
Rrk_print( "ary[0]=" ary[0] "\n" )
Rrk_print( "ary[1]=" ary[1] "\n" )
Rrk_print( "ary[2]=" ary[2] "\n" )

上記ではforeach内部において、要素参照用変数を10倍していますが、それにより元の配列本体(ary)の各要素もすべて10倍されることになります。 これの実行結果は次のようになります。

ary[0]=10
ary[1]=20
ary[2]=30

ただし foreach に与える配列が const 型である場合は、要素参照用変数の値の変更も禁止されます。

const ary int[3] = [ 1, 2, 3 ]
foreach e : ary {
	e *= 10 /* コンパイルエラー : ary は const型であり、その要素 e は変更不可である */
}

この場合、foreach内のブロックで e の値を変更しようとすると、以下のようなコンパイルエラーが表示されます。

  You are trying to assign to the const type.
  const : [e]


foreach文でのconst指定


constではない配列を foreach 文に与えた場合、通常ではその要素参照用変数の値は変更可能であるわけですが、 要素参照用変数の前にconstを付けることで、この値の変更を一時的に禁止できます。 例えば以下の通りです。

varia ary int[3] = [ 1, 2, 3 ]
foreach const e : ary {
	e *= 10 /* コンパイルエラー : e は const であるのに値を書き換えようとしている. */
}

上記で e の前に const が付加されていること、さらには指定した配列 ary は元々 const ではないことに注意してください。 この場合、foreach内のブロックで e の値を変更しようとすると、 ary 自体が const である場合と同様、以下のようなコンパイルエラーが狙い通り表示されます。

  You are trying to assign to the const type.
  const : [e]

foreach 内において e を変更する必要がない場合、const を付ける方が望ましいでしょう。 これにより要素参照用変数 e の値を誤って変更するようなプログラミングミスを防止できます。

ただし、この const はあくまで要素参照用変数 e に対してのみ作用するものです。 ary の要素に e ではなく、インデックス指定によって直接アクセスした場合、その変更を禁止することはできません。 例えば以下の通りです。

varia ary int[3] = [ 1, 2, 3 ]
foreach const e : ary {
	ary[0] *= 10 /* e を介した変更ではない。このような値の変更は禁止できない */
}

上記のような場合でも要素の値の変更を確実に禁止したい場合、やはり ary 自体が const配列である必要があります。 ary が元からconst文で宣言されている場合なら問題ないのですが、varia文で宣言されている場合どうすればよいでしょうか?

foreach文では配列指定部の前にも const を付けることができ、これによりそのような状況に対応できます。 例えば以下の通りです。

varia ary int[3] = [ 1, 2, 3 ]
foreach e : const ary { /* ary に対してconstモディファイア */
	ary[0] *= 10 /* コンパイルエラー : aryはこのブロック内で一時的にconstになっている */
}
ary[0] *= 10 /* ここはOK */

上記のforeach文では要素参照用変数 e ではなく、配列変数 ary の前に const を付けていることに注意してください。 ここで指定された配列変数 ary はvaria文で宣言されたものですが、 この foreach文のブロック内限定で一時的に const であるとみなされます。 従って、このブロック内で「ary[0]」の値を変更するような処理は禁止され、 上記の例は狙い通りコンパイルエラーとなります。

const を付加できるのは、配列指定部の部分に単なる変数/定数の名前が指定された場合に限られます。 この部分が複雑な式である場合、この前に const を付加することはできません。

「複雑な式」とはこの部分が配列の要素、三項演算子、(後のセクションで述べる)構造体のメンバなどの場合であり、 例えば以下のような指定はコンパイルエラーとなります。

array IAry int[]
varia iary_ary IAry[] = [ [0], [1] ]

foreach const e : iary_ary[0] { /* OK */
}
foreach const e : const iary_ary[0] { /* コンパイルエラー: iary_ary[0] に constを付加することはできない */
}

上記では2次元配列iary_aryの要素「iary_ary[0]」(この要素自体も配列)を指定しています。 4行目ではこれを普通に指定していますので何の問題もありませんが、 一方で6行目ではこれにconstを付加しています。 ところがこの「iary_ary[0]」は「複雑な式」に該当するため、ここでのconstの付加はできないことになります。

参考: final文
詳しい説明はここでは割愛しますが、後のセクションで登場する final 文でも foreach文の内部だけ一時的に ary 自体を const にするといったようなことが可能です。

varia ary int[3] = [ 1, 2, 3 ]
foreach const e : ary {
	final ary /* このブロック内のみ ary は const 化する */
	ary[0] *= 10 /* コンパイルエラー : このブロック内配下のみ ary はあたかも constのように扱われる */
}
/* ブロックの外では元に戻る(constでなくなる)*/

final文については「final文」のセクションで詳しく説明します。

Close


foreach文でのtight指定


一つ前の項目では foreach 文にconstを指定する例を見ましたが、 constの替わりにtightを指定することもでき、 またその書式もconstの場合と全く同様です。 例えば以下の通りです。

array IAry int[]
varia ary_of_ary IAry[] = [ 
	[ 1, 2, 3 ],
	[ 10, 20, 30 ],
]
foreach tight ary : ary_of_ary {
	Rrk_push_bk( ary, 9 ) /* コンパイルエラー: aryはtight指定されている */
}

上記は多重配列になっているため若干複雑な例になっていますが、 要はforeach文の要素参照用変数 ary (ary_of_aryの要素)が明示的に tight と指定されており、 foreach文のブロック内でこれを引数としてRrk_push_bk関数を呼んでいますので、 コンパイルエラーとなります。

配列指定部の前に tight を付けた場合も全く同様で、例えば以下の通りです。

varia ary int[3] = [ 1, 2, 3 ]
foreach e : tight ary { /* ary に対してtightモディファイア */
	Rrk_push_bk( ary, 10 ) /* コンパイルエラー : aryはこのブロック内で一時的にtightになっている */
}
Rrk_push_bk( ary, 10 ) /* ここはtightとはみなされない(宣言通りvariaとみなされる) */

多重配列の場合、const/tight指定の効力は要素の配列にまで及びます。 例えば以下の指定はコンパイルエラーとなります。

array IAry int[]
varia ary_of_ary IAry[] = [ 
	[ 1, 2, 3 ],
	[ 10, 20, 30 ],
]
foreach ary : tight ary_of_ary {
	Rrk_push_bk( ary, 9 ) /* コンパイルエラー: ary_of_aryのtight指定の効力はaryにまで及ぶ */
}

以下のように両方に tight 指定することもできますが、 外側のtightが内側にも及ぶため、結局aryの前のtightはあってもなくても同じです。

array IAry int[]
varia ary_of_ary IAry[] = [ 
	[ 1, 2, 3 ],
	[ 10, 20, 30 ],
]
foreach tight ary : tight ary_of_ary {
	Rrk_push_bk( ary, 9 ) /* コンパイルエラー: aryはtight */
}

要素参照用変数でのconststr指定


まずはforeach 文にconstを指定した以下の例をご覧ください。

import std/str
varia sary string[]
foreach s : const sary {
	RrkStr_set( s, "a" ) /* コンパイルエラーとはならない */
}

上記では元々saryはvaria string[]型ですが、foreach文の「配列指定部」の直前にconstを指定しているため、 foreach文内部ではconst string[]型とみなされます。また「要素参照用変数」であるsもconst string型とみなされます。 しかし文字列のセクションでも説明しましたが、const string型では「=」による再代入は禁止できますが、 その実体の文字列の加工を禁止することまではできません。 この s を(const string型ではなく)conststr型にできないでしょうか?

foreach文では「要素参照用変数」の直後にconststrを指定することでこのようなことが可能です。 例えば以下の通りです。

import std/str
varia sary string[]
foreach s conststr : const sary { /* s をconststr型とみなす */
	RrkStr_set( s, "a" ) /* コンパイルエラー: sはconststr型となっている */
}

上記ではsaryの要素はstring型ですが、sをconststrと明示的に指定しているため、foreachブロック内ではconststr型となります。 そのため、(引数としてstring型をとる)RrkStr_set関数の呼び出しで狙い通りコンパイルエラーとなります。 この指定を行う場合、saryは要素の型が文字列の配列(string[]またはconststr[])でなければなりません (要素の型が文字列ではない配列の場合、この指定をすることはできません)。

この指定でconststr化されるのはあくまで s だけであり、sary自体はconst string[]のままとなります。 例えば sary[0] といったアクセスをした場合、それはconst stringのままであると言うことです。 またこれに関してsary自体を conststr[] 化するような構文はサポートされていません。

配列指定部に文字列型を与えた場合


本来、foreach文の「配列指定部」には配列を与えなければなりませんが、 特例としてここに文字列(string型やconststr型や文字列リテラル)を与えることもできます。 その場合、「要素参照用変数」はconst int型とみなされ、文字列の各要素である文字がそこに格納されます。 そのため、文字列を更新することはできませんが、文字列の中身を整数レベルで確認する用途では便利でしょう。 例えば以下の通りです。

import std/str
varia ans string
{
	varia str string = "abc"
	ans = ""
	foreach ch : str {
		Rrk_print( "ch=" ch "\n" )
		RrkStr_add_c( ans, ch )
	}
	Rrk_print( \=ans \n )
}
{
	ans = ""
	foreach ch : "ABC" {
		Rrk_print( "ch=" ch "\n" )
		RrkStr_add_c( ans, ch )
	}
	Rrk_print( \=ans \n )
}

この例の実行結果は以下のようになります。

ch=97
ch=98
ch=99
ans=abc
ch=65
ch=66
ch=67
ans=ABC

この用法では(たとえ「配列指定部」にstringを指定したとしても)「要素参照用変数」は必ずconst int型とみなされますので、 これに代入することはできません。 例えば以下はコンパイルエラーとなります。

varia str string = "abc"
varia new_ch = 'x'
foreach ch : str {
	ch = new_ch /* Compile error. */
}

これが許可されない理由は、str[idx] という形式では代入が許可されない理由と同じです。 つまり文字列の要素の中途半端な位置にもしも '\0' が代入された場合、strの内部状態が破壊されるからです。

index付きforeach文


場合によってはforeachの中で、(for文のときのように)現在の要素に相当する配列のインデックスを参照したいことがあります。 そのためにはforeach文に(「:」区切りで)新たに第3の引数を与え、そこにインデックス名を指定します。 これは「foreach 要素参照用変数 : 配列指定部 : インデックス名 ブロック」という書式になります (つまり今まで説明してきたforeach文は、実はこの第3の引数を省略した形でした)。

例を見た方がわかりやすいかもしれません。 以下をご覧ください。

varia ary int[] = [ 10, 20, 30 ]
foreach e : ary : idx { /* 第3の引数idxを与えた場合 */
	Rrk_print( "ary[" idx "]=" e \n )
}

今回の例ではforeach文に第3の引数が与えられており、これはuint型の変数idxとしてforeach文内のブロックに宣言されているのと同じ効果を持ちます。 このidxには最初 0 がセットされており、foreach文のループを回るたびに1加算されていき、最後の要素のインデックス(上の例では3-1=2)が最終の値となります。 上記の実行結果は次のようになります。

ary[0]=10
ary[1]=20
ary[2]=30

for文と同様、foreach文もネストすることができます。 このとき、外側のforeachと内側のforeachに別々の「要素参照用変数」と「インデックス名」を付ければ、 以下のように二次元配列にアクセスする処理を記述できます。

array AryInt3 int[3]
varia ary2d AryInt3[2] = [
	[ 1, 2, 3 ],
	[ 4, 5, 6 ],
]
foreach ary : ary2d : i {
	foreach e : ary : j {
		Rrk_print( "ary2d[" i "][" j "]=" e \n )
	}
}

上記は「for文」のセクションで挙げた多次元配列の例をforeach文で書き直したものです。 ary2dは要素としてAryInt3という配列型を持つ配列であり、最初のforeach文でその各要素をaryに格納しています。 次に内側のforeachでそのaryの各要素を e で取り出してそれを表示しています。 またインデックスについては、iがary2dに関するインデックスを、jがaryに関するインデックスを保持します。 このとき i と j の値の変わり方はfor文をネストさせた時と同様です。 上記の実行結果は次のようになります。

ary2d[0][0]=1
ary2d[0][1]=2
ary2d[0][2]=3
ary2d[1][0]=4
ary2d[1][1]=5
ary2d[1][2]=6

for文とどちらが見やすいかは結局好みによるでしょう。 このような場合どちらを使っても構わないと思います。

あまりない状況な気もしますが「要素参照用変数」が不要で「インデックス名」の方だけが必要な場合、 前者を省略して記述することもできます。 ただしその場合、一番始めの「:」を省略してはいけません。 例えば次のようになります。

const ary int[] = [ 10, 20, 30 ]
foreach : ary : i { /* 「要素参照用変数」を省略した形 */
	Rrk_print( \=i\n )
}

文法上はさらに以下のように両方省略した形も一応許されますが、さらにレアケースでしょう。 この場合、本当に単に中味が配列の要素数分、反復して実行されるだけです。

const ary int[] = [ 10, 20, 30 ]
foreach : ary { /* 「要素参照用変数」と「インデックス名」を両方省略した形 */
	Rrk_print( "hello"\n )
}

もっとも、このようにわざわざ省略しなくともforeach内のブロック内で単にそれを無視すればよいだけなので、 (名前衝突の恐れがなければ)次のように書いても一向に構わないわけではあります(パフォーマンスは変わりません)。

const ary int[] = [ 10, 20, 30 ]
foreach e : ary : i { /* 「要素参照用変数」と「インデックス名」を両方書いてはいるが使っていない */
	Rrk_print( "hello"\n )
}

for文とforeach文の使い分け


foreachは配列のすべての要素にアクセスする場合は非常に便利ですが、 一方で「すべて」ではなく「一部」の要素にアクセスしたい場合は、 for文よりかえって使いにくいかもしれません。 例えば開始インデックスが0より大きい場合や終了インデックスが「配列の要素数-1」より小さい場合、 さらに複雑な例では例えば奇数番目だけにアクセスしたい場合などです。

以下の例は配列の2番目から4番目までの要素の和を計算するものですが、 foreach文を使った場合、1番目以下の要素をスキップするため、 ブロック内部で現在のインデックスを確認した上で、若干工夫してこれを調整しなければなりません。

varia iary int[] = [
	10, 20, 30, 40, 50, 60,
]
varia sum int = 0
foreach e : iary : idx {
	/***
	 * idxが 2 未満の場合はまず 1 へ変更.
	 * continueのタイミングでプラスされるため、結局2から始まることになる.
	 */
	if idx < 2 {
		idx = 2 - 1
		continue
	}
	/***
	 * idxが 4 より大きい場合はbreak
	 */
	if idx > 4 {
		break
	}

	Rrk_print( "e=" e \n )
	sum += e
}
Rrk_print( "sum=" sum \n )

上記の実行結果は次のようになります。

e=30
e=40
e=50
sum=120

あるいは配列 iary の一部を別の配列として独立化させ、それに対してforeach文を適用するなどする必要がありますが、 それならばおそらくもうfor文の方が手っ取り早いでしょう。 全く同じ処理をfor文を使って書いたものが以下になります。

varia iary int[] = [
	10, 20, 30, 40, 50, 60,
]
varia sum int = 0
for idx:=2; idx<5; ++idx {
	const e = iary[idx]
	Rrk_print( "e=" e \n )
	sum += e
}
Rrk_print( "sum=" sum \n )

その他、行列やベクトル計算などのように複雑なインデックス操作が必要な場合もfor文の方が適しています。

foreach文のまとめ


foreach文はfor文とwhile文と同じくループ文の一種であり、Rarakuのループ文の説明はこれで全て完了です。 foreach文は配列の要素を最初から順に辿っていくような処理に関して最も簡潔に書ける文です。 そして実際のプログラミングではそのような状況は頻繁に発生するため、 while文やfor文よりもforeach文を使う機会の方が多いと思われます。

またforeach文では配列変数や要素参照用変数にモディファイアによる制限を加えることもでき、 その分コードの安全性を高かめる効果が期待できます。

一方でそこに配列がない場合は基本的にforeach文を使うことはできません。 そのような状況ではwhile文やfor文を使用することになります。



目次に戻る

構造体

構造体の基本


構造体とは複数の異なる型のデータを一括で纏めて一つの変数にしたものです(C言語における構造体と役割としてはほぼ同じものです)。 構造体を構成する各データのことを構造体のメンバまたは構造体のフィールドと呼びます。 構造体を宣言するには struct 構造体型名 { ... } という書式を使います。

structはstructureの略です。 他の言語でも登場するキーワードですが「ストラクト」と発音することが多いと思います。

参考: C言語の構造体との違い
この「参考」はC言語をご存知の方のみお読みください。

Rarakuのstruct文では、ちょうどC言語における「typedef struct { ... } *構造体型名」に相当することをやっています。 ここで「*」がついているのは、Rarakuの構造体は実際にはC言語で言えば「構造体のポインタ」のように振舞うからです。 そしてその実体はちょうどmallocで確保されたような状態であると考えて構いません。 ただしRarakuではこのとき確保された領域はガベージコレクタにより自動的に解放されます。 従って、freeに該当するような関数呼び出しは不要になります。

Close


この書式ではまず構造体の型名を定義し、{ ... } 内には構造体のメンバを列挙して書きます。 各メンバの書式は「メンバ名 型名」となり、その書式がメンバの個数分繰り返されることになります。 例えば以下のようになります。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = {}

この例では、MyStructという構造体型を新たに定義しており、この構造体はiとrをメンバに持ちます。 次にそれを使ってvaria文によりMyStruct型の変数 s を宣言しています。 この記事では簡単のため「構造体変数 s」を単に「構造体 s」などと表記することもあります。

varia文の後ろにある「={ }」の意味ついてはすぐ後で述べますが、この記述は大抵の場合省略可能です。

構造体変数の初期化は、構造体の初期化子を用いて行います。 構造体の初期化子とは「{」で始まり、「}」で終わる範囲に、初期値となる要素をカンマで区切って指定したものです。

以下の例では構造体変数 s の各要素を 10, 20.5 で初期化しています。 初期化子の最後の要素(この例では20.5)の直後にあるカンマはあってもなくても構いません。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = { 10, 20.5, }

また以下のように初期化子を用いて、後から構造体変数に代入することもできます。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = { 0, 0.0 }
s = { 10, 20.5, } /* 途中で別の初期化子を代入 */

varia/const文における初期化および代入文のいずれにおいても、 実際は初期化子の各メンバを構造体変数の各メンバへコピーしているわけではなく、 一旦初期化子によって作られた無名の構造体全体を、左辺の変数が参照している形になります。 ただしこの実体は文字列リテラルとは異なり、どの変数からも参照されなくなった瞬間にGC(ガベージコレクタ)により自動的に削除されます。 例えば上の例の代入によって元々の「{ 0, 0.0 }」はどの変数からも参照されなくなりますから、この瞬間GCにより削除されます。
C言語等と異なり、struct文内の各メンバの終わりの「;」は通常不要ですが、 何か理由があってこれを付けたいのであれば付けても構いません。 例えば以下のように書くのは特に問題ありません。。

struct MyStruct {
	i int;
	r real;
}

ただしその場合「;」を2個以上連続させてはいけません。

宣言した構造体変数のメンバにアクセスするには「.」演算子を使います(C言語の「->」演算子と同様に機能します)。 例えば以下のようになります。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = { 0, 0.0 }
s.i = 10
s.r = 20.5

この例ではまず構造体MyStruct型の変数 s を宣言し、そのメンバであるiとrにそれぞれ個別にアクセスし、10と20.5を代入しています。

構造体変数には null を代入することもできます。 例えば以下の通りです。

struct MyStruct {
	i int
	r real
}

varia s MyStruct^? = { 10, 20.5, }
s = null /* nullを代入 */

ただしこの場合、構造体型名の直後に「^?」を付記する必要があります。 また上記の「s = null」を実行後は、この変数 s は null 値そのものを指し示します。

構造体型名の直後に「^?」を付記した場合、その構造体変数のメンバにアクセスすることが(そのままでは)できなくなります。 例えば以下のように(単に何もせず)「.」演算子を使ってそのメンバにアクセスするとコンパイルエラーとなります。

struct MyStruct {
	i int
	r real
}

varia s MyStruct^? = null
s.i = 10 /* NG: Compile error. */

null値は無効な構造体を意味します。そのためnull値である構造体においては、そのメンバへアクセスは不可能です。 「^?」が付記された構造体変数においては(無効な構造体を意味する)nullが代入されている可能性が排除できないため、 このようなメンバへのアクセスを記述した場合、コンパイルの段階でエラーとなります。

仮にこれがコンパイルエラーにならなかった場合、実行時にそのような状況が発生する可能性が生じます。 そして実際そのようなことが発生した場合、RarakuVMはエラー(ランタイムエラー)を表示して実行を中止します。 十分なテストが行われている場合であればこのようなエラー(バグ)を検出できますが、 そもそもコンパイル段階でその可能性を排除する方が一般にはより堅牢であるため、 型名に「^?」を指定してnull値となる可能性の有無を厳密に区別しています。

このような(「^?」が付記された)構造体変数においてこのようなメンバへのアクセスを可能にするには notnull指定を伴ったif文を使用し、その中身でメンバにアクセスしなければなりません。 これについては「nullableとnotnull」のセクションで詳しく述べますが、およそ以下のような形になります。

struct MyStruct {
	i int
	r real
}

varia s MyStruct^? = null
if s { /* notnullへ再解釈 */
	Rrk_print( "s.i=" s.i "\n" )
	Rrk_print( "s.r=" s.r "\n" ) 
} else {
	Rrk_print( "s is null.\n" )
}

ここで「if s {」と記述した部分は、意味的には「if s != null {」と同じです。 ただし上記では「if s != null {」ではなく必ず「if s {」と記述しなければなりません。 また if 文の条件式には括弧を付けることも可能と説明しましたが、今回のケースでは「if( s ){」のように括弧を付けてはいけません。 この「if s {」といった記述により、ifブロックの内部では s がnullになる可能性が排除されるため、 コンパイラは s を「MyStruct^?」型ではなく「MyStruct」型であると再解釈します。 つまり通常の構造体型と解釈しますので「s.i」や「s.r」といったメンバへのアクセスが普通に許可されます。

ちなみに「if !s」と記述した場合はこの対応が真逆になります(つまり「if s == null」と全く同じです)。 ただしそのように記述した場合、ifブロックの中では以前として s は「MyStruct^?」型のままであるため、 「s.i」や「s.r」といったメンバへのアクセスは禁止されます。
一般にbool型変数に構造体変数の値を代入することはできませんが、 if文などの条件分岐やwhile文などのループ文における条件式、あるいは論理演算子(「&&」「||」「!」)では、 例外的に構造体変数を単独で指定できます(これは整数などの場合と同様です)。 このとき、その構造体変数がnull以外の値に等しいならば true、null値に等しいならば false と評価されます。

例えば「if s1 && s2」、「if s1 || s2」などと記述することも可能で、 その場合も各構造体変数がnull値に等しいか否かで同様にbool値として評価されます。

ただしこのような条件式の評価の結果、その構造体が「^?」付きのもの(nullable)から 「^?」付きでないもの(notnull)へ常に再解釈されるとは限りません。 このような再解釈をさせるためには細かい記法上のルールがありますが、このセクションではこれ以上詳しくは取り上げません (詳しくは「nullableとnotnull」のセクションで述べます)。

初期化子内で指定したメンバの数よりも構造体で定義されたメンバの数が多い場合、 足りない残りのメンバはその型のデフォルト値で自動的に初期化されます。 その型のデフォルト値とは、例えばint型のメンバならば0、real型のメンバならば0.0、文字列のメンバならば""(空文字列)となります。 実際の例も見てみましょう。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = { 10, } /* varia s MyStruct = { 10, 0.0 } と全く同じ */

上記で「{ 10, }」では、メンバ r だけ明示的に初期化されていないため、 その残りのメンバ r は自動的に 0.0 で初期化されます。 このような初期化子内の足りないメンバに対する自動的な初期化を初期化子の自動補完と呼びます。

一番最初の例では以下のように初期化子内にメンバを何も指定せずに「{ }」と書いていました。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = {} /* varia s MyStruct = { 0, 0.0 } と全く同じ */

この場合も初期化子の自動補完は同様に機能します。 即ちまず構造体の実体が確保され、その構造体のすべてのメンバがその型のデフォルト値で自動的に初期化されます。 上記の例では、「{ 0, 0.0 }」で初期化されたのと同じになります。

varia文において初期化子を記述せず構造体変数を宣言した場合、デフォルトで「={}」で初期化されたものとみなされます。 例えば以下の通りです。

struct MyStruct {
	i int
	r real
}

varia s MyStruct /* varia s MyStruct = {} と全く同じ */

上記は、右辺に「={}」が指定されたのと全く同じになります (初期化子の自動補完まで考えるとこの場合、結局「{ 0, 0.0 }」で初期化されることになります)。

複雑な構造体


構造体のメンバがまた構造体であるような記述も可能です。 例えば以下のようになります。

struct OtherStruct {
	c conststr
}
struct MyStruct {
	i   int
	r   real
	other OtherStruct
}

varia s MyStruct /* 一旦デフォルト値で確保 */
s.i = 10
s.r = 20.5
s.other.c = "hello"

ただしこれはMyStructがOtherStruct全体をそのまま含んでいるのではなく、単に(MyStructが)OtherStructを指し示すメンバotherを一つ持つということです。

参考: C言語との比較
この「参考」はC言語をご存知の方のみお読みください。

これはC言語において以下のように記述しているのと概念的には同じです。

typedef struct OtherStruct_tag {
	const char* c;
} *OtherStruct; /* ポインタ型としてOtherStruct型を新たに定義 */

typedef struct MyStruct_tag {
	int    i;
	double r;
	OtherStruct other; /* OtherStructはポインタ型 */
} *MyStruct; /* ポインタ型としてMyStruct型を新たに定義 */

struct MyStruct_tag    s__instance__ = { 0 };
struct OtherStruct_tag other__instance__ = { 0 };
MyStruct s = &s__instance__;
s->other = &other__instance__;

s->i = 10;
s->r = 20.5;
s->other->c = "hello";

Close


そしてRarakuでは構造体のメンバもまたデフォルトでは「={}」が指定されたかのように初期化されます。 そのため上記では、s.otherを初期化子「{ }」により明示的に初期化する必要はありません(してもかまいませんけども)。

(C言語などとは異なり)以下のようにstruct文をネストして書くことはできません。

struct MyStruct {
	i   int
	r   real
	struct OtherStruct { /* コンパイルエラー */
		c conststr
	}
}

Rarakuでは厳密な意味で構造体に構造体を含めることはできません。 あくまで構造体を指し示すものがそのメンバとなります。

構造体の中の構造体のメンバにアクセスするには、「.」でのメンバアクセスを連続させます。 これはちょうどディレクトリを階層的にアクセスするために「/」区切りでパスを記述するのに似ています。 その後、s.otherでOtherStruct型のメンバにアクセスできる形になります。 otherの後ろにさらに「.」を続けて s.other.c とすることで最奥にあるメンバ c にアクセスできます。

この「.」はC言語で言えば「->」演算子に相当します。

構造体のメンバとして配列を含めることもできます。 例えば以下のようになります。

struct MyStruct {
	i   int
	r   real
	ary int[3]
}

varia s MyStruct /* 一旦デフォルト値で確保 */
s.i = 10
s.r = 20.5
s.ary[0] = 30

上記で s.ary は int[3] 型の配列としてRaraku内部で自動的に確保と初期化が行われます。 そのため、特になにもせずとも例えば s.ary[0] へのアクセスが可能となっています。

メンバに構造体や配列を持つような複雑な構造体の場合でも、初期化子による初期化ができます。

構造体をメンバとして持つ構造体の場合、そのメンバに対応する位置に構造体の初期化子 { ... } を記述します。 例えば以下のようになります。

struct OtherStruct {
	c conststr
}
struct MyStruct {
	i   int
	r   real
	other OtherStruct
}

varia s MyStruct = {
	10,
	20.5,
	{ /* otherメンバ(構造体OtherStruct)の初期化 */
		"hello",
	},
}

逆にそのメンバに対応する位置に構造体の初期化子 { ... } を記述しなかった場合、 そのメンバ(構造体)のデフォルト値、すなわち「{}」が指定されたのと同じになります。

配列をメンバとして持つ構造体の場合、その配列のメンバに対応する位置に配列の初期化子 [ ... ] を記述します。 例えば以下のようになります。

struct MyStruct {
	i   int
	r   real
	ary int[3]
}

varia s MyStruct = {
	10,
	20.5,
	[ /* aryメンバ(int[3]配列)の初期化 */
		0, 1, 2,
	],
}

逆にそのメンバに対応する位置に配列の初期化子 [ ... ] を記述しなかった場合、 型の情報に従って全体を自動的に確保し、各要素値をデフォルト値で初期化します。

構造体を要素に持つ配列を宣言して、それを初期化子により初期化することもできます。 この場合における初期化子は、まず配列の初期化子 [ ... ] が外側に来て、構造体の初期化子 { ... } が内側に来ます。 例えば以下のようになります。

struct MyStruct {
	i   int
	r   real
}

varia s_ary MyStruct[] = [
	{ 10, 10.5 },
	{ 20, 20.5 },
	{ 30, 30.5 },
]

varia sum = s_ary[0].i + s_ary[1].i + s_ary[2].i

構造体を要素として持つ配列で、その要素の初期化子 { ... } を記述しなかった場合、 各要素はそのデフォルト値、すなわち(構造体のデフォルト値ですから) 「{}」が指定されたものとみなされます。

デフォルトメンバ初期化子


初期化子の自動補完についてもう一度考えてみます。 以下の例の一番最後の行では、初期化子においてメンバ r だけ明示的に指定されていません。 このときこれは自動的に 0.0 で初期化されるのでした。

struct MyStruct {
	i int
	r real
}

varia s MyStruct = { 10, } /* varia s MyStruct = { 10, 0.0 } と全く同じ */

このように例えばreal型のメンバの場合、デフォルトでは 0.0 で初期化子の自動補完が行われますが、 Rarakuではこのデフォルト値を構造体の定義において変更することができます。 例えば以下をご覧ください。

struct MyStruct {
	i int
	r real = 2.5 /* デフォルト値を2.5に変更 */
}

varia s MyStruct = { 10, } /* varia s MyStruct = { 10, 2.5 } と全く同じ */

上の例では一つ前の例と同じく、最後の行で r の指定が省略されていますが、今回は 0.0 ではなく 2.5 で補完されます。 これは構造体の定義部において、「r real」の後ろに「=2.5」と記述することで デフォルト値を(通常は0.0であるところを)2.5に変更しているためです。 このような構造体定義内での「=」を使った指定をデフォルトメンバ初期化子と呼びます。

この「デフォルトメンバ初期化子」という用語は C++ から拝借したものです。 また関数で言えば、デフォルト引数によく似た仕組みです。

デフォルトメンバ初期化子を指定していたとしても、 初期化子においてそのメンバの値を明示的に指定している場合は、もちろんそれが優先されます。 例えば以下の通りです。

struct MyStruct {
	i int
	r real = 2.5
}

varia s MyStruct = { 10, 3.0 } /* もちろん書いたとおりの意味 */

上の例ではデフォルトメンバ初期化子の指定より明示的な指定3.0が優先され、 s.r の値は 3.0 になります。

念のために補足しますが、このデフォルトメンバ初期化子の指定はその構造体でのみ有効です。 他の構造体のメンバに影響を与えることはありません。 例えば以下のように3つの構造体があった場合、それぞれ独自にデフォルト値を持つ形になります。

struct MyStruct {
	i int
	r real = 2.5
}
struct YourStruct {
	i int
	r real
}
struct OtherStruct {
	i int  = 30
	r real = 3.5
}

varia m MyStruct    = {} /* varia m MyStruct = { 0,  2.5 } と全く同じ */
varia y YourStruct  = {} /* varia y MyStruct = { 0,  0.0 } と全く同じ */
varia o OtherStruct = {} /* varia o MyStruct = { 30, 3.5 } と全く同じ */

デフォルトメンバ初期化子を指定する場合、右辺がリテラルであればメンバの型を省略して記述できます。 これはconst/varia文で見られた省略と規則は同じで、右辺のリテラルの型からそのメンバの型が推論されます。 例えば上の例は以下のように記述可能です。

struct MyStruct {
	i int
	r = 2.5
}
struct YourStruct {
	i int
	r real
}
struct OtherStruct {
	i = 30
	r = 3.5
}

varia m MyStruct    /* varia m MyStruct = { 0,  2.5 } と全く同じ */
varia y YourStruct  /* varia y MyStruct = { 0,  0.0 } と全く同じ */
varia o OtherStruct /* varia o MyStruct = { 30, 3.5 } と全く同じ */

上記の「r = 2.5」では、この r は自動的に real 型とみなされますし、 「i = 30」では、この i は自動的に int 型とみなされます。 上記ではついでにm、y、oの初期化子「={}」も省略しています。

整数型や実数型以外のメンバに対してもデフォルトメンバ初期化子を指定できます。 例えば文字列型の場合以下の通りです。

struct MyStruct {
	cs = "Hello" /* cs conststr = "Hello" と全く同じ */
}

varia m MyStruct /* varia m MyStruct = { "Hello" } と全く同じ */

配列型の場合以下の通りです。

struct MyStruct {
	ary int[3] = [ 10, 11 ] /* デフォルト値を[ 10, 11 ]へ変更 */
}

varia m MyStruct /* varia m MyStruct = { [ 10 , 11 ] } と全く同じ */

上記では、m.ary のそれぞれの要素の値は 10, 11, 0 となります。

その他、後のセクションで登場する列挙型、ハンドラ型、関数型についてもデフォルトメンバ初期化子を指定できますが、 これらについてはそれが登場するセクションで改めて述べます。

構造体のメンバが構造体である場合、そのメンバのデフォルト値は「{}」になるのでした。 例えば以下の通りです。

struct OtherStruct {
	cs = "Hello Other"
}
struct MyStruct {
	other OtherStruct
}

varia m MyStruct = {} /* varia m MyStruct = { {} } と全く同じ */
/* m.other の値は{}であり、従って m.other.cs へのアクセスも可能 */

このように m を単に「{ }」で初期化した場合、その中身の other も「{ }」となり(つまり全体として「{ { } }」となり)、 特に何もせずとも「m.other.cs」などのようにotherのメンバにアクセスすることもできます。 さらに今回の場合OtherStruct内でデフォルト初期化子でcsの値が指定されていますから、other内のメンバも連鎖的に初期化され、 結局「m.other.cs」の値も自動的に "Hello Other" となります。

上記の m の初期化部分「= { }」は省略可能です。 省略した場合でも、この場合結局全体として「={ { } }」が指定されたのと同じになります。

ここで以下のように構造体の定義でのメンバ other において、デフォルトメンバ初期化子「=null」を指定してみます。

struct OtherStruct {
	cs = "Hello Other"
}
struct MyStruct {
	other OtherStruct^? = null /* メンバotherに対するデフォルト初期化子 */
}

varia m MyStruct = {} /* varia m MyStruct = { null } と全く同じ */
/* m.other が null となる(従って m.other.cs へのアクセスはこのままでは不可となる). */

このようにした場合、同じように m を単に「{ }」で初期化したとしても、今度はその中身の other は「{ }」ではなく「null」で初期化されます (つまり全体として「{ null }」となります)。 今度は other は OtherStruct^? と「^?」が付記された型となりますから、 この状態では「m.other.cs」へアクセスすることが制限されます。

ここまでの説明ですとわざわざデフォルトメンバ初期化子をnullで指定する意味は全くないように思えますが、 これは後述する自己参照構造体などを定義する場合に重要になります。

名前付き引数による構造体の初期化


関数呼び出し時、「引数名 : 値」のように記述することで、どの引数に何の値を設定するかを明示的に指定することができました。 これを名前付き引数(named arguments)と呼びましたが、構造体の初期化においても同様な指定ができます。

構造体のメンバに対する値の指定を「引数」と呼ぶのは厳密には違う気もしますが、 指定のルールとしては関数における名前付き引数とほとんど同じであるため、ここでは便宜上「引数」という用語を使うことにします。 また同様の理由で、これまでの構造体の初期化において行ってきた通常の値指定では、その値の順番と位置を正確に指定しなければなりませんが、 これを位置引数(positional arguments)と呼ぶことにします。

例えば以下の通りです。

struct S {
	x = 1 
	y = 2
	z = 3
}
varia s1 S = { 10, 20, 30 } /* A */
varia s2 S = { x:10, y:20, z:30 } /* B */
varia s3 S = { y:20, z:30, x:10 } /* C */

function print( s S ){
	Rrk_print( \=s.x \, \=s.y \, \=s.z \n )
}
print( s1 )
print( s2 )
print( s3 )

まず上記のAの指定「{ 10, 20, 30 }」は、これまで見て来た位置引数による指定です。

その次のBの指定「{ x:10, y:20, z:30 }」では、 第1番目に「x:10」(メンバxに10を指定する名前付き引数)、 第2番目に「y:20」(メンバyに20を指定する名前付き引数)、 第3番目に「z:30」(メンバzに30を指定する名前付き引数) を指定しており、結局Aの指定と結果は同じになります。

最後に上記のCの指定「{ y:20, z:30, x:10 }」では、 Bの指定とは順番が入れ替わってはいますが、どの引数に何の値を設定するかの対応はBと全く同じなので、 結果もBと全く同じになります。

結局、指定A,B,Cはどれも結果は同じであり、この例の実行結果は以下のようになります。

s.x=10, s.y=20, s.z=30
s.x=10, s.y=20, s.z=30
s.x=10, s.y=20, s.z=30

構造体の初期化における名前付き引数の指定で、存在しないメンバ名を指定した場合はコンパイルエラーとなります。 例えば以下の通りです。

struct S {
	x = 1 
	y = 2
	z = 3
}
varia s S = { x:10, w:20, z:30 } /* Compile error */

上記の2番目において、「w:20」と指定していますが、w という名前のメンバは構造体Sには存在しないため、 これは許可されずコンパイルエラーとなります。

構造体にデフォルトメンバ初期化子がある場合でも名前付き引数を指定することができます。 例えば以下の通りです。

struct S {
	x = 10 
	y int
	z = 30
}
varia s S = { y:20 }

function print( s S ){
	Rrk_print( \=s.x \, \=s.y \, \=s.z \n )
}
print( s )

上記では、メンバxとzはデフォルトメンバ初期化子が付属していますので、初期化において値の指定を省略した場合はその値となります。 一方でyはデフォルトメンバ初期化子が付属していませんので、初期化において値を省略した場合は、 その型固有のデフォルト値(int型の場合は0)で自動的に初期化されます。 尚、今回は「y:20」と明示的にメンバyの値を指定しています。 この例の実行結果は以下のようになります。

s.x=10, s.y=20, s.z=30

名前付き引数の指定において、特にメンバ名(「:」の左側)と指定する変数名(「:」の右側)が全く同じ場合、つまり「x:x」のような指定となる場合、 簡易記法として値の方(右のx)を省略して単に「x:」と書くこともできます(xを2回冗長に書かなくても済むということです)。 例えば以下の通りです。

struct S {
	x int
}
varia x = 10
varia s S = { x: }

Rrk_print( \=s.x \n )

上記で「x:」と指定していますが、これは「x:x」と指定したのと全く同じです。 この例の実行結果は以下のようになります。

s.x=10

尚、念のために補足しますと、値として「x+1」のように何か付加された式を指定する場合は、 この簡易記法は使えず「x:x+1」のように完全な形で指定しなければなりません。
ここまでの例だけを見ても、指定のルールは関数における名前付き引数とほとんど同じであることがわかると思います。

初期化における位置引数(通常の引数)と名前付き引数は混ぜて指定することもできますが、全く自由に指定できるわけではありません。 関数における「位置引数と名前付き引数の混合」で述べたルールと全く同じ規則が構造体の初期化においても当てはまります。 それとほぼ同じ説明にはなりますが、以下にこれについて述べます。

Rarakuでは名前付き引数を使ったときにそれが位置引数において本来指定すべきメンバの位置とズレている場合、「名前付き引数必須モード」へ移行します。 そしてひとたび「名前付き引数必須モード」へ移行した後は、位置引数で指定することはもう許可されず、 残りのメンバは名前付き引数で指定するか、値の指定自体を省略しなければなりません。 例えば以下の通りです。

struct S {
	x int
	y int
	z int
}
varia s S
s = { 10, y:20, 30 }   /* A:OK */
s = { 10, z:20, y:30 } /* B:OK */
s = { y:20, x:10, 30 } /* C:NG Compile error */

上記のAの指定「{ 10, y:20, 30 }」では、名前付き引数「y:20」が、本来 y が来るべき位置(2番目の位置)で 指定されていますので「名前付き引数必須モード」へは移行しません。 その次に位置引数として30が指定されていますが、これは許可されます(コンパイルエラーとはなりません)。 この30は(名前付き引数も含めて)3番目の位置に指定されていますので、3番目のメンバ、すなわちメンバzの値として指定されたものとみなされます。

次のBの指定「{ 10, z:20, y:30 }」では、名前付き引数「z:20」が、本来 z が来るべき位置(3番目の位置)で 指定されておらず、2番目の位置で指定されています。 そのため、この2番目の位置の時点で「名前付き引数必須モード」へ移行し、それ以降の指定もすべて名前付き引数で指定しなければなりません。 Bの指定ではその後の指定(「y:30」)も名前付き引数であり、(2番目以降は)最後まで位置引数は1つもありませんので、 これは許可されます(コンパイルエラーとはなりません)。

次のCの指定「{ 10, z:20, 30 }」では、名前付き引数「y:20」が、本来 y が来るべき位置(2番目の位置)で 指定されておらず、1番目の位置で指定されています。 そのため、この1番目の位置の時点で「名前付き引数必須モード」へ移行し、それ以降の指定もすべて名前付き引数で指定しなければなりません。 しかしCの指定ではその後の指定 30 が位置引数であり、これは許可されません(コンパイルエラーとなります)。

位置引数と名前付き引数が競合するような指定はコンパイルエラーとなります。 例えば以下の通りです。

struct S {
	x int
	y int
	z int
}
varia s S = { 10, x:10, z:30 } /* Compile error */

上記では、まず1番目で位置引数によりメンバ x の値を10で指定しており、 次に2番目でも名前付き引数によりメンバ x の値を10で指定しています。 今回はたまたま同じ値10を指定している形にはなっていますがそこは問題ではなく、 そもそも2箇所から同一のメンバ x を重複して指定するような形になりますのでこれは許可されずコンパイルエラーとなります。

Rrk_resize関数と要素がnull値の構造体配列


varia文での構造体使用において初期化を記述しなかった場合、その値は自動的に(「={}」が与えられたものとして)初期化されるのでした。 例えば以下の通りです。

struct MyStruct {
	i = 10
}

varia m MyStruct /* varia m MyStruct = {} と全く同じ */
/* m.i へのアクセスも可能 */

つまりこのとき構造体の実体が自動的に確保されるわけですが、 しかしこの仕様は「実体を確保する必要がない場合」において効率が問題にならないでしょうか? 例えば以下のような場合を考えます。

struct MyStruct {
	ia int[100]
}

varia ary MyStruct[100] /* こちらが実体 */
varia ary_of_pointer MyStruct[100] /* こちらは単に指し示す用途のための配列 */

上の例でMyStructは比較的サイズの大きな構造体であり、さらにその下にはそれを要素として沢山持つ配列が宣言されています。 またこの宣言においてaryがその配列の実体であり、ary_of_pointerは(何らかのアルゴリズムにおいて一時的に使用されるような)それらの要素を指し示す配列であり、事前にaryと同じ要素数を確保しておくものとします。

このとき、デフォルトでは ary_of_pointer の定義でも自動的に全要素の実体が無駄に確保されてしまいさすがに非効率です。 このような場合はむしろ各要素がnullで初期化された方が効率面では望ましいと思われます。 そのためにまずary_of_pointerを固定長配列ではなく可変長配列とし、以下のようにRrk_resize関数で初期化する方法が考えられます。

struct MyStruct {
	ia int[100]
}

varia ary MyStruct[100] /* こちらが実体 */
varia ary_of_pointer MyStruct[^?] /* こちらは単に指し示す用途のための配列 */
Rrk_resize( ary_of_pointer, Rrk_numof(ary) )

Rrk_resize関数は、配列の要素数を変更するという意味ではRrk_resizeWithInitValと同じですが、 配列の要素数が増加する場合、その増分の要素をすべて 0 か null 値で初期化します。 今回、配列の要素はMyStruct(構造体)型ですので、Rrk_resize関数によってary_of_pointerの要素はすべて null で初期化されます。 尚、Rrk_resize関数に指定する配列の要素はnullになり得る(nullable)ですので、 このような配列の宣言は、「[」と「]」の中に「^?」を付記しなければなりません。 上記の「MyStruct[^?]」の部分がこれにあたります。

自己参照構造体



構造体のメンバがその構造体自身と同じ型を持つものである場合、それを自己参照構造体と呼びます。

参照先の実体が(型が同じであっても)本当に自己であるとは限らないので、この名前には少し語弊があるような気もしますが、 一般的にはそう呼ばれる場合が多いと思います。 型だけを見た場合、(親となる)構造体自身の型が再び(子となる)メンバ内に現れるという意味で自己参照ということなのでしょう。 このような構造体はリンクリストや木構造などを実装する場合に用いられます。

例えば以下の通りです。

struct MyStruct {
	i int
	next MyStruct^?=null
}
varia m MyStruct /* varia m MyStruct = { 0, null } と全く同じ */

上記では大きなポイントが二つあります。 一見、無限に自分自身を含む奇妙な構造に見えますが、中身のメンバは単に構造体を指し示すもの(C言語でいえばポインタ)にすぎないという点が一つ目のポイントです。 そして中身のメンバnextのデフォルトメンバ初期化子として「=null」が指定されていますが、これが二つ目のポイントです。

このような自己参照構造体の定義においては、(自己参照となっている構造体メンバに)必ず「=null」を指定する必要があるとまずは考えましょう。 以下のようにnextのデフォルトメンバ初期化子として「={}」を指定した場合はコンパイルエラーとなります。

struct MyStruct {
	i int
	next MyStruct ={} /* Compile error: 自己参照しているメンバにデフォルトメンバ初期化子を指定することはできない */
}
varia m MyStruct

あるいは以下のようにこの指定を忘れた場合、 デフォルトメンバ初期化子の指定が自動的に補われますので、 結局コンパイルエラーになります。

struct MyStruct {
	i int
	next MyStruct /* Compile error: これは結局「={}」が省略されている形である */
}
varia m MyStruct

これらがコンパイルエラーになる理由を説明しましょう。 仮にここでデフォルトメンバ初期化子を「=null」以外にしてしまうと(つまり実体を確保するような指定にしてしまうと)、 意味上は構造体変数 m の宣言において再帰的かつ無限循環的に初期化が発生する形になります。 Rarakuコンパイラではこれを避けるため、この状況を自動的に検出してコンパイルエラー(circulating structure)とするわけです。

さて、varia文で宣言された構造体変数 m のメンバnextはnullとなりますから、 この段階ではまだどの「実体」も指し示していません。 この後、実際に宣言し使用する段階において、別のMyStructの実体を指し示すようにすることもできますし、本当に自分自身を指すようにすることもできます。

この nextに実体を与えるために、例えば次のように初期化子をネストすることができます。

struct MyStruct {
	i int
	next MyStruct^?=null
}

varia m MyStruct = { 0, {} } /* varia m MyStruct = { 0, { 0, null } } と全く同じ */

あるいは以下のように m.next に初期化子を明示的に代入することもできます。

struct MyStruct {
	i int
	next MyStruct^?=null
}

varia m MyStruct /* varia m MyStruct = { 0, null } と全く同じ */
m.next = {}

nextのデフォルトメンバ初期化子として「=null」を指定する方法以外に、 以下のように宣言時に明示的に完全な初期化をすれば、構造体定義の方では特に何もする必要がありません。

struct MyStruct {
	i int
	next MyStruct^?
}
varia m MyStruct = { 0, null } /* 完全な指定 */

ただ、この方法ではそれらvaria文での初期化を毎回忘れずに正確に行う必要があります (とはいえ忘れた場合でもコンパイルエラーにはなるはずですので、大した問題ではないのかもしれません)。

以下のように間接的に自己参照が発生するケースも見てみましょう。

struct MyStruct {
	i int
	other OtherStruct
}
struct OtherStruct {
	my MyStruct /* コンパイルエラー: 自己参照しているメンバであるのに「=null」が指定されていない */
}

varia m MyStruct

上記の例では、デフォルトメンバ初期化子として「=null」を指定し忘れています。 少し分かりにくいですがOtherStructを経由して結局MyStructに戻りますから、間接的には自己参照しており、 そのためコンパイルエラーとなります。 このような場合、少なくとも自己参照が発生するメンバ my においてデフォルトメンバ初期化子を「=null」として指定し、 自動初期化の効果を打ち消さなければなりません。 例えば以下の通りです。

struct MyStruct {
	i int
	other OtherStruct = {}
}
struct OtherStruct {
	my MyStruct^? = null /* OK: このnullにより無限循環は遮断される */
}

varia m MyStruct

構造体同士の代入


同じ型の構造体変数であれば代入することができます。 例えば次の通りです。

struct MyStruct {
	i   int
	r   real
}

varia s1 MyStruct = { 10, 20.5, }
varia s2 MyStruct
s2 = s1

Rarakuにおける構造体の代入では、ディープコピーは行われません。 左辺値には新たな構造体のメンバは確保されず、右辺値を参照しているだけになります。

本当にディープコピーを行いたい場合は、構造体の各メンバ毎に個別にコピー処理を行わなければなりません。 これについては次の「構造体コピー」の項で述べます。

特例として、左辺値の構造体のメンバの型の並びが、右辺値の構造体のメンバの型の並びに完全に含まれており、 かつその出だしの並びも一致している場合、キャスト演算子「->」を用いればそれらを代入できます。 例えば以下をご覧ください。

struct Struct1 {
	i   int
	r   real
}
struct Struct2 {
	ii  int
	rr  real
	c   conststr
}

varia s1 Struct1 = { 10, 20.5, }
varia s2 Struct2 = { 30, 40.5, "hello" }

s1 = s2->Struct1 /* OK : キャスト演算子「->」によりStruct1型へキャストしてから代入 */
//s2 = s1->Struct2 /* NG */

上記においてStruct1のメンバの型の並びとStruct2のメンバの型の並びを比較してください。 Struct1の方では int, real、Struct2の方では、int real, conststr となっており、 Struct1の全序列( int real )が Struct2 の開始の序列と一致しています。

このような型の組み合わせの場合、キャスト演算子「->」によりまずStruct1型へキャストして それをStruct1型の変数へ代入することができます(上記の例の場合「s1 = s2->Struct1」)。

このとき、s1に元々代入されていた「{10, 20.5}」という実体はもはや誰からも参照されなくなり、 従ってガベージコレクタにより自動的に削除されます。

一方、この逆は通常はできません(上記の例の場合「s2 = s1->Struct2」)。 コンパイルエラーとなります。

ただし左辺と右辺が「見かけ上」上記の条件を満たさない場合でも、右辺が指し示す実体の型が「実質上」その条件を満たしている場合があり、 さらにそれら二つが親子関係を持つならばダウンキャストと呼ばれる方法で代入ができます。 構造体の親子関係の定義方法とダウンキャストについては「型のキャストと暗黙変換」のセクションで詳しく述べます。

自己参照構造体の代入についても見ておきましょう。 以下の例をご覧ください。

struct MyStruct {
	i int
	next MyStruct^?=null
}

varia m MyStruct /* varia m MyStruct = { 0, null } と全く同じ */
varia n MyStruct /* varia n MyStruct = { 0, null } と全く同じ */
m.next = n

上記の例ではMyStruct型の構造体 m と n を宣言し、m のメンバ next に n を代入しています。

さらに以下のように本当に自分自身を参照させることもできます (もっとも、あまりこんなことはやらないかもしれませんが、グラフ構造としては一応想定できます)。

struct MyStruct {
	i int
	next MyStruct^?=null
}

varia m MyStruct /* varia m MyStruct = { 0, null } と全く同じ */
m.next = m /* m.next は本当にm自身を参照している */

この場合、「m.next.next.next …」と無限にアクセスすることもできてしまいますから、 ループ文でメンバへ連鎖的にアクセスして処理するような場合は注意が必要になります。

参考: 構造体の参照が循環している場合におけるGCの挙動
RarakuではGC(ガベージコレクタ)として基本的には reference-counting 方式を採用しています。

この方式はほとんどのケースでうまく機能し、動作が軽快で無駄なメモリは極力その瞬間に解放できるという点がメリットです。 一方、唯一参照が循環しているようなケースではうまく機能しないという点がデメリットです。 例えば上で述べたような本当に自分自身を参照しているような構造体があった場合、その部分だけが解放されずに残ります。 ただ、このように参照が循環しているような複雑なデータ構造を大量に確保したいケースというのは、それなりに限られますので 多くのプログラミングではこれはほとんど問題にならないと思います。

もしも参照の循環を伴う複雑なデータ構造(例えばグラフ構造)を大量に確保する必要があって、 このような残骸を確実に解放したい場合、RrkGC_clean 関数を呼び出す必要があります。 ただしこの関数はGCが管理するすべてのオブジェクトをスキャンするため、比較的重い処理になります。 例えば処理速度がシビアに要求されるようなタイミングでの呼び出しは避けつつ、 比較的余裕があるタイミングを狙ってこれを呼び出すようにすればよいかと思います(そのタイミングを見測るのが難しいケースもありますが)。

この関数RrkGC_cleanはPythonで採用されているGCの方式に相当する処理を内部で行っています。 この処理は mark-and-sweep と呼ばれるGCの方式とアイデアとしてはよく似たものになります。

RrkGC_clean は Raraku VMの実行終了時には必ず呼び出されます(従ってコードの最後に明示的に記述する必要はありません)。 これによりRarakuの実行全体が終了した段階では、そのような(参照の循環に由来する)残骸もすべて自動的に解放されることになります。

Close


構造体のconst/tight宣言


構造体をconstで宣言した場合、その「初期化」と「再代入」の可否の規則(アサイナビリティ)は配列のときと同様になります。 そのすべての組み合わせと可否をまとめて表にしたものを以下に示します(配列のセクションで示した表と全く同じです)。

  • 初期化の場合

  • 左辺値右辺値OK/NG
    varia varia OK
    varia const NG
    const varia OK
    const const OK


  • 再代入の場合

  • 左辺値右辺値OK/NG
    varia varia OK
    varia const NG
    const varia NG
    const const NG
特に「再代入」の場合、左辺値がconstであれば問答無用でNGとなることに注意してください。

尚、これは構造体の場合の規則であって、int型などではまた規則が変わってきます。 int型の場合、「初期化」についてはすべての組み合わせでOKであり、 また「再代入」については左辺値がvariaの場合は全てOK、左辺値がconstの場合は全てNGとなります。

構造体にconstを指定した場合、その構造体への再代入は勿論、そのメンバへの代入も禁止されます。 例えば以下をご覧ください。

struct Info {
	m int
}
function func( const info Info )
{
	info.m = 9 /* コンパイルエラー: infoはconst指定された構造体である. よってそのメンバmもconstである */
}

上記では関数funcの引数に構造体Infoがあり、しかもそれにconstが指定されています。 この場合、この関数内ではinfoのメンバの値を変更することが禁じられます。 上記ではこれを代入で変更しようとしているためコンパイルエラーとなります。

次にtight指定された例も見てみます。

struct Info {
	ary int[]
}
function func( tight info Info )
{
	Rrk_push_bk( info.ary, 9 ) /* コンパイルエラー: infoはtight指定された構造体である. よってそのメンバaryもtightである */
}

上記では関数funcの引数に構造体Infoがあり、しかもそれにtightが指定されています。 さらに構造体Info内のメンバ ary は今回は配列です。 この場合、この関数内ではinfoのメンバの配列がtightとみなされます。 配列がtightであるとは、その配列の要素数の変更が禁止されることでした(配列のセクションで説明しました)。 よってinfoのメンバaryに対して、Rrk_push_bk関数の実行は認められずコンパイルエラーとなります。

多重にネストされた構造体の場合も同様で、その場合const/tightの効果が再帰的に内側の構造体にも適用されます。

構造体のコピー


構造体同士の代入を行うと、代入先は代入元と同じ構造体の実体を指し示す形になります。 この状態で代入元のメンバの値を変更すると、それに連動して代入先の該当メンバもその値に変わります。 また全く同様に代入先のメンバの値を変更すると、それに連動して代入元の該当メンバもその値に変わります。 次の例でこの様子を確認できます。

struct MyStruct {
	i int
}
varia m1 MyStruct = { 10 }
varia m2 MyStruct = { 20 }

m1 = m2 /* A */
Rrk_print( \=m1.i \n ) /* m1.i=20と表示される */
Rrk_print( \=m2.i \n ) /* m2.i=20と表示される */

m2.i = 30 /* B */
Rrk_print( \=m1.i \n ) /* m1.i=30と表示される */
Rrk_print( \=m2.i \n ) /* m2.i=30と表示される */

m1.i = 40 /* C */
Rrk_print( \=m1.i \n ) /* m1.i=40と表示される */
Rrk_print( \=m2.i \n ) /* m2.i=40と表示される */

上記では「/* A */」の部分で構造体m1に構造体m2を代入しています。 この結果、m1が指し示している実体はm2と同一になります。 この状態で「/* B */」においてm2のメンバiだけを30に変更していますが、 m1も同じ構造体を指し示しているため、直後の確認表示ではm1のメンバiも30に変更されているかのように見えることになります。 全く同様に「/* C */」においてm1のメンバiだけを40に変更していますが、 m2も同じ構造体を指し示しているため、直後の確認表示ではm2のメンバiも40に変更されているかのように見えることになります。 単なる構造体の代入は非常に高速ですが、上記のような性質を持つことに注意が必要です。 尚、上記の実行結果は以下のようになります。

m1.i=20
m2.i=20
m1.i=30
m2.i=30
m1.i=40
m2.i=40

一方、代入先の構造体は別の実体を指し示しており、その状態を維持したままその実体のメンバへ値をコピーしたい場合があります。 その場合、最も素直な方法は「m1 = m2」といった構造体自体の代入は行わず、かつ「m1.i = 30」のようにして、 メンバだけの代入を行うことです。

次の例でメンバへの値の代入とその値の確認を行ってみます。

struct MyStruct {
	i int
}
varia m1 MyStruct = { 10 }
varia m2 MyStruct = { 20 }

m1.i = 30
Rrk_print( \=m1.i \n ) /* m1.i=30と表示される */
Rrk_print( \=m2.i \n ) /* m2.i=20と表示される */

m2.i = 40
Rrk_print( \=m1.i \n ) /* m1.i=30と表示される */
Rrk_print( \=m2.i \n ) /* m2.i=40と表示される */

上記の例では、「m1 = m2」といった構造体自体の代入は行われていません。 そのため、m1 と m2 はそれぞれ別の実体を示したままです。 また「m1.i = 30」 と「m2.i = 40」といった代入についてもそれぞれのメンバは別の実体ですから、 それぞれ独立して変化します。 上記の実行結果は以下のようになります。

m1.i=30
m2.i=20
m1.i=30
m2.i=40

このように、m1 と m2 をそれぞれ別の実体を示したままに、 構造体m2の全メンバの値を、構造体m1の(対応する)全メンバへコピーしたければ、 「m1.i = m2.i」と「m1.j = m2.j」の二つを個別に実行すればよいことになります。 例えば次の通りです。

struct MyStruct {
	i int
	j int
}
varia m1 MyStruct = { 10, 20 }
varia m2 MyStruct = { 100, 200 }

/* それぞれのメンバを個別にコピー */
m1.i = m2.i
m1.j = m2.j

Rrk_print( \=m1.i \, \=m1.j \n ) /* m1.i=100, m1.j=200と表示される */
Rrk_print( \=m2.i \, \=m2.j \n ) /* m2.i=100, m2.j=200と表示される */

上記の実行結果は以下のようになります。

m1.i=100, m1.j=200
m2.i=100, m2.j=200

このようにコピー元とコピー先のそれぞれが指し示す実体が別々である状態で、 それぞれに対応するすべてのメンバーについてコピーすることを(構造体の)ディープコピーと呼びます。 ディープコピーは単なる構造体の代入に比べ低速ですが、このようなコピーが必要な場合もあります。

ところで上の例では各構造体のメンバ i、j を直接指定してこれらを逐一コピーしていますが、 構造体のコピーが発生するたびに直接このように書くと例えばコードを修正してこの構造体に新しいメンバ k を追加した場合、 すべての箇所にkに関するコピー処理「m1.k = m2.k」を忘れずに追加しなければならなくなります。

そのため、基本的には構造体毎にコピー用の関数を定義し、構造体のコピーが発生する場合はその関数を呼び出すようにすべきです。 例えば次の通りです。

struct MyStruct {
	i int
	j int
}
/* 構造体MyStructのコピー用関数 */
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	/* それぞれのメンバを個別にコピー */
	dst.i = src.i
	dst.j = src.j
}

varia m1 MyStruct = { 10, 20 }
varia m2 MyStruct = { 100, 200 }

MyStruct_copy( m1, m2 )
Rrk_print( "m1.i="m1.i  \,  "m1.j="m1.j \n ) /* m1.i=100, m1.j=200と表示される */
Rrk_print( "m2.i="m2.i  \,  "m2.j="m2.j \n ) /* m2.i=100, m2.j=200と表示される */

次に文字列をメンバに持つ構造体のディープコピーを考えます。 まずは以下をご覧下さい。

import std/str

struct MyStruct {
	c conststr
	s string
}
/* 構造体MyStructのコピー用関数 */
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	/* それぞれのメンバを単純に代入 */
	dst.c = src.c
	dst.s = src.s
}

varia m1 MyStruct = { "M1_C", "M1_S" }
varia m2 MyStruct = { "M2_C", "M2_S" }

MyStruct_copy( m1, m2 ) /* A */
Rrk_print( \=m1.c \, \=m1.s \n ) /* m1.c=M2_C, m1.s=M2_Sと表示される */
Rrk_print( \=m2.c \, \=m2.s \n ) /* m2.c=M2_C, m2.s=M2_Sと表示される */

RrkStr_set( m2.s, "Hello" ) /* B */
Rrk_print( \=m1.c \, \=m1.s \n ) /* m1.c=M2_C, m1.s=Helloと表示される */
Rrk_print( \=m2.c \, \=m2.s \n ) /* m2.c=M2_C, m2.s=Helloと表示される */

コピー用関数MyStruct_copy内では、それぞれのメンバを単純に代入しています。 この実行結果は以下のようになります。

m1.c=M2_C, m1.s=M2_S
m2.c=M2_C, m2.s=M2_S
m1.c=M2_C, m1.s=Hello
m2.c=M2_C, m2.s=Hello

上記の例では「/* A */」の地点でMyStruct_copy関数を実行した直後の確認表示では、 すべてのメンバがうまくコピーできたかのように見えます。 しかし今回の場合、MyStruct_copy関数内で行われているのは単純な文字列の代入であり、 それは文字列のコピーが行われているわけではなく単にその指し示す実体が変更されるだけの処理です。 従ってこの方法の場合、MyStruct_copyを実行後はm1.cとm2.cは同じ実体を指し示すことになります。 m1.sとm2.sも同様です。

これは意図してそのようにしたのでなければ問題です。 ここで上記の「/* B */」のようにm2.sの文字列の中身を"Hello"に書き換えたとします。 このとき(m1.sも同じ実体を示していますから)、m1.sの値も"Hello"に書き換わったように見えることになります。 そのためいわゆるディープコピーとはなっていません。

これを完全なディープコピーとするには、コピー用関数MyStruct_copy内でそれぞれのメンバを単純に代入するのではなく、 それぞれのメンバのクローン文字列を作成してそれを代入する必要があります。 文字列のクローンを作成するには標準関数RrkStr_cloneを呼び出します。 例えば以下の通りです。

import std/str

struct MyStruct {
	c conststr
	s string
}
/* 構造体MyStructのコピー用関数 */
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	/* それぞれのメンバのクローンを代入 */
	dst.c = RrkStr_clone( src.c )
	dst.s = RrkStr_clone( src.s )
}

varia m1 MyStruct = { "M1_C", "M1_S" }
varia m2 MyStruct = { "M2_C", "M2_S" }

MyStruct_copy( m1, m2 ) /* A */
Rrk_print( \=m1.c \, \=m1.s \n ) /* m1.c=M2_C, m1.s=M2_Sと表示される */
Rrk_print( \=m2.c \, \=m2.s \n ) /* m2.c=M2_C, m2.s=M2_Sと表示される */

RrkStr_set( m2.s, "Hello" ) /* B */
Rrk_print( \=m1.c \, \=m1.s \n ) /* m1.c=M2_C, m1.s=M2_Sと表示される */
Rrk_print( \=m2.c \, \=m2.s \n ) /* m2.c=M2_C, m2.s=Helloと表示される */

上記では、MyStruct_copy関数内でRrkStr_cloneを呼び出し、 src.cのクローンをdst.cへ、src.sのクローンをdst.sへそれぞれ代入しています。 ここで先ほどと同じく、上記の「/* B */」のようにm2.sの文字列の中身を"Hello"に書き換えたとします。 今度は(m1.sはm2.sと異なる実体(クローン)を指し示していますから)、m1.sの値が"Hello"に書き換わることはなく、 「M2_S」のままとなります。 そのためディープコピーとなっていることが確認できます。 上記の実行結果は以下のようになります。

m1.c=M2_C, m1.s=M2_S
m2.c=M2_C, m2.s=M2_S
m1.c=M2_C, m1.s=M2_S
m2.c=M2_C, m2.s=Hello

このように構造体のコピー用関数を実装する場合においては、 そのメンバの型にも注意しなければなりません。 int型のように単なる「=」による代入で値のコピーが行える型もあれば、 文字列型のように特別な関数(RrkStr_clone)を使わなければならない場合もあるからです。

一般にint型やreal型のような数値型や、bool型、また次のセクションで述べるenum型であれば 単なる代入でコピーが行えます。 一方、文字列型や配列や構造体の場合は単なる代入ではディープコピーとはなりません。

次に数値型やbool型や文字列など様々な型のメンバを多く持つ構造体のディープコピーを考えます。 まずは以下をご覧下さい。

import std/str

struct MyStruct {
	i1 int
	i2 int
	i3 int
	b1 bool
	b2 bool
	r1 real
	r2 real
	c1 conststr
	c2 conststr
	s1 string
	s2 string
}
/* 構造体MyStructのコピー用関数 */
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	/* 数値型やbool型のメンバのコピー */
	dst.i1 = src.i1
	dst.i2 = src.i2
	dst.i3 = src.i3
	dst.b1 = src.b1
	dst.b2 = src.b2
	dst.r1 = src.r1
	dst.r2 = src.r2

	/* 文字列のメンバのコピー */
	dst.c1 = RrkStr_clone( src.c1 )
	dst.c2 = RrkStr_clone( src.c2 )
	dst.s1 = RrkStr_clone( src.s1 )
	dst.s2 = RrkStr_clone( src.s2 )
}
function MyStruct_print( s MyStruct, label conststr ){
	Rrk_print( label \n )
	Rrk_print( \w
		"i1=" s.i1 \,
		"i2=" s.i2 \,
		"i3=" s.i3 \n
	)
	Rrk_print( \w
		"b1=" s.b1 \,
		"b2=" s.b2 \n
	)
	Rrk_print( \w
		"r1=" s.r1 \,
		"r2=" s.r2 \n
	)
	Rrk_print( \w
		"c1=" s.c1 \,
		"c2=" s.c2 \,
		"s1=" s.s1 \,
		"s2=" s.s2 \n
	)
}

varia m1 MyStruct = { 11,12,13, true,  true,  1.1, 1.2, "M1_C1","M1_C2", "M1_S1", "M1_S2" }
varia m2 MyStruct = { 21,22,23, false, false, 2.1, 2.2, "M2_C1","M2_C2", "M2_S1", "M2_S2" }

Rrk_print( "before:\n" )
MyStruct_print( m1, "m1" )
MyStruct_print( m2, "m2" )

MyStruct_copy( m1, m2 ) /* A */

Rrk_print( "after:\n" )
MyStruct_print( m1, "m1" )
MyStruct_print( m2, "m2" )

構造体のメンバの数がかなり多いためコードは長くなっていますが、上記の例では特に新しい事項はありません。 上記の実行結果は以下のようになります。

before:
m1
 i1=11, i2=12, i3=13
 b1=true, b2=true
 r1=1.100000, r2=1.199999
 c1=M1_C1, c2=M1_C2, s1=M1_S1, s2=M1_S2
m2
 i1=21, i2=22, i3=23
 b1=false, b2=false
 r1=2.100000, r2=2.200000
 c1=M2_C1, c2=M2_C2, s1=M2_S1, s2=M2_S2
after:
m1
 i1=21, i2=22, i3=23
 b1=false, b2=false
 r1=2.100000, r2=2.200000
 c1=M2_C1, c2=M2_C2, s1=M2_S1, s2=M2_S2
m2
 i1=21, i2=22, i3=23
 b1=false, b2=false
 r1=2.100000, r2=2.200000
 c1=M2_C1, c2=M2_C2, s1=M2_S1, s2=M2_S2

上記のように記述しても何も問題はないのですが、 このようなコピーにおけるメンバの直接指定をできるだけ回避するため、 Rarakuではコピーヘルパー演算子がいくつか用意されています。 コピーヘルパー演算子だけを使っても常に完全なディープコピーができるとは限らないのですが、 ある程度記述を簡略化できます。

例えば^copy_podを使うと、メンバが数値型、bool型であるすべてのメンバを一括してコピーすることができます。

正確には数値型とbool型の他、enum型や関数型も一括してコピーできます。 enum型については「列挙型とその意義」のセクションで、関数型については「関数型とfunctype文」のセクションで詳しく述べます。

また^copy_strを使うと、メンバが文字列型であるすべてのメンバを一括してコピーできます。 これらを使うと上記は以下のように記述することができます。

import std/str

struct MyStruct {
	i1 int
	i2 int
	i3 int
	b1 bool
	b2 bool
	r1 real
	r2 real
	c1 conststr
	c2 conststr
	s1 string
	s2 string
}
/* 構造体MyStructのコピー用関数 */
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	/* 数値型やbool型のメンバのコピー */
	/***
	 * 以下は
	 * dst.i1 = src.i1
	 * dst.i2 = src.i2
	 * dst.i3 = src.i3
	 * dst.b1 = src.b1
	 * dst.b2 = src.b2
	 * dst.r1 = src.r1
	 * dst.r2 = src.r2
	 * と書いたのと同じ
	 */
	dst ^copy_pod src

	/* 文字列のメンバのコピー */
	/***
	 * 以下は
	 * dst.c1 = RrkStr_clone( src.c1 )
	 * dst.c2 = RrkStr_clone( src.c2 )
	 * dst.s1 = RrkStr_clone( src.s1 )
	 * dst.s2 = RrkStr_clone( src.s2 )
	 * と書いたのと同じ
	 */
	dst ^copy_str src
}
function MyStruct_print( s MyStruct, label conststr ){
	Rrk_print( label \n )
	Rrk_print( \w
		"i1=" s.i1 \,
		"i2=" s.i2 \,
		"i3=" s.i3 \n
	)
	Rrk_print( \w
		"b1=" s.b1 \,
		"b2=" s.b2 \n
	)
	Rrk_print( \w
		"r1=" s.r1 \,
		"r2=" s.r2 \n
	)
	Rrk_print( \w
		"c1=" s.c1 \,
		"c2=" s.c2 \,
		"s1=" s.s1 \,
		"s2=" s.s2 \n
	)
}

varia m1 MyStruct = { 11,12,13, true,  true,  1.1, 1.2, "M1_C1","M1_C2", "M1_S1", "M1_S2" }
varia m2 MyStruct = { 21,22,23, false, false, 2.1, 2.2, "M2_C1","M2_C2", "M2_S1", "M2_S2" }

Rrk_print( "before:\n" )
MyStruct_print( m1, "m1" )
MyStruct_print( m2, "m2" )

MyStruct_copy( m1, m2 ) /* A */

Rrk_print( "after:\n" )
MyStruct_print( m1, "m1" )
MyStruct_print( m2, "m2" )

上記の実行結果は先ほどと全く同じになります。

上記のMyStruct_copy内において、「m1^copy_pod m2」とある部分では、構造体の中で数値型、bool型のメンバを抽出し、 その各メンバxについて「m1.x = m2.x」という処理を実行します。 この演算子により、数値型とbool型のメンバに限っては、「m1.x = m2.x」という記述が不要になります。

また、上記のMyStruct_copy内において、「m1^copy_str m2」とある部分では、構造体の中で文字列型のメンバを抽出し、 その各メンバsについて「m1.s = RrkStr_clone(m2.s)」に相当する処理を実行します。 この演算子により、文字列型のメンバに限っては、「m1.s = RrkStr_clone(m2.s)」という記述が不要になります。

例えばMyStructに新しいメンバを追加した場合でも、そのメンバがこれらの演算子がサポートする型であれば、 MyStruct_copy内を修正する必要がありません。

コピーヘルパー演算子においても代入演算子と同様の用語を使う場合があります。 例えば「^copy_pod」の左側の式(この例ではm1)を左辺値、右側の式(この例ではm2)を右辺値などと呼称します。 代入演算子「=」を使った単なる構造体の代入では、代入先と代入元の構造体の型が完全に一致していなくても キャスト演算子を用いて代入可能な特別なルールがありましたが、似たようなルールがコピーヘルパー演算子「^copy_pod」等においても存在します。

コピーヘルパー演算子では、左辺値の構造体のメンバの型の並びが右辺値の構造体のメンバの型の並びに完全に含まれており、 かつその出だしの並びも一致している場合、これを使用することができます(この点は「=」の場合と同じです)。 またこの条件を逆にした、すなわち左辺値と右辺値を入れ替えた場合でも使用が許可されます(この点は「=」の場合と異なります)。 例えば以下をご覧ください。

struct Struct1 {
	i   int
	r   real
}
struct Struct2 {
	ii  int
	rr  real
	uu  uint
	c   conststr
}

varia s1 Struct1 = { 10, 20.5, }
varia s2 Struct2 = { 30, 40.5, 5u, "hello" }

s1^copy_pod s2 /* OK */
s2^copy_pod s1 /* OK */

上記においてStruct1のメンバの型の並びとStruct2のメンバの型の並びを比較しますと、 Struct1の方では int, real、Struct2の方では、int real uint conststr となっており、 Struct1の全序列(int real)が Struct2 の開始の序列と一致しています。

このような型の組み合わせの場合、「s1^copy_pod s2」やそれを逆にした「s2^copy_pod s1」という記述が許可されます。 このときs1とs2のメンバの個数のうち小さい方の個数分、それぞれのメンバが(上から順に)コピーされるといった処理が行われます。

例えば「s1^copy_pod s2」では、s1のメンバの個数は2個、s2のメンバの個数は3個ですので、最初の2個分のメンバだけがコピーされます。 つまり、「s1.i = s2.ii」と「s1.r = s2.rr」がまとめて行われます (残りのs2.uu s2.cについては単に無視されます)。

また逆に「s2^copy_pod s1」では、s2のメンバの個数は3個、s1のメンバの個数は2個ですので、こちらもまた最初の2個分のメンバだけがコピーされます。 つまり、「s2.ii = s1.i」と「s2.rr = s1.r」がまとめて行われます (残りのs2.uu s2.cについては単に無視されます)。

上記の実行結果は以下のようになります。

before
s1.i=10, s1.r=20.499999
s2.ii=30, s2.rr=40.499999, s2.uu=5, s2.c=hello
after
s1.i=30, s1.r=40.499999
s2.ii=30, s2.rr=40.499999, s2.uu=5, s2.c=hello

上記の例で「s2^copy_pod s1」ではなく「s2^copy_str s1」とした場合、 「s1.i = s2.ii」と「s1.r = s2.rr」はどちらも文字列型のメンバに関するものではありませんから、 結局何も行われないことになります。

メンバが構造体であるような構造体のコピー


ここまで扱ってきた構造体のコピーは、メンバとして構造体を含まないものでした。 次にメンバとして構造体を含むような構造体のコピーを考えましょう。

コピーヘルパー演算子「^copy_pod」や「^copy_str」を使ったコピーでは、オペランドとなる構造体直下のメンバのみがコピー対象となります。 メンバとして構造体があり、その構造体の中にint型のメンバ等があった場合、 「^copy_pod」はそれを再帰的にコピーするといったことは行いません。

一般にメンバとして構造体を持つような複雑な構造体のコピーを実装する場合、 メンバとなり得るそれぞれの構造体毎にコピー用関数を用意し、それらについて適切なコピー用関数を呼び出す必要があります。 例えば以下をご覧ください。

struct OtherStruct {
	c conststr
}
function OtherStruct_copy( varia dst OtherStruct, src OtherStruct ){
	dst ^copy_str src
}

struct MyStruct {
	i   int
	r   real
	other OtherStruct
}
function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	dst ^copy_pod src
	OtherStruct_copy( dst.other, src.other )
}

varia s1 MyStruct = {
	10,
	1.5,
	{
		"hello",
	},
}
varia s2 MyStruct = {
	20,
	2.5,
	{
		"world",
	},
}

Rrk_print( "before\n" )
Rrk_print( \=s1.i \,  \=s1.r \,  \=s1.other.c \n )
Rrk_print( \=s2.i \,  \=s2.r \,  \=s2.other.c \n )

MyStruct_copy( s1, s2 )

Rrk_print( "after\n" )
Rrk_print( \=s1.i \,  \=s1.r \,  \=s1.other.c \n )
Rrk_print( \=s2.i \,  \=s2.r \,  \=s2.other.c \n )

上記で行いたいのは最終的にはMyStructのコピーであり、 そのためのコピー用関数MyStruct_copyを実装したいわけですが、 MyStructの中には別の構造体OtherStruct型のメンバotherが含まれます。 そのため、事前準備として構造体OtherStructのコピー用関数OtherStruct_copyの実装もしておく必要があるわけです。 尚、OtherStruct_copyの実装では演算子「^copy_str」を、MyStruct_copyの実装では演算子「^copy_pod」を用いています。 上記の実行結果は以下のようになります。

before
s1.i=10, s1.r=1.500000, s1.other.c=hello
s2.i=20, s2.r=2.500000, s2.other.c=world
after
s1.i=20, s1.r=2.500000, s1.other.c=world
s2.i=20, s2.r=2.500000, s2.other.c=world

では自己参照構造体の場合はどうでしょうか? このような構造体のコピーを実装する場合、再帰的に構造体メンバへ入り込みながらコピーを行う過程において、 無限にコピーされるのを防ぐための何らかの措置を講じなければなりません。 ただしこれを実装するためには、まだ説明していない機能をいくつか使わなければなりませんので 「nullableとnotnull」セクションにおいて改めて述べます。

構造体のまとめ


構造体をvaria文等で宣言して使う場合において、初期値を省略した場合はデフォルトで{}が指定されたものとみなされます。 構造体の中に構造体のメンバがある場合、そのメンバにおいてもデフォルトでは{}が指定されたものとみなされます。 構造体のメンバにアクセスする場合は「.」演算子を使います。

構造体の定義はstruct文で行いますが、その中でデフォルトの初期値を指定することができます。 また自分自身と同じ型を持つ構造体メンバを含ませることもできます。

「=」演算子による構造体の単なる代入はディープコピーとは異なります。 ディープコピーを行いたい場合は構造体毎にコピー用の関数を用意し、それを呼び出すようにしましょう。 その際、コピーヘルパー演算子「^copy_pod」や「^copy_str」を使うといくらか記述を省略でき便利です。

目次に戻る

nullableとnotnull

nullable型の性質


nullや型名の後ろに「^?」を付記する記法についてはこれまでも時々出現しましたが、 このセクションでは改めてそれについて詳しく説明することにします。

まずnullとは、一般にはそれが無効であることを示す値であり、しかも他のどの値とも一致しないものとなります。 このような値を導入することで、例えば関数の処理が失敗したことを(bool型を用いずに)示したり、 その値がまだ未初期化であることを明示したりすることができます。 Rarakuではすべての型の変数にnullを代入できるわけではなく、例えば文字列や配列、構造体などの変数にnullを代入可能です。

一方、nullは通常の値とは異なるため、例えば配列がnullである場合はその要素へのアクセスができませんし、 構造体がnullである場合はそのメンバへのアクセスができません。 そのため、そのような状況に至った場合はそれを単に無視して何もしないか、 何らかのデフォルト値を自動的に割り当てるか、 エラーを表示して処理をそこで停止するなどの措置を講じなければなりません。

ただし、エラーを表示して停止する以外の方法は、if文等でそれを切り分けてそれを無視することを明示した形ではない限り、 コード上は問題なくそのアクセスが行われたように見えることが問題です。 つまり、そのコードの作成者の意図しない状態のままその後の処理がどんどん進み、 後になって(原因の掴みにくい)バグという形で現れることがあります。 そこで、一般的にはエラーを(nullに遭遇した時点で)表示して処理をそこで停止する方が望ましいと言えます。

ただし文字列連結演算子「&=」の左辺にはnullableな文字列(string^?)を指定することが可能です。 このとき、左辺がnullでなければ通常の文字列の連結と同じ処理が行われ、nullであれば単に無視して何も行われません。 これはエラーログ用文字列がnullでなければそこへ新しい文字列を次々に連結し、 nullであれば何も行わないといった処理が、実際には非常に頻繁に行われるであろうことに配慮した仕様です。

とはいえ、変数の値の中身がnullであるか否かは、一般にはコンパイル時に把握することは難しく、 実行時にならないとわかりません。そのため、このエラーは(コンパイルエラーとして表示することが可能な場合もありますが)、 一般的にはランタイムエラー(実行時エラー)という形で出現します。 つまりテストで実際にその部分が実行されない限り、潜在的なエラーを発見しにくいといった問題があります。

そこで変数の値ではなく型でnullが含まれる可能性を管理するといった手法があります。 型ならばコンパイル時にチェックすることができますので、 この種のエラーを事前にコンパイルエラーとして表示することがはるかに容易になります。

Rarakuでは、nullが含まれる可能性のある型をnullable、nullが含まれる可能性のない型をnotnullと呼び、 基本的にはこれらを別の型として区別します。 これまでに登場した通常の型はnotnullです。 一方、その型名の後ろに「^?」を付記したものはnullableとなります。 例えばstring型はnotnullですが、string^?型はnullableとなります。

notnullな変数/定数をnullで初期化/再代入することはコンパイルの時点で許可されません。 またnotnullな変数/定数をnullableな変数/定数で初期化/再代入することも同じく許可されません。 この規則により、notnullな変数/定数にnullが含まれる可能性を通常は排除できることになります。 一方、nullableな文字列や配列や構造体の変数/定数においては、その要素やメンバへのアクセスが コンパイルの時点で許可されません。 この規則により、nullに関するランタイムエラーが発生する可能性を通常は排除できることになります。

しかし、これらの規則を完全に徹底することができれば理想なのですが、実際のプログラミングはそう単純ではありません。 多くの場合、notnullとnullableを相互に変換したい場合が生じます。 nullableはnotnullでとり得る値にnullを追加したものですので、notnull変数をnullable変数へ代入するのは(一部の特殊な状況を除き)通常は許可されます。 しかしその逆、すなわちnullable変数をnotnull変数に代入することはそのままではできません。 これを可能にするためには、様々な構文においてnullable変数をnotnull変数に変換する必要がありますが、 この変換をRarakuではnotnull化と呼びます。

notnull化は一般的な用語ではありませんが、以降の説明ではこの用語を使用するため、ここで定義は押さえておく必要はあります。 尚、一般的なプログラミング言語ではこのようなnullable変数は構造体のようなものに包んで実装されることも多いため、 notnull化のことをunwrapと呼ぶこともありますが、Rarakuのnullableは何かに包んで実装しているわけではないため、 そのような呼び方は避けています。
notnull変数はnullable変数へ代入することが可能と述べましたが、 例外として関数におけるrefer指定された仮引数や、配列の要素、後のセクションで述べるrefer文による宣言の初期化では、 代入先と代入元のnotnull/nullableが完全に一致していなければなりません。

以降の項ではこのnotnull化を行う方法について解説します。

ifによるnotnull化

notnull化する最も基本的な方法は、if文を用いることです。 例えば以下のようにnullableなstr1変数があった場合、if文を使うとそれをある範囲においてnotnull化できます。

varia str1 string^? = null /* str1 is nullable */
varia str2 string          /* str2 is notnull */
if str1 {
	/* str1 is notnull in this block */
	str2 = str1 /* OK */
}
//str2 = str1 /* Compile error: str1 is nullable */

上記のif文で「if str1 {」とある部分は、str1の値がnullではない場合はtrue、nullである場合はfalseとなります。 従ってこれがtrueのとき、ifブロックの内部においてはstr1の値は必ずnullではない(非nullである)ことが保証されるため、 Rarakuはこのブロック内の範囲において、str1を一時的にnotnull化します。 notnull化されたstr1は、このブロック内の範囲においてstr2への代入が可能となります。 ここで「可能」とは、コンパイルの段階においてそれがコンパイルエラーとはならないということです。 尚、ifブロックを出た後は、str1は再びnullableに戻ります。

「if str1 {」と書く替わりに「if str1 != null {」と書いてもif文の条件式の評価自体は同じ結果になります。 しかし後者のように記述した場合(前者と異なり)str1のnotnull化は行われません。 ifでnotnull化するには、その条件式に変数の識別子を単独で指定する必要があり、それ以外の指定ではnotnull化は行われません。

では「if str1 != null {」のような書き方を全くする必要がないかと言うとそうでもなく、 例えば以下のように「str1がnullではない場合にnullを代入する」といった処理を記述する場合、 むしろ「if str1 != null {」の方を使う必要があります。

varia str1 string^? = "hello" /* str1 is nullable */
if str1 != null {
	/* str1 is nullable */
	str1 = null /* OK */
	/* other processing */
}
if str1 {
	/* str1 is notnull in this block */
	//str1 = null /* Compile error: assign nullable to notnull */
	/* other processing */
}

上記ではnullはnullableとみなされますので、str1にnullを代入したいならばstr1をnullableのままにしておく必要があります。 「if str1 != null {」による判定では、ifブロックの内部においてもstr1はnullableのまま維持されますが、 一方で「if str1 {」による判定では、ifブロックの内部においてstr1はnotnull化されますので、nullを代入することができないことになります。

もっとも上記の例で単にstr1にnullを代入することだけが目的(/* other processing */ の部分に他のコードが何もない)なら、 (そもそもif文を使わずに)単に「str1 = null」とだけ書けば済む話ではあります。 /* other processing */ の部分に何かある場合でも、例えば以下のように条件式の部分に代わりに三項演算子を使って明示的にbool型へ変換するなど、 他の手段がないわけではありません。

varia str1 string^? = "hello" /* str1 is nullable */
if str1 ? true : false {
	/* str1 is nullable */
	str1 = null /* OK */
	/* other processing */
}

言語仕様の背景: 「!=null」の扱いについて
既に述べたように現在のRarakuでは「!=null」を伴う条件式ではnotnull化は行われません。 また現在のRarakuでは「varia i int^? = null」のようにint型に「^?」演算子を付加することはできません。

ただしこれはRarakuの将来のバージョンでサポートされる可能性はあります。 その場合、「if i {」という判定では i の値がnullではない場合と0ではない場合の両方を含意してしまうため、 i が0である場合にif文のブロック内が実行されないという状況が発生してしまいます。 かといって「if i || i==0 {」という判定(条件式が論理和演算子「||」を伴い、i に対して否定演算子も使われていない場合)では、 すぐ後で述べますがnotnull化が行われません。 もしも「int^?」という宣言が可能になったならば、 0 である場合でもこの条件式を真にするために「if i != null {」という判定でも notnull化を行えるようにすべきでしょう。

Close


一度に複数の変数をnotnull化することもできます。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
if s && t {
	/* s and t is notnull */
}

上記のようにsとtを単独で指定し、しかもそれを「&&」で連結して繋いだ場合、これらを両方ともにnotnull化できます。

この記述は「&&」だけを使うならもう少し一般化した形で行うこともできます。 例えば下記のように「&&」だけで様々な条件式を連結した形では、単独の識別子として指定したものだけがnotnull化されます。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if s && t != null && u {
	/* s is notnull */
	/* t is nullable */
	/* u is notnull */
}
if s != null && t && u != null {
	/* s is nullable */
	/* t is notnull */
	/* u is nullable */
}

上記の「if s && t != null && u {」では、 s と u のみが(このifブロック内部において)notnull化され、t はnullableのままとなります。 また上記の「if s != null && t && u != null {」では、t のみが(このifブロック内部において)notnull化され、s と u はnullableのままとなります。

「&&」と「||」が混じった条件式を指定する場合、それが全体の大枠として「&&」だけで連結された式でない場合はnotnull化は行われません。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if s && t || u {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
}

上記は論理演算子の優先順位を考えた場合、 「s && t」と「u」が「||」で連結された式となります。 つまり全体の大枠を考えた場合、これは「&&」で連結された式ではなく「||」で連結された式となりますのでnotnull化は行われず、 s、t、uは(単独の識別子で指定されてはいますが)すべてnullableのままとなります。

実際、上記のような状況では、u が非nullでさえあれば、sやtが何であってもifブロックの内部は実行されますし、 逆に sとtが両方とも非nullでさえあれば、uが何であってもifブロック内部が実行されます。 つまり s、t、u のいずれもnullではないと断定するのが(コンパイルの段階では)困難な状況であり、 このような場合はnotnull化は行われません。

もう一つ似たような例を見てみましょう。 以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if s && ( t || u ) {
	/* s is notnull */
	/* t is nullable */
	/* u is nullable */
}

上記もまた、「&&」と「||」が混ざって指定されていますが、先ほどの例とは異なり「t || u」には括弧がついているため、 「s」と「t || u」が「&&」で連結された式となります。 つまり全体の大枠を考えた場合、これは「&&」で連結された式となり、 sについては単独の識別子で指定されてはいますのでnotnull化されます (それ以外の t と u はnullableのままとなります)。

構文解析木についてご存知の方は次のように考えて構いません。 この仕様をもう少し厳密に言えば、Rarakuのif文の条件式の構文解析木を根から子へ向かって走査したとき、 二項演算子「&&」にあたるノードを検出した場合はその子ノードへ走査をさらに進め、 変数識別子が単独で存在するような葉ノードを検出した場合はそれをnotnull化します。 一方、二項演算子「||」にあたるノードを検出した場合は(それ以上は)その子ノードへの走査を行わずスキップします。

elseによるnotnull化


ここまではifブロック内をnotnull化するためにif文の条件式において単独の変数識別子を「&&」で連結して指定する方法を見てきました。 しかし場合によってはifブロック内ではなく、それが対応するelseブロック内でnotnull化して欲しいこともあります。 この場合、単独の変数識別子の直前に否定演算子を一つ付けたものを指定します。 例えば以下をご覧下さい。

varia str1 string^? = null /* str1 is nullable */
if !str1 {
	/* str1 is nullable in this block */
} else {
	/* str1 is notnull in this block */
}

上記ではif文の条件式として(これまでとは異なり)「!str1」を指定しています。 この場合、ifブロックの内部ではstr1はnullableのままですが、一方でelseブロックの内部ではstr1はnotnull化されます。

この「単独の変数識別子に否定演算子を付けたもの」は、「||」で連結して複数同時に指定することもできます。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
if !s || !t {
	/* s and t is nullable */
} else {
	/* s and t is notnull */
}

上記のように「!s」と「!t」を指定し、しかもそれを「||」で連結して繋いだ場合、 elseブロックにおいてこれらを両方ともにnotnull化できます。

これはド・モルガンの法則を考えれば、ifブロックでnotnull化したい場合の条件から自然に導出される規則となります。 ifブロックでnotnull化したい場合は、「A && B && C」といったような条件式を指定しましたが、 これにド・モルガンの法則を適用して否定を取れば、「!A || !B || !C」といったような条件式になります。

この記述は「||」だけを使うならもう少し一般化した形で行うこともできます。 例えば下記のように「||」だけで様々な条件式を連結した形では、「単独の変数識別子に否定演算子を付けたもの」だけがelseブロックにおいてnotnull化されます。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if !s || t == null || !u {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
} else {
	/* s is notnull */
	/* t is nullable */
	/* u is notnull */
}

上記の「if !s || t == null || u {」では、 s と u のみが(このelseブロック内部において)notnull化され、t はnullableのままとなります。

「&&」と「||」が混じった条件式を指定する場合、それが全体の大枠として「||」だけで連結された式でない場合は(elseブロックでの)notnull化は行われません。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if !s && ( !t || !u ) {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
} else {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
}

上記は「&&」と「||」が混ざって指定されていますが、「t || u」には括弧がついているため、 「s」と「t || u」が「&&」で連結された式となります。 つまり全体の大枠を考えた場合、これは「&&」で連結された式となりますので(elseブロックにおいて)notnull化は行われず、 s、t、uは(単独の識別子に否定演算子が指定された形ではありますが)すべてnullableのままとなります。

もう一つ似たような例を見てみましょう。 以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if !s && !t || !u {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
} else {
	/* s is nullable */
	/* t is nullable */
	/* u is notnull */
}

上記は論理演算子の優先順位を考えた場合、 「!s && !t」と「!u」が「||」で連結された式となります。 つまり全体の大枠を考えた場合、これは「||」で連結された式となり、「!u」の部分がelseブロックにおいてnotnull化されます (!s、!tはすべてnullableのままとなります)。

実際、上記のような状況では、u がnullである状況では必ずifブロックの内部が実行されます。 これは裏を返せばelseブロックが実行される場合はuは必ず非nullになっているということです。 一方、sとtにおいては、ifブロックとelseブロックのいずれにおいてもnullではないと断定するのが(コンパイルの段階では)困難な状況であり、 notnull化は行われません。
構文解析木についてご存知の方は次のように考えて構いません。 この仕様をもう少し厳密に言えば、Rarakuのif文の条件式の構文解析木を根から子へ向かって走査したとき、 二項演算子「||」にあたるノードを検出した場合はその子ノードへ走査をさらに進め、 否定演算子と変数識別子が単独で存在するような葉ノードを検出した場合はそれをelseブロックにおいてnotnull化します(ifブロックについてはnullableのままです)。 一方、二項演算子「&&」にあたるノードを検出した場合は(それ以上は)その子ノードへの走査を行わずスキップします。

if文の後ろの範囲がnotnull化される場合


実際のプログラミングでは、事前にif文で変数の値のnullチェックをし、それがnullであればその場でreturnなどをして 以降の処理が実行されないようにする記法があります。このような記法では通常(else文がなくとも)if文の後ろの範囲がnotnull化されます。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

function func()
{
	varia s = createInfo()
	if !s {
		/* s is nullable */
		Rrk_print( "error: s is null\n" )
		return /* return statement */
	}
	/* s is notnull after if { } */

	Rrk_print( \=s.m \n )
}
func()

上記では、createInfo()の戻り値をsに格納しています。 s の宣言では型が記述されていませんが、この場合関数の戻り値の型から「Info^?」型であると型推論されます。 次に、「if !s {」において事前にsの値がnullか否かを調べています。 s がnullであればifブロック内が実行されますが、このifブロック内部にはreturn文があるため、 (ひとたびifブロックに入ったなら)もうif文の後ろが実行されることはありません。 つまりこのif文の後ろの範囲においては s は必ず非nullとなります。 Rarakuではこのような場合、このif文の後ろの範囲において s を自動的にnotnull化します。

この「if文の後ろの範囲」とは、このif文の直後から、このifを直接含む親ブロックが終了するまでの範囲を意味します。 上記の例では、if文の直後から「Rrk_print( \=s.m \n )」の実行が終了する位置までの範囲となります。

また厳密に言えば、この親ブロックにgoto文におけるラベルやsetjmp文(これらはまだ紹介していませんが)が存在する場合、 このようなnotnull化は行われません。goto文については「goto文とラベル文」、setjmp文については「大域脱出とtry文」のセクションで詳しく述べます。
上記の状況でさらにelse文がついていた場合、elseブロックとelse文の後ろの両方の範囲において s がnotnull化されます。

return文がifブロックの直下ではない場合、通常はこのようなnotnull化は行われません。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

function func()
{
	varia s = createInfo()
	if !s {
		/* s is nullable */
		Rrk_print( "error: s is null\n" )
		while !s {
			return /* return statement */
		}
	}
	/* s is nullable after if { } */
}
func()

上記の(whileブロックの中にある)return文は丁寧に意味を追えば(ifブロックに入りさえすれば)確実に実行されることはわかりますが、 これはifブロックの直下に存在するわけではないため、Rarakuではそこまでの探索は行われません。 そのため、if文の後ろの範囲においては自動的にはnotnull化されません。 ただしこれには例外があり、return文がたかだか無名ブロックだけで囲まれているならば、 このようなnotnull化が行われる場合があります。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

function func()
{
	varia s = createInfo()
	if !s {
		/* s is nullable */
		Rrk_print( "error: s is null\n" )
		{ /* anonymous block */
			return /* return statement */
		}
	}
	/* s is notnull after if { } */

	Rrk_print( \=s.m \n )
}
func()

上記では、return文はif文の直下にはないものの、無名ブロックだけで囲まれた状態です。 この場合、Rarakuではその無名ブロックの内部まで探索を行い、そこにreturn文があるなら if文の後ろの範囲において自動的にnotnull化します。

ただし以下のようにreturn文が無名ブロック以外のブロックに一つでも含まれる場合は、 探索はそこで打ち切られます。

if !s {
	{ /* anonymous block */

		for i:=0; i<3; ++i { /* for block ( it is not an anonymous block ) */

			{ /* anonymous block */
				return /* return statement */
			}
		}
	}
}

尚、無名ブロック以外に、dowhile文のブロックでもこのような探索が行われます。 その場合は上記の説明における「無名ブロック」の部分を「dowhile文のブロック」と読み替えて考えてください。

ループ文の中におけるifブロック直下においてbreakやcontinueが含まれている場合でも return文の場合と同様のnotnull化が行われる場合があります。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

varia count = 3
while( count ){
	varia s = createInfo()
	if !s {
		/* s is nullable */
		Rrk_print( "error: s is null\n" )
		break /* break statement */
	}
	/* s is notnull after if { } */
	Rrk_print( \=s.m \n )

	--count;
}

上記では「if !s {」に対応するifブロックの直下にbreak文があり、 この場合もRarakuではif文の後の範囲において s をnotnull化します。

その他、上記のような状況で、if文の中にfallthrough文(「switch文」のセクションで説明します)や、 final notnull文(すぐ後で述べます)がある場合でもifブロックの後ろがnotnull化される場合があります。

(elif文のない)if-else文において、elseブロック内にreturn文等があった場合にelse文の後ろの範囲がnotnull化される場合があります。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

function func()
{
	varia s = createInfo()
	if s {
		/* s is notnull */
	} else {
		return /* return statement */
	}
	/* s is notnull after else { } */

	Rrk_print( \=s.m \n )
}
func()

今回はif文の条件式には「!s」ではなく「s」が指定されています。 この「if s {」がtrueになるとき、このifブロック内では s は必ず非nullですので、 逆にelseにおいてはsは必ずnullになります。またelseへ分岐した場合はreturn文によりelse文以降が実行されることはありません。 else文以降の範囲において s がnullになる可能性はないことになりますが、 この場合Rarakuはその範囲において s をnotnull化します。

尚、elseブロック内にreturn文等があっても、その上にelifブロックが存在する場合は、この種の自動的なnotnull化は行われません。 一方、ifブロック内にreturn文等がある場合は、その下のelifブロックの有無に関係なく、この種のnotnull化が行われます。

elif文を伴う場合のnotnull化


elif文を伴う複雑なケースについても見ておきましょう。 以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if s && t {
	/* s is notnull */
	/* t is notnull */
	/* u is nullable */
} elif t && u {
	/* s is nullable */
	/* t is notnull */
	/* u is notnull */
} else {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
}

基本的にはこれまでと同様に考えることができます。 elif文の条件式に単独の変数識別子を指定した場合は、そのelifブロックの内部においてその変数がnotnull化されます。 上記ではelifブロック内において、tとuがnotnull化されています。

また以下は否定演算子と「||」を伴うケースになります (この場合notnull化される範囲が少し複雑です)。

struct Info {
	m int
}
varia s Info^? = { 10 } /* s is nullable */
varia t Info^? = { 20 } /* t is nullable */
varia u Info^? = { 30 } /* u is nullable */
if !s || !t {
	/* s is nullable */
	/* t is nullable */
	/* u is nullable */
} elif !t || !u {
	/* s is notnull ( by if ) */
	/* t is notnull ( by if ) */
	/* u is nullable */
} elif false {
	/* s is notnull */
	/* t is notnull */
	/* u is notnull ( by previous elif ) */
} else {
	/* s is notnull */
	/* t is notnull */
	/* u is notnull */
}

上記では「if !s || !t {」により、if文より後のelifブロック内とelseブロック内の両方において、sとtがnotnull化されます。 また「elif !t || !u {」においても同様に、それより後のelifブロック内とelseブロック内の両方において、tとuがnotnull化されます。 注意していただきたいのは、この「elif !t || !u {」におけるelifブロック内では、u はまだnotnull化されないということです。 s と t については既にif文の条件式の効果が効いていますので、その効果によって(このブロック内で)notnull化されています (この t のnotnull化は「elif !t || !u {」に由来するものではありません)。

結局上記の状況では t については既にif文の条件式の効果が効いていますので、 実際にはelifでわざわざ「!t」を指定する意味はありませんが、 このように重複して指定してもエラーとまではなりません。

while文とfor文によるnotnull化


if文以外にも、始めに条件式を伴うようなループ文のブロック内部において、if文の場合と同様のnotnull化が行われる場合があります。 例えば以下はwhile文を用いた例です。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

varia s = createInfo()
while s {
	/* s is notnull in while */
	Rrk_print( \=s.m \n )
	break
}

上記では「while s {」に対応するwhileブロック内において、s はnotnull化されます。

dowhile文についてはブロックに入る前に条件式が評価されるわけではないため、 このようなnotnull化は行われません。

for文についても見ておきましょう。 以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

for s := createInfo(); s ; {
	/* s is notnull in for */
	Rrk_print( \=s.m \n )
	break
}

上記ではfor文の条件部に単に s が指定されているため、対応するforブロック内において、s はnotnull化されます。

ループ文の条件式においても、単に「s」と指定するのと「s != null」と指定するのとでは、 if文のときと同じ違いがあります(すなわち後者の場合、notnull化はされません)。

一方、否定演算子と単独の識別子を組み合わせた記法(「!s」のような指定)は、 ループ文の条件式では(notnull化に関して)特に何の効果もありません (通常の条件式としての評価のみが行われます)。 これはelseに相当するような処理の分岐がループ文では表面上は存在しないからです。

if文の条件式で変数宣言も同時に行う特殊記法


if文のブロック内部だけで使用するようなnullable変数を、そのif文でチェックするような状況を考えます。 まずは以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

varia s = createInfo()
if s {
	/* s is notnull */
	Rrk_print( \=s.m \n )
}
/* s is not used any longer */

上記において、s が必要になるのは直後のif文のブロック内部だけであるものとします。 ただこのように書いてしまうと、if文を抜けた後はもう s を使うことはないにも関わらず s のスコープはその後も続いてしまうことになります。

ではこの s のスコープを「実際に使用される範囲(if文のブロック内部だけ)」に限定して書けないものでしょうか? 一応以下のように一旦無名ブロックを作ればそれを実現できますが、このようにするとインデントが一段深くなってしまいます。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

{
	varia s = createInfo()
	if s {
		/* s is notnull */
		Rrk_print( \=s.m \n )
	}
}
/* cannot access to s */

実はRarakuのif文には、まだ紹介していない別の書式が存在します。 それは条件式の部分で変数宣言を同時に行えるといったもので、その書式は「if 識別子 := 初期化式 ; 条件式」となります。 この書式を使うと、上記と全く同じ処理を以下のように記述できます。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

if s := createInfo(); s {
	/* s is notnull */
	Rrk_print( \=s.m \n )
}
/* cannot access to s */

if文の条件式の部分の中にいきなり「s := createInfo()」という文が入り込んでいますが、 この部分において変数 s を宣言しています。その直後「;」を挟んで条件式が続きます。 if文が実際に評価するのはこの後半の条件式になります。またこの書式においては中間の「;」は必須となります。 尚、条件式では単独の変数識別子 s が指定されていますので、この s は(このifブロック内において)notnull化されることになります。

for文の初期化文の中においてもこのような「:=」を使った記述が可能でしたが、if文における「:=」もあれと同様です。 尚、s の型は createInfo の戻り値が Info^? であることから自動的に型推論されるため、記述不要です (というより「s」と「:=」の間にはそれは記述できません。明示的に型を指定したい場合は右辺をその型にキャストし、型推論させます)。
この書式はgo言語等で導入されているif文の書式と似ています。 またこの記法を使う場合は以下のように全体を括弧で囲う(「if(」と「){」で囲う)ことはできません。

if( s := createInfo(); s ){ /* NG: compile error */
}

上の例は一つ前の例と全く同じですから、s のスコープは「/* cannot access to s */」とある部分の直前にあるブロックの終わりまでです。

ちなみに上記にもしもelse文が付属している場合は、s のスコープはif側のブロックから始まり、else側のブロックまでとなります (else側のブロックからでもこの s が使用可能です)。 else側のブロック終了後は s のスコープも終了します。

同様の記述をelif文に書くことも可能です。 例えば以下の通りです(あまり面白くない例ですが)。

if     x := 10; x {
	Rrk_print( "if:"   \=x \n )
} elif y := 20; y {
	Rrk_print( "elif:" \=x \, \=y \n )
} else {
	Rrk_print( "else:" \=x \, \=y \n )
}

上記ではif文でまず変数xが宣言され、次にelif文で変数yが宣言されます。 このときelif文やelse文のブロック内では変数xとyの両方にアクセスできます。 一方、if文のブロック内では変数yにはアクセスできないことに注意してください。 if文のブロックの時点ではまだ変数yは宣言されていないからです。 これは結局以下のように書いたのと結果的には同じです。

{
	varia x = 10
	if x {
		Rrk_print( "if:"   \=x \n )
	} else {
		varia y = 20
		if y {
			Rrk_print( "elif:" \=x \, \=y \n )
		} else {
			Rrk_print( "else:" \=x \, \=y \n )
		}
	}
}

実際のRarakuコンパイラの内部処理では、elif文が追加されるたびにブロック階層を深くしているわけではなく、 一番外側のブロックとif-elif-else自身のブロックだけで上記と同じスコープをうまくエミュレートできるように解析を行っています。

では、この書式を用いてif文とelif文で同じ名前の変数を宣言した場合、何が起きるのでしょうか? 例えば次をご覧ください。

if     x := 10; false {
	Rrk_print( "if:"   \=x \n )
} elif x := "hello"; true {
	Rrk_print( "elif:" \=x \n )
} else {
	Rrk_print( "else:" \=x \n )
}

上記ではif文、elif文ともにxが宣言されています。 Rarakuでは上記のような場合、最初(if文の「:=」の左辺)に現れたxは右辺が10ですのでint型で宣言、 2度目(elif文の「:=」の左辺)に現れたxは右辺が"hello"ですのでconststr型とみなします。 つまり、このとき上記のif文で宣言されたint型のxの有効範囲は、elif文で宣言されたconststr型のxが宣言された時点で終了し、 それ以降に出現する識別子 x は elif文で宣言された x の方が参照されます。 通常、同じブロック内では同じ名前の変数を二度以上宣言することはできませんが、if-elif文における「:=」による宣言では このような挙動が許可されます。

このような同じブロック内での同じ変数名の二度以上の宣言はシャドウイングと呼ばれます。 尚、プログラミング言語によっては違うブロックで同じ変数名を付けて宣言することをシャドウイングと呼ぶ場合もありますが、 Rarakuではそれは当たり前にできることなので、ここでは「同じブロック内で」という条件を付けるものとします。

従って上記の実行結果は「elif: x=hello」と表示されます。 これは以下のように書いたのと同じ結果になります。

{
	varia x = 10
	if false {
		Rrk_print( "if:"   \=x \n )
	} else {
		varia x = "hello"
		if true {
			Rrk_print( "elif:" \=x \ \n )
		} else {
			Rrk_print( "else:" \=x \ \n )
		}
	}
}

もっとも上記はやや極端な例ではあります。 上記のように二つのxにあまり脈絡がない場合、通常は変数名そのものを別にした方がよいでしょう。

しかし一方で、これを同じ名前にした方がおそらく望ましい場合もあります。 例えば整数と文字列をそれぞれ包含するような構造体PrimIntとPrimStrを考え、 それを返すgetPrimIntとgetPrimStrを右辺として指定するような状況を考えます。 例えば以下の通りです。

struct PrimInt {
	val int
}
struct PrimStr {
	val string
}
function getPrimInt() PrimInt^? {
	varia u PrimInt
	u.val = 10
	return u
}
function getPrimStr() PrimStr^? {
	varia u PrimStr
	u.val = "world"
	return u
}

if     ui := getPrimInt(); ui {
	Rrk_print( "The content of PrimInt:" ui.val \n )
} elif us := getPrimStr(); us {
	/* この中でもuiを参照できてしまう */
	Rrk_print( "The content of PrimStr:" us.val \n )
} else {
	Rrk_print( "else:" \n )
}

上記ではif文の「ui := getPrimInt();」における ui はPrimInt^?型、 elif文の「us := getPrimStr();」における us はPrimStrt^?型です。 このような状況で名前を ui、usのように別々にしますと、単純にこの型の種類が増えてさらにelif文を追加するとなった場合に もう一つ別の名前を付けるのが面倒です。

またifブロックでの「Rrk_print( "The content of PrimInt:" ui.val \n )」をコピーして直下のelifブロックへペーストするとき、 ui を us へ修正し忘れるといったシナリオは普通に想定できます。 しかもその場合でもコンパイルエラーは発生しません。 uiはelifブロックの中でも依然として有効であり、普通に参照できてしまうためです。

従って、このようなケースでは大抵の場合、(型は異なるとはいえ)敢えて u という名前で統一して書いた方が望ましいでしょう。

struct PrimInt {
	val int
}
struct PrimStr {
	val string
}
function getPrimInt() PrimInt^? {
	varia u PrimInt
	u.val = 10
	return u
}
function getPrimStr() PrimStr^? {
	varia u PrimStr
	u.val = "world"
	return u
}

if     u := getPrimInt(); u {
	Rrk_print( "The content of PrimInt:" u.val \n )
} elif u := getPrimStr(); u {
	Rrk_print( "The content of PrimStr:" u.val \n )
} else {
	Rrk_print( "else:" \n )
}

上記ではif文の「u := getPrimInt();」における u はPrimInt^?型、 elif文の「u := getPrimStr();」における u はPrimStrt^?型です。 通常、u を PrimIntの意味で使うのは if ブロックの中だけでしょうし、 また u を PrimStrの意味で使うのは elif ブロックの中だけであると考えられますが、 上記はそのような制限が加味された記述であると言えます。

あまりないことかもしれませんが、elif文でのxの宣言の右辺にif文でのxを指定することができます。 例えば以下をご覧下さい。

if x := 10; false {
}: x := ("[" x "]"); true {
	Rrk_print( \=x \n )
}

上記では、「"[" x "]"」における x は、まだelif文の宣言自体が完了していないため、 ギリギリif文での x (int型)となります。 上記の実行結果は以下のようになります。

x=[10]


さらに「:=」によって宣言された変数の識別子が、「;」の直後にただ一つだけある場合に限り、 後ろの「条件式」の部分を省略できます(ただしその場合でも「;」自体は省略できません)。 例えば以下をご覧下さい。

struct Info {
	m int
}
function createInfo() Info^? {
	varia tmp Info = { 10 }
	return tmp
}

if s := createInfo(); {
	/* s is notnull */
	Rrk_print( \=s.m \n )
}
/* cannot access to s */

上記で「if s := createInfo(); {」とある部分は、「if s := createInfo(); s {」の省略形です。 つまりこれは条件式の位置に s が単独で指定されているのと同じであるため、この場合もifブロック内において s はnotnull化されます。

言語仕様の背景: 「;」を省略できない仕様にしている理由
Rarakuでは(C言語のように)「=」を使った代入文をif文の条件式に書くことはできませんが、 上で述べたように「:=」を使った宣言であれば、それと似たように記述することが可能です (ただし「:=」は代入ではなく新規の変数の宣言とその初期化を行う演算子となります)。

この記述において「;」も省略できる仕様にしてもよさそうに思えますが、 Rarakuではこの場合の「;」の記述は必須にしてあります。 これは例えば以下のように論理演算子「&&」や「||」をうっかり組み合わせて使った場合、 コンパイルエラーとするためです。

if x := 10 && y == 20 { /* コンパイルエラー: 「;」がない */
	/* something */
}

おそらくこのように記述した場合、x と y の論理積を取る意図での記述だと思います。 しかし実際にはこれは「10 && y == 20」の部分が「x :=」の右辺値としてまとめて解釈されてしまいます。 すなわち、この記述は以下のように明示的に括弧を付けて書いたのと同じ意味になります。

if x := ( 10 && y == 20 ); {
	/* something */
}

とはいえ「;」をつけた場合でも結局以下のように似たような記述は書けてしまいます。

if x := 10 && y == 20; {
	/* something */
}

ただ少なくとも「;」が字面上に現れない場合よりも、この方が変数宣言部の終わりが明確化され、 プログラマが条件式を省略して書いているという意図も幾分はっきりします。 そのような理由で「;」が必須となっています。

Close


配列要素へのアクセスに関するnotnull化


配列の要素へのアクセスをnotnull化したい場合を考えます。 まずは以下をご覧下さい。

struct Info {
	m int
}
varia ary Info[^?] = [ { 10 }, null ]

上記においてaryは要素がInfo構造体であるような配列です。 この場合、通常その型は「Info[]」と書くところですが、上記ではさらにaryの要素としてnullを格納できるものとしている (つまりaryの要素がnullableである)ため、「Info[]」の替わりに「Info[^?]」と記述しています。 このようにRarakuでは配列型の宣言における「[]」の中に「^?」を書くことによってその要素がnullableであることを宣言できます。

「Info[]^?」と「Info[^?]」の違いに注意してください。 前者はaryそのものがnullableであることを示し、「ary[0] = null」といった記述は不可であり、 「ary = null」といった記述のみが可能であることを意味します。 一方後者では「ary = null」といった記述は不可であり、「ary[0] = null」といった記述のみが可能です。 尚、「Info[^?]^?」と書くこともでき、 この場合は「ary = null」と「ary[0] = null」の両方が可能であることを意味します。

さてこの配列におけるary[0]をnotnull化するために以下のように書いたとします(ただしこれはうまくいきません)。

struct Info {
	m int
}
varia ary Info[^?] = [ { 10 }, null ]
if ary[0] {
	/* ary[0] is nullable */
}

上記のように記述した場合、ary[0] がnullでなければtrue、nullであればfalseといった条件式としての評価は通常通り行われるものの、 ifブロックの内部においてary[0]がnotnull化されるといったことは行われません。 これはary[0]が(if文の条件式において)単独の変数識別子としての表記になっていないためです。 そこで前項で紹介したif文の書式を用います。 以下をご覧下さい。

struct Info {
	m int
}
varia ary Info[^?] = [ { 10 }, null ]
if e := ary[0]; {
	/* e is notnull */
	Rrk_print( \=e.m \n )
}

上記ではary[0]を一旦一時変数eに格納し、それを条件式に当てることでnotnull化を行っています。 結果的にeを介したアクセスによってary[0]をnotnull化できていることになります。

構造体メンバへのアクセスに関するnotnull化


前項で見たif文における一時変数を用いたnotnull化は汎用的な方法であり、 配列要素へのアクセス以外にも例えば以下のような構造体のメンバへのアクセスでも使用できます。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2 = { { 10 } }
if e := s.n; {
	/* e is notnull */
	Rrk_print( \=e.m \n )
}

ただこのような構造体のメンバへの単純なアクセス(途中に配列要素へのアクセスなど複雑なものが混じっていないアクセス)の場合、 上記のように書かなくてもRarakuでは基本的には直接「if s.n.m {」のように記述できます (ただしこの場合 s と s.n は事前にnotnullとなっていなければなりません)。 例えば以下の通りです。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2 = { { 10 } }
if s.n {
	/* s.n is notnull */
	Rrk_print( \=s.n.m \n )
}

既に述べた通り、Rarakuでは基本的にはif文の条件式に単独の識別子が記述された場合に、その識別子がifブロック内でnotnull化されます。 ただし構造体のメンバへのアクセスする記述のうち、識別子と「.」のみから構成される表記については、特例として 単独の識別子と同様にそれをnotnull化することができます。 例えば上記では s.n がifブロック内でそのままnotnull化されています。

上記でs自身もnullableである場合、先にsをnotnull化しておく必要があります。 例えば以下の通りです。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2^? = { { 10 } } /* s is nullable */
if s && s.n { /* check s and s.n */
	/* s and s.n is notnull */
	Rrk_print( \=s.n.m \n )
}

上記では s はnullableであるため、そのままでは「s.n」といった表記自体が認められず、 if文の条件式に直接書くことができません。そのため、まず「s.n」の左隣に「&&」で連結した形で「s」をnotnull化します。 論理演算子は左から右へ順番に実行されますので、「&&」の左にある「s」がnotnull化された時点で その右にある「s.n」といった表記も可能になり、晴れて「s」と「s.n」の両方をnotnull化できるわけです。 尚、この条件式における「s」と「s.n」を逆順に指定した場合、Rarakuではコンパイルエラーとなります。

ところで、このような構造体のメンバへのアクセスのnotnull化では、通常そのメンバもnotnull化されます。 これは大抵の場合妥当な挙動ですが、木構造を走査する場合においてこれでは不便な場合があります。 例えば(少し難解ですが)次のような状況を考えます(ただしこの例はコンパイルエラーとなります)。

struct Node {
	left_  Node^? = null
	right_ Node^? = null
	key_ = ""
}

varia q Node = {}
while q.right_ {
	q = q.right_ /* Compile error */
}

上記は二分木と呼ばれるデータ構造の原型のようなものですが、 このようなデータを扱う場合、子ノードを走査するために上記のwhile文のような記述をすることがあります。 このwhile文はメンバの right_ がnullとなるまでループします。

ここで「q = q.right_」という代入に注意してください。 残念ながらこれはコンパイルエラーとなりますが、以下ではその理由について説明します。

今 q 自体は元々notnullであり、また「while q.right_ {」といった条件式によって、q のメンバである「q.right_」がnotnull化されています。 一方、q.right_自体は(繰り返しになりますが)notnull化されていますが、 そのメンバである「q.right_.right_」はnullableのままとなっています。 つまりここでこの式における左辺と右辺には、(メンバの基本的な型構成自体は同じですが)そのメンバがnullableか否かの点で違いが生じています。 模式的に書けば「notnull.notnull = notnull.nullable」といった形式の代入となっているということです。 Rarakuでは、代入元のメンバがnullableであり、代入先のメンバがnotnullである場合、 (それらの構造体のメンバの構成は同じであっても)このような代入が許可されません。

このようなやや特殊な状況の場合、通常の規則によるnotnull化ではやや制限がきつ過ぎるということになります。 そこでこのような場合、以下のように「while」キーワードの後ろに「^skimp」指定子を指定します。

struct Node {
	left_  Node^? = null
	right_ Node^? = null
	key_ = ""
}

varia q Node = {}
while^skimp q.right_ {
	q = q.right_ /* OK */
}

「^skimp」が指定された場合、whileブロック内では表面上「q.right_」と表記された場合のみそれがnotnull化されます。 逆に言えば表面上「q」としか表示されていない場合はそのメンバ「right_」はnotnull化されません。 これにより上記のような代入が(模式的に書けば「notnull.nullable = notnull.nullable」といった形式になり)許可されます。

「skimp」はあまり馴染みのない英単語かもしれませんが、「控える」とか「節約する」といった意味です。 つまりここでは「通常の制限を緩める」といったニュアンスで使っていますが、 「制限を緩める」ということは、ある種の状況において別の問題が発生する可能性があるということです。 実際、^skimpを指定した場合、以下のような記述が可能となってしまいます。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2 = { { 10 } }
varia t Info2 = { null }
if^skimp s.n {
	/* s.n is notnull */
	s = t /* danger! */
	s.n.m /* Runtime Error. s.n is null! (by s=t ) */
}

「^skimp」はif文においても指定可能であり、上記ではそれを指定しています。 上記の「s = t」という代入に注目してください。 「^skimp」を指定しない場合は(notnull.notnull = notnull.nullableという形式になり)このような代入が許可されませんが、 一方で「^skimp」を指定した場合は(notnull.nullable = notnull.nullableという形式になり)このような代入が許可されます。 つまり上記ではこれが許可されるわけですが、t のメンバをよく見てみるとnullが格納されています。 if文の条件式には s.n を指定したため、本来 s.n がnullであってはならないわけですが、 t の代入を介してこの制限を潜り抜け、s.n がnullとなってしまうわけです。 (実際はs.nがnullであるにも関わらず)ifブロック内では「s.n.m」というアクセスは許可されてしまうため、 ここでnullアクセスに関するランタイムエラーが発生することになります。

ここで言う「nullアクセスに関するランタイムエラー」とは、Javaなどで言う「Null Pointer Expception」に相当するものです。 Rarakuではこれが発生した場合、スタックバックとレースを表示してプログラムの実行を中断します。 スタックバックとレースとは、そのエラーが発生するまでにどのファイルの関数がどのように呼び出され、 どの行においてそのエラーが発生したかを表示するもので、エラー箇所を特定するのに役立ちます。

もっとも上記はかなりわざとらしい例ではあるのですが、 一応このような状況も想定されるため、通常は「^skimp」を乱用すべきではなく、 その使用は木構造を走査するような特殊な状況に限るべきです。

三項演算子によるnotnull化


三項演算子にもif文と同様に条件指定部がありますが、これについてもif文と同様にnotnull化が可能になります。 以下をご覧下さい。

struct Info {
	m int
}
varia s Info = { 10 }
varia i = s ? s.m : 20
Rrk_print( \=i \n )

三項演算子は「条件式 ? A : B」という書式です。 上記では三項演算子の条件式の部分に単に「s」と指定されていますのでif文と同様にsはnotnull化されます。 ただし三項演算子には勿論ifブロックはなく、notnull化される範囲は上記の「A」の部分になります。 「B」の部分や三項演算子全体としての値はnotnull化されるとは限らないことにご注意ください。

三項演算子の条件式に否定演算子を伴う単独の識別子を指定した場合は、 notnull化される範囲は上記とは逆に三項演算子の「B」の部分になります。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info = { 10 }
varia i = !s ? 20 : s.m
Rrk_print( \=i \n )

上記では三項演算子の条件式の部分に「!s」と指定されていますので、 notnull化されるのは「s.m」における s になります。

次にもう少し複雑な例を見ましょう。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2^? = { { 10 } }
varia d Info    = { 20 }
varia t = s && s.n ? s.n : d
/* t is notnull */
Rrk_print( \=t.m \n )

上記では三項演算子の条件式の部分に単に「s && s.n」と指定されており、 この場合条件式の中も一部notnull化の効果が及びます。 つまり「s && s.n」のうち「s.n」における「s」はnotnull化され、結果的に「s.n」といった表記をここで記述することが許可されます。 次に上記では三項演算子の「A」の部分に「s.n」と指定されており、これは条件式の「s.n」という指定からnotnull化されます。 また三項演算子の「B」の部分では「d」と指定されており、notnullなInfo型です。 三項演算子の「A」と「B」のいずれもnotnullである場合、全体の値としてもnotnullとなり、 従って上記の t はnotnullなInfo型として型推論されます。

上記の例では「s.n」の値がnullの場合はdをデフォルト値としてtへ格納する形になっていますが、 この d の替わりに「{}」を指定するような記述も可能です。 例えば以下をご覧下さい。

struct Info {
	m int
}
struct Info2 {
	n Info^?
}
varia s Info2^? = { { 10 } }
varia t = s && s.n ? s.n : {}
/* t is notnull */
Rrk_print( \=t.m \n )

上記では、最終的に「{}」の部分の型推論がどのようになされるかが重要です。 この場合、手がかりは三項演算子の「A」の部分に相当する「s.n」しかありませんが、 まずこれより t の型がInfoと推論されます(これはnotnull化されているはずです)。 次に t の型より(三項演算子の「B」の部分に相当する)「{}」の型がInfoと推論されます。

final notnull文


元々nullableな変数であっても、そこにnotnullなものが再代入されればそれ以降は明らかにnotnullとみなすことができます。 final notnull文を使うとそのような変換を明示できます。

正確には final notnull文は後のセクションで紹介する「final文」の亜種となります。

例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = null
varia t Info = { 10 }

final notnull s = t

/* s is notnull */
Rrk_print( \=s.m \n )

上記では s は元々nullableの変数として宣言されていましたが、「final notnull s = t」によって(notnullである)tの値がsに代入されたため、 それ以降 s は(このブロックの終了まで)notnullとみなされます。 ただしこのとき「final notnull」を先頭に付ける必要があります。 単なる「s = t」といった代入文では s の notnull 化まではされません。

final notnull文では必ず「X=Y」という代入文の形で指定する必要があります。 左辺Xについては既に宣言された単なる変数名、あるいはその変数における構造体メンバへのアクセスの形をした式である必要があります (代入が発生するため、Xがconstなどで宣言されている場合はこれをfinal notnullで指定することはできません)。 右辺Yの指定も必須であり、しかもその右辺は必ずnotnullな式でなければなりません(こちらは複雑な式の指定も可能です)。 これらの条件を満たさない場合はコンパイルエラーとなります。

上記の例は t という別のnotnullな変数が予め用意されている状況でしたが、 以下のように三項演算子を組み合わせて s 自身をnotnull化する方法もあります。

struct Info {
	m int
}
varia s Info^? = null

final notnull s = s ? s : {}

/* s is notnull */
Rrk_print( \=s.m \n )

上記のfinal notnull文の右辺「s ? s : {}」の値は、s が非nullの場合は(notnull化された)s 、s が null の場合は(notnullな)「{}」となります。 いずれもnotnullであるため、右辺の値全体としてもnotnullとなり、左辺における s のnotnull化が成功します。

if文の条件に否定演算子が指定されており、かつ final notnull文がifブロックの中にある場合、 そのifブロックの後ろがnotnull化される場合があります。 例えば以下をご覧下さい。

struct Info {
	m int
}
varia s Info^? = null

if !s {
	final notnull s = s ? s : {}
}

/* s is notnull */
Rrk_print( \=s.m \n )

上記では if文の条件式は s! ですから s がnullの場合にifブロックに入りますが、 ifブロック内部でfinal notnull文により s は確実にnotnull化されるため、ifブロックの後ろの範囲においてもまた s がnotnull化されます。

上記のようなfinal notnullの右辺に三項演算子を使うコードでは、同じ変数名を冗長に三つも記述することになります。 これを嫌うなら、関数形式マクロの一つであるRrk_notnull_elseを使うこともできます。 例えば以下のようになります。

struct Info {
	m int
	n int
}
varia s Info^? = null

/* 以下は final notnull s = s ? s : {10,20} と同じ */
^!Rrk_notnull_else( s, Rrk_asv^<Info>({10,20}) )
/* 以降、sはnotnullとみなされる */


上記で「^!Rrk_notnull_else( s, Rrk_asv^({10,20}) )」の部分は結果的に「final notnull s = s ? s : {10,20}」と書いたのと同じとなります。 マクロについては後のセクションで詳しく述べますが、マクロの呼び出しでは先頭に必ず「^!」を指定し、 その後にマクロ名、さらに引数があるマクロの場合は引数を書きます。 中身に「,」を含む引数を一つの塊として指定したい場合は、Rrk_asv関数(これはマクロではなくジェネリクス関数と呼ばれるものですが)を使い、 その直後に上記のように引数の型(Info)と引数の実体({10,20})を記述します (そうしないとその「,」がRrk_notnull_elseマクロにおける引数の区切りとして解釈されてしまいます)。 例えば今回の場合、引数「{10,20}」は中身に「,」を含みますので、このRrk_asvを使う必要があります。

もしもこの引数が「{10,20}」ではなく単に「{}」であるなら、これは中身に「,」を含みませんので、 「Rrk_asv^({})」と書く代わりに単に「{}」と書くこともできます。

さて、指定された変数名がnotnullであることが確実に明らかな場合でも、 このようなnotnullな代替値を指定しなければならないのは面倒ではないでしょうか? その場合、関数形式マクロRrk_notnull_assertを使うこともできます。 例えば以下のようになります。

struct Info {
	m int
	n int
}
varia s Info^? = null

^!Rrk_notnull_assert( s )
/* 以降、sは強制的にnotnullとみなされる */


Rrk_notnull_assertを使用した場合、s は強制的にnotnullとみなされます。一方で、万一 s の中身がnullであった場合、 Rrk_notnull_assertではこれを使用した位置においてnullアクセスに関するランタイムエラーが発生します。 本来このセクションでの目的は、このランタイムエラーが発生する可能性を極力ゼロにすることですが、 これはその目的に反して(明らかにnotnullであることがわかっている状況で使うことが前提とは言え)その可能性を与えてしまうことになります。 そのため、Rrk_notnull_assertを使うのは、これまで紹介してきた方法ではどうしても記述が煩雑になったりうまくいかない場合の最後の手段とすべきでしょう。


自己参照構造体のコピー


最後に、練習問題として構造体のセクションで後回しにしておいた自己参照構造体のコピーの実装を考えましょう。

構造体のセクションでも軽く触れましたが、このような構造体のコピーを実装する場合、再帰的に構造体メンバへ入り込みながらコピーを行う過程において、 無限にコピーされるのを防ぐための何らかの措置を講じなければなりません。 例えば以下をご覧ください。

struct MyStruct {
	i int
	other OtherStruct^?
}

struct OtherStruct {
	c  string
	my MyStruct^? = null
}

function MyStruct_copy( varia dst MyStruct, src MyStruct ){
	dst.i = src.i
	if src.other {
		final notnull dst.other = dst.other ? dst.other : {}
		OtherStruct_copy( dst.other, src.other )
	}
}
function OtherStruct_copy( varia dst OtherStruct, src OtherStruct ){
	dst.c = src.c->string
	if src.my {
		final notnull dst.my = dst.my ? dst.my : {}
		MyStruct_copy( dst.my, src.my )
	}
}

function MyStruct_print( s MyStruct ){
	Rrk_print( "  " \=s.i \n )
	if s.other {
		OtherStruct_print( s.other )
	}
}
function OtherStruct_print( o OtherStruct ){
	Rrk_print( "  " \=o.c \n )
	if o.my {
		MyStruct_print( o.my )
	}
}


varia s1 MyStruct = { 10, { "hello", { 1, null } } }
varia s2 MyStruct = { 20, { "world", { 2, null } } }

Rrk_print( "before\n" )
MyStruct_print( s1 )
MyStruct_print( s2 )

MyStruct_copy( s1, s2 )

Rrk_print( "after\n" )
MyStruct_print( s1 )
MyStruct_print( s2 )

上記では、MyStruct内にOtherStruct型のメンバotherがあり、OtherStructを経由して結局MyStructに戻りますから間接的には自己参照しています。 そのため、MyStruct_copy内においては、コピー元のMyStructのメンバotherがnullである場合、それ以上の再帰的なコピーをしないようにしています。 OtherStruct_copy内においても同様で、コピー元のOtherStructのメンバmyがnullである場合、それ以上の再帰的なコピーをしないようにしています。 ただしコピー元がnullではなくしかもコピー先がnullであった場合は、まずコピー先の実体を確保してからコピーするようにしてあります。 上記を実行した結果は以下のようになります。

before
  s.i=10
  o.c=hello
  s.i=1
  s.i=20
  o.c=world
  s.i=2
after
  s.i=20
  o.c=world
  s.i=2
  s.i=20
  o.c=world
  s.i=2

nullableとnotnullのまとめ


Rarakuの型はデフォルトではnullを代入することはできません。 nullを代入可能(nullable)とするには型名の後ろに「^?」を付記する必要があります。 また逆にnullableである変数には色々と制限がかかるため、notnull化する必要があり、 if文や三項演算子など様々な指定でこれを行うことができます。

目次に戻る

列挙型とenum文

列挙型とその意義


列挙型とは名前を持った整数の集合をグループ化した型です。 その集合の元のことを特に列挙子と呼びます。

列挙型の型名と集合はenum文で定義されます。 enum文の書式は「enum 列挙型名 { 列挙子の並び }」のようになります。 以下に例を示します。

enum Animal {
	Rabbit
	Dog
	Cat
}
enum OS {
	Windows
	Linux
	Android
	MacOS
}

上記では二つの列挙型AnimalとOSを定義しています。 AnimalはRabbit、Dog、Catという3つの列挙子を持ち、OSはWindows、Linux、Android、MacOSという4つの列挙子を持ちます。 それぞれの列挙子はカンマで区切るのではなくホワイトスペース(半角スペースやタブ文字、改行)で区切ります

Rarakuのenum文では、ちょうどC言語における「typedef enum { ... } 列挙型名」に相当することをやっています。

enumはenumerationの略です。 他の言語でも登場するキーワードですが「イーナム」と発音することが多いと思います。

この例では整数値がどこにも現れていませんが、この場合一番上の列挙子から順番に 0, 1, 2, 3, …と整数値が自動的に割り振られます。 すなわち列挙型Animalにおける Rabbitは 0、Dogは 1、Catは 2 といったように整数が割り振られます。 列挙型OSにおいても同様に Windowsは0、Linuxは 1、Androidは 2、MacOSは 3 となります。 (この割り振りを明示的に指定して変更することもできますが、それについては後述します)。

一旦このように定義した後は、これらの列挙型変数を宣言し使用することができます。 また列挙型変数には、その列挙型で定義された列挙子だけが代入できます(それ以外のものを代入しようとした場合コンパイルエラーとなります)。 以下に例を示します。

enum Animal {
	Rabbit
	Dog
	Cat
}
enum OS {
	Windows
	Linux
	Android
	MacOS
}

varia animal Animal = Rabbit
animal = Dog

varia os OS = Windows
os = Linux

この例ではAnimal型変数animalを宣言し、列挙子Rabbitで初期化した後、これに列挙子Dogを代入しています。 またOS型変数osを宣言し、列挙子Windowsで初期化した後、これに列挙子Linuxを代入しています。

さて、このような列挙型をわざわざ用意することに何の意味があるのでしょうか? 一見するとint型定数でも同様のことが実現できそうに思います。 すなわち以下のようにすればよさそうです。

/* Animal */
const Rabbit  = 0
const Dog = 1
const Cat = 2

/* OS */
const Windows = 0
const Linux   = 1
const Android = 2
const MacOS   = 3

varia animal int = Rabbit
animal = Dog

varia os int = Windows
os = Linux

わざわざすべての定数に整数値を代入しなければならない手間はありますが、本質的な差はなさそうに思えます。 しかしint型定数と列挙型では重要な違いがあります。

列挙型を使用した例では変数animalに Linuxを代入したり、逆に変数osに Dogを代入したりすることはできません。 それらは異なる列挙型の定義に含まれる列挙子であり、互換性がないからです。 そしてそれはデータの意味的にも理にかなっています。

これにより、万が一プログラマが誤ってそのような代入式を書いてしまったとしても、コンパイルの時点でエラー表示が行われ、 即座に間違いに気づくことができます。

一方、int型定数を使用した例ではこれができてしまいます。 すなわちanimalもosも単なるint型変数に過ぎないため、変数animalに Linuxを代入したり、逆に変数osに Dogを代入できてしまいます。 これはデータの意味的にも間違った代入です。

これにより、万が一プログラマが誤ってそのような代入式を書いてしまったとしても、コンパイルの時点でエラー表示が行われません。 おそらく実際に実行してテストしてみるまで、その間違いに気づくことができなくなります。

これが列挙型の一番重要な性質です。

文字列と一緒に使うようなコンテキストでは、列挙型変数や列挙子は自動的にその名前に変換されます。 例えば以下をご覧ください。

enum Animal {
	Rabbit
	Dog
	Cat
}
enum OS {
	Windows
	Linux
	Android
	MacOS
}

varia animal Animal = Rabbit
Rrk_print( "animal=[" animal "]\n" )

varia os OS = Windows
Rrk_print( "os=[" os "]\n" )

上記を実行すると以下のように表示されます。

animal=[Rabbit]
os=[Windows]

これは現在、列挙型変数にどんな値が格納されているかを確認するのに便利です。 整数ではなく名前の方で表示される点もint型定数を使った場合との違いと言えます。

列挙型と整数の相互変換


ここまでの説明ですと、そもそも列挙型に整数を割り振る意味自体がないように思えます。 しかし現実には列挙型と整数を相互変換したい場合も多くあります。 列挙子に整数値が割り振られているのはこれを実現するためです。

例えばある種のバイナリデータのフォーマットでは、0 がRabbit、1 がDog、2 がCatといった意味を持っているかもしれません。 そのような時、整数と列挙型を相互変換する必要性が生じます。 列挙型を持つプログラミング言語であれば、このための手段もまた言語レベルで提供されるべきです。 さもなければ列挙型自体が簡単には使えなくなってしまいます (if文や後述するswitch文等で、列挙型と整数を相互変換するようなプログラムを(列挙型の種類毎に)自前で実装しなければならなくなるでしょう)。

Rarakuではこのような場合、明示的なキャストにより列挙型から整数値へ、あるいは整数型から列挙型へ変換することができます。 明示的なキャストを行うには「->変換先の型名」という書式を後ろに続けます。 例えば以下の通りです。

enum Animal {
	Rabbit
	Dog
	Cat
}
varia animal Animal = Dog
varia ival  int

ival   = animal->int  /* Animal型変数animalの値をint型にキャストし、列挙子に割り振られた整数値を得る */ 
Rrk_print( "ival=[" ival "]\n" )

animal = ival->Animal /* int型変数ivalの値をAnimal型にキャストし、整数値から列挙子を復元する */
Rrk_print( "animal=[" animal "]\n" )

この例で「ival = animal->int」によりanimalがint型にキャストされます。 今、変数animalにはDog列挙子が格納されていますが、これはenum文の定義により整数値1が割り振られているため、 int型にキャストした結果は 1 となります。

また逆に「animal = ival->Animal」により、animalにはDog列挙子が格納されます。 このプロセスは若干複雑です。 まずスタートとなる ival には 1 が格納されています。 このとき列挙型Animalの定義が参照され、列挙子の中で 1 が割り振られているものを上から順に検索します。 今回の場合それはDog列挙子に相当しますので、結果的にDog列挙子へと変換されるわけです。

上記の実行結果も見ておきましょう。 以下のようになります。

ival=[1]
animal=[Dog]

列挙型の定義で整数を明示的に指定する


既に述べたように、列挙型の定義(enum文)では、一番上の列挙子から順番に 0, 1, 2, 3, …と整数値が自動的に割り振られます。 しかし、この割り振り方では困る場合、列挙子に割り振られる整数値を直接指定することもできます。 そのためには以下のように列挙子の後ろに「=整数値」という記述を続けます。

enum Animal {
	Rabbit  = 10
	Dog
	Cat
}

上の例では列挙子Rabbitには「=10」が明示的に指定されており、これに割り振られる整数値は10となります。 一方、Dog、Catについては後ろに「=整数値」が指定されていませんが、このような場合 直前の列挙子に割り振られた整数値に1加算したものが自動的に割り振られます。 つまりこのとき列挙子Dogが割り振られる整数値は(1ではなく)11、列挙子Catが割り振られる整数値は(2ではなく)12となります。

もう少しだけ複雑な例を見てみましょう。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Carrot = 10
	Potato
	Broccoli
	Burdock = Carrot->int + 10
	Pumpkin
}
Rrk_print( "Tomato  =[" Tomato  ->int "]\n" )
Rrk_print( "Onion   =[" Onion   ->int "]\n" )
Rrk_print( "Lettuce =[" Lettuce ->int "]\n" )
Rrk_print( "Carrot  =[" Carrot  ->int "]\n" )
Rrk_print( "Potato  =[" Potato  ->int "]\n" )
Rrk_print( "Broccoli=[" Broccoli->int "]\n" )
Rrk_print( "Burdock =[" Burdock ->int "]\n" )
Rrk_print( "Pumpkin =[" Pumpkin ->int "]\n" )

この例ではまず一番上の列挙子Tomatoに値が明示されていませんが、一番上なのでデフォルトでは 0 が割り振られます。 その後のOnion、Lettuceは1ずつ加算して1、2となります。

Carrotでは値が10と明示されていますから 10 が割り振られます。 続くPotato、Broccoliは1ずつ加算して11、12となります。

Burdockでは値が Carrot->int + 10 と指定されています。 ここで「Carrot->int」は、「Carrotをint型にキャストせよ」という意味ですが、 Carrotには既に値10が割り振られていますのでこの値は10になります。

結果的にBurdockの値は 10 + 10 で20が割り振られます。 続くPumpkinは1加算されて21となります。

これを実行した結果を以下に示します。

Tomato  =[0]
Onion   =[1]
Lettuce =[2]
Carrot  =[10]
Potato  =[11]
Broccoli=[12]
Burdock =[20]
Pumpkin =[21]

列挙子に同じ整数値を割り振るのは構いません。 例えば以下の例では、TomatoとBroccoliには 0 が、 PotatoとBurdockには -1 が割り振られます。

enum Vegetable {
	Tomato 
	Onion
	Lettuce
	Carrot = -2
	Potato
	Broccoli
	Burdock = -1
	Pumpkin
}
Rrk_print( "Tomato  =[" Tomato  ->int "]\n" )
Rrk_print( "Onion   =[" Onion   ->int "]\n" )
Rrk_print( "Lettuce =[" Lettuce ->int "]\n" )
Rrk_print( "Carrot  =[" Carrot  ->int "]\n" )
Rrk_print( "Potato  =[" Potato  ->int "]\n" )
Rrk_print( "Broccoli=[" Broccoli->int "]\n" )
Rrk_print( "Burdock =[" Burdock ->int "]\n" )
Rrk_print( "Pumpkin =[" Pumpkin ->int "]\n" )

この例の実行結果は以下のようになります。

Tomato  =[0]
Onion   =[1]
Lettuce =[2]
Carrot  =[-2]
Potato  =[-1]
Broccoli=[0]
Burdock =[-1]
Pumpkin =[0]

この例で整数値 0 からVegetable型に明示的にキャストした場合、列挙子はTomatoとBroccoliのどちらになるのでしょうか? 例えば以下のようにキャストした場合です。

varia veg Vegetable = 0->Vegetable /* veg は Tomato と Broccoli のどちらになるのか? */

このような場合、列挙型Vegetableの定義を上から順に検索し、最初に 0 に割り振られた列挙子が採用されます。 つまりこの例では Broccoli より Tomato の方が上にありますから Tomato が採用されます。

整数値 -1 からVegetable型に明示的にキャストした場合も同様で、列挙型Vegetableの定義を上から順に検索し、 最初に -1 に割り振られた列挙子が採用されます。 つまりこの例では Burdock より Potato の方が上にありますから Potato が採用されます。

varia veg Vegetable = -1->Vegetable /* veg は Potatoの方になる */

一方、列挙子の名前については同じものを重複して定義してはいけません。 以下の例では、列挙子Tomatoが二つ重複して定義されており、コンパイルエラーとなります。

enum Vegetable {
	Tomato 
	Onion
	Tomato /* NG: Tomatoという列挙子は(この列挙型内に)既にある */ 
}

上の例では、同じVegetableという列挙型内での名前の重複でしたが、これは異なる列挙型内であっても同様です。 以下の例では、AnimalとVegetableという異なる列挙型にそれぞれTomatoが定義されてありますが、 このような場合も重複とみなされ、コンパイルエラーとなります。

enum Animal {
	Cat
	Tomato 
}
enum Vegetable {
	Tomato /* NG: Tomatoという列挙子は(Animal型内に)既にある */ 
	Onion
}

列挙型変数のデフォルト値とその変更


列挙型変数の宣言において、初期化されていない場合はその列挙型の一番上にある列挙子がデフォルト値となります。 例えば以下の例をご覧ください。

enum Vegetable {
	Tomato
	Onion
	Pumpkin
}
varia veg Vegetable
Rrk_print( "veg=" veg "\n" )

上の例では、変数vegは明示的に初期化されていません。 列挙型Vegetableの定義において一番上に指定されている列挙子はTomatoですから、この場合それがデフォルト値となります。 よってvegにはTomatoが格納されます。 この例の実行結果は以下のようになります。

veg=Tomato

ただしこのデフォルト値はdefaultキーワードによって変更することもできます。 例えば以下の例をご覧ください。

enum Vegetable {
	Tomato
	Onion
	Pumpkin default
}
varia veg Vegetable
Rrk_print( "veg=" veg "\n" )

この例でもまた変数vegは明示的に初期化されていません。 また今回の場合、列挙子Pumpkinの後ろにキーワードdefaultが指定されています。 この場合デフォルト値として列挙子Pumpkinが採用され、その値が格納されます。 この例の実行結果は以下のようになります。

veg=Pumpkin

列挙子に整数値が明示的に指定されている場合は、その指定の後ろにキーワードdefaultを指定します。 例えば以下の通りです。

enum Animal {
	Rabbit
	Dog = 10 default
	Cat
}

enum Vegetable {
	Tomato
	Onion
	Pumpkin = Tomato->int default
}
varia anim Animal
varia veg  Vegetable
Rrk_print( "anim=" anim "\n" )
Rrk_print( "veg="  veg "\n" )

この例ではAnimalのデフォルトの列挙子はDog、Vegetableのデフォルトの列挙子はPumpkinとなり、実行結果は以下のようになります。

anim=Dog
veg=Pumpkin

一つのenum文の中に複数のdefault指定を行うことはできません。 例えば以下のように記述した場合はコンパイルエラーとなります。

enum Animal {
	Rabbit
	Dog default
	Cat default /* コンパイルエラー: 複数のdefaultを指定している */
}

列挙型の比較演算


二つの列挙型を比較する場合、整数と同様に比較演算子「==」と「!=」を使うことができます。 例えば以下の例をご覧ください。

enum Vegetable {
	Tomato  = 0 
	Onion   = 1
	Pumpkin = 0
}
Rrk_print( "Tomato == Onion   is [" (Tomato == Onion) "]\n" )
Rrk_print( "Tomato != Onion   is [" (Tomato != Onion) "]\n" )
Rrk_print( "Tomato == Pumpkin is [" (Tomato == Pumpkin) "]\n" )

列挙型において「==」や「!=」を適用すると、列挙子に割り振られた整数値での比較になります。 この例では、Tomatoが 0 、Onionが 1 ですのでそれらは異なっているとみなされます。 つまり「Tomato == Onion」はfalse、「Tomato != Onion」は true になります。 また Pumpkinが 0 ですので、Tomato と等しいことになります。 つまり「Tomato == Pumpkin」は true になります。

上記のように文字列を連結するような場面で条件式を記述したい場合は、「(Tomato == Onion) 」のようにその条件式を括弧で囲む必要があります。 この括弧がない場合、直前の文字列リテラルとの連結が優先的に実行されてしまいますのでコンパイルエラーとなります。

これを実行した結果を以下に示します。

Tomato == Onion   is [false]
Tomato != Onion   is [true]
Tomato == Pumpkin is [true]

では上の例で Tomato と Pumpkin を(割り振られた整数値が何であるかに関わらず)異なるものと扱って欲しい場合はどうすればよいでしょうか? その場合列挙型を文字列型にキャストして比較します。 例えば次の通りです。

enum Vegetable {
	Tomato  = 0 
	Onion   = 1
	Pumpkin = 0
}
Rrk_print( "Tomato == Onion   is [" (Tomato->conststr == Onion->conststr) "]\n" )
Rrk_print( "Tomato != Onion   is [" (Tomato->conststr != Onion->conststr) "]\n" )
Rrk_print( "Tomato == Pumpkin is [" (Tomato->conststr == Pumpkin->conststr) "]\n" )

この場合、各列挙子を一旦文字列リテラルに変換したものが比較されます。 例えばTomatoは"Tomato"に、Pumpkinは"Pumpkin"に変換されますが、これらは文字列として異なるため、 結果的に望み通りの比較ができたことになります。 これを実行した結果を以下に示します。

Tomato == Onion   is [false]
Tomato != Onion   is [true]
Tomato == Pumpkin is [false]

ちなみに列挙型においては大小比較演算子「>=」「<=」「>」「<」を直接使うことはできません (コンパイルエラーとなります)。 ただし以下のように int 型にキャストすれば、それらに割り振られた整数値での大小比較が可能です。

enum Vegetable {
	Tomato  = 0 
	Onion   = 1
	Pumpkin = 0
}
Rrk_print( "Tomato > Onion is [" ( Tomato->int > Onion->int ) "]\n" )

上の例では「Tomato->int」により0に変換され、「Onion->int」により1に変換されます。 結局「0>1」という条件式になりますからこの値はfalseとなります。

intではなくconststrへキャストした場合も同様です(あまり使いどころはないかもしれませんが)。

enum Vegetable {
	Tomato  = 0 
	Onion   = 1
	Pumpkin = 0
}
Rrk_print( "Tomato > Onion is [" ( Tomato->conststr > Onion->conststr ) "]\n" )

上の例では「Tomato->int」により"Tomato"に変換され、「Onion->int」により"Onion"に変換されます。 結局「"Tomato" > "Onion"」という条件式になりますからこの値は(文字列は辞書順で大小が評価されますから)trueとなります。

列挙型の配列


ここで今までの知識の応用として、列挙型の配列を取り扱う少し長めのコードを見てみましょう。 列挙型の配列の書式も他の型の配列と同様で、「varia/const 配列変数名 列挙型名[ ]」といった形になります。

以下の例では列挙型の配列 veg_ary を宣言し、その全要素の名前と値を表示しています。

import std/str /* for RrkStr_leng */

enum Vegetable {
	Tomato 
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}

/* Vegetable型配列の宣言 */
const veg_ary Vegetable[] = [
	Tomato, 
	Onion,
	Lettuce,
	Carrot,
	Potato,
	Broccoli,
	Burdock,
	Pumpkin,
]

/***
 * nameの文字列長がmax文字に満たない場合は残りの文字を半角スペースで埋め、
 * 全体の文字列長を max 文字に整えたものを返す。
 * さもなければ name をそのまま返す。
 */
function name_with_ws( name conststr, max uint ) conststr {
	const leng = RrkStr_leng( name )
	varia ws string
	if leng < max {
		varia num = max - leng
		while num { ws &= " " --num; }
	}
	return name & ws
}

foreach veg : veg_ary {
	const name = veg->conststr
	Rrk_print( name_with_ws(name,8) "=[" veg->int "]\n" )
}

この例では、文字列の長さを得るための標準関数 RrkStr_leng を使いたいため、 まず一番上で std/str をインポートしています。

次に列挙型の定義、ならびに列挙型配列の宣言を行っています。

その下ではユーザ定義関数として name_with_ws を定義しています。 この関数は列合わせのために後ろに半角スペースを追加する補助関数で、 nameの文字列長がmax文字に満たない場合は残りの文字を半角スペースで埋めて、 全体の文字列長を max 文字に整えたものを返します(さもなければ name をそのまま返します)。

最後の foreach 文では、veg_ary の全ての要素についてその名前と整数値を表示する処理を行っています。 実際の処理はここから始まります。

この例の実行結果は以下のようになります。

Tomato  =[0]
Onion   =[1]
Lettuce =[2]
Carrot  =[3]
Potato  =[4]
Broccoli=[5]
Burdock =[6]
Pumpkin =[7]

ところで上の例での veg_ary は、元のenum文で定義されたすべての列挙子を同じ順番で並べたような配列になっています。 この部分だけを以下に抜粋します。

enum Vegetable {
	Tomato 
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}

/* Vegetable型配列の宣言 */
const veg_ary Vegetable[] = [
	Tomato, 
	Onion,
	Lettuce,
	Carrot,
	Potato,
	Broccoli,
	Burdock,
	Pumpkin,
]

これは冗長です。 このような配列を定義する場合、替わりにRrk_enum_ary関数を使うことができます。 例えば以下の通りです。

enum Vegetable {
	Tomato 
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}

/* Vegetable型配列の宣言 */
const veg_ary = Rrk_enum_ary^<Vegetable>()

まず関数名Rrk_enum_aryを記述し、その後にenum型 Vegetable を「^<」と「>」の間に入れて指定することに注意してください。 最後に「()」が来ます。

これは通常の関数とは異なるジェネリクスと呼ばれるもので、そのために「^<」や「>」といった記号が関数名と関数引数の間に含まれています。 ただしジェネリクスについてはまだ説明していませんので、ここでは深入りはしません。 ジェネリクスについては「ジェネリクス」のセクションで詳しく説明します。 尚、C++に慣れている方は、「<」と「>」ではなく「^<」と「>」であるという点にもご注意ください。

これをforeachに指定することもできますから、結局上の例は以下のようにも記述できます。

import std/str /* for RrkStr_leng */

enum Vegetable {
	Tomato 
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}

function name_with_ws( name conststr, max uint ) conststr {
	const leng = RrkStr_leng( name )
	varia ws string
	if leng < max {
		varia num = max - leng
		while num { ws &= " " --num; }
	}
	return name & ws
}

 foreach veg : Rrk_enum_ary^<Vegetable> {
	const name = veg->conststr
	Rrk_print( name_with_ws(name,8) "=[" veg->int "]\n" )
}



列挙型とenum文のまとめ


列挙型は整数の集合をグループ化したものであり、最も重要な性質は他の列挙型や整数と型が区別されることです。 その集合の元のことを特に列挙子と呼び、列挙型の型名と集合はenum文で定義します。

各列挙子は必ず識別子を持ち、同じ識別子の列挙子を重複して定義することはできません。 一番上の列挙子には通常、0 が割り振られます。 ただし列挙子に割り振る整数は明示的に指定することもできます。 指定を省略した列挙子は直前の列挙子に1加算した値が割り振られます。

defaultキーワードによりデフォルト列挙子を明示的に変更することもできます。 これを指定しなかった場合は一番上の列挙子がデフォルトとなります。

キャスト演算子「->」を用いることにより、列挙子をそれに割り振られた整数値や識別子名に該当する文字列へ変換することができます。 列挙型は比較演算子「==」と「!=」で比較することもでき、(キャスト等で変換していなければ)それは割り振られた整数を基に行われます。



目次に戻る

switch文

switch文の基本書式


ある変数の整数値によって処理を分岐するには if 文を使って次のように書くのでした。

varia ival = 2

if ival == 0 {
	Rrk_print( "when ival is 0\n" )
}: ival == 1 {
	Rrk_print( "when ival is 1\n" )
}: ival == 2 {
	Rrk_print( "when ival is 2\n" )
}: ival == 3 {
	Rrk_print( "when ival is 3\n" )
}: ival == 4 {
	Rrk_print( "when ival is 4\n" )
}:{
	Rrk_print( "when ival is other\n" )
}

この例は ival の値が 0、1、2、3、4、それ以外によって処理を分岐するものです。

ちなみに「}: 条件式」は「} elif 条件式」、「}:{」は「} else {」のシンタックスシュガーです。

switch文とはこのような整数値に応じた分岐処理をもう少しすっきり書くためのものです。 これは以下のような書式になります。

switch 条件式 {
case 候補値リテラル : 文の集合
case 候補値リテラル : 文の集合
…
default : 文の集合
}

この書式はGo言語のswitch文とよく似ています。

ここで「条件式」には判定対象となる変数を指定します。 switch文の場合、条件式の直後に「{」が続き、それに呼応する「}」が現れた地点でそのswitch文が終了します。

「候補値リテラル」には条件式で指定した値の候補を指定しますが、 これらは条件式に指定したものと同じ型をもつリテラル式でなければなりません。 また、ここに指定可能なものは整数の数値リテラル、文字整数リテラル、列挙子、文字列リテラルに限られます (real型の数値リテラルを指定することはできません)。

「case 候補値リテラル : 文の集合」は複数個指定することができ、必要な数だけそれを並べます (ただしこのとき「候補値リテラル」の値が他と重複してはいけません)。 「default : 文の集合」は0個または1個指定することができます。

上の ival の値に応じて処理を分岐する例をswitch文で書き直したものを以下に示します。

varia ival = 2

switch ival {
case 0 :
	Rrk_print( "when ival is 0\n" )
case 1 :
	Rrk_print( "when ival is 1\n" )
case 2 :
	Rrk_print( "when ival is 2\n" )
case 3 :
	Rrk_print( "when ival is 3\n" )
case 4 :
	Rrk_print( "when ival is 4\n" )
default :
	Rrk_print( "when ival is other\n" )
}

毎回「ival ==」を書かなくてもよくなった分、if文よりスッキリしていることがわかります。 例えば ivalの値が0ならば case 0 : とある部分の文の集合のみが実行され、それ以外はスキップされて最後へ制御を移動します。 同様に ivalの値が1ならば case 1 : とある部分の文の集合のみが実行されるといった具合です。 ivalの値が0, 1, 2, 3, 4 のいずれでもなければ、default : とある部分の文の集合のみが実行されます。

参考: C言語のswitch文との違い
この「参考」はC言語をご存知の方のみお読みください。

switch文の書式はC言語のswitch文と似ている点もありますが異なる点もあります。

まず、switchに与える「条件式」を囲う「(」と「)」は書いても書かなくてもどちらでも構いません。 その直後にcaseキーワードかdefaultキーワードが続きます。 これを今便宜上、case部やdefault部と呼ぶことにします。

Rarakuのcase部やdefault部においては、(C言語のそれと異なり)暗黙のブロックが設定されます。 そのため、もしもそこに(varia/const文による)変数/定数を宣言した場合、 各case部、default部を抜けたタイミングでそのスコープは終了します。

一方、C言語のswitch文内では、そもそもそこに変数/定数を宣言すること自体が(直接は)できません。 変数を宣言するためにはそこに一旦ブロックを明示的に記述する必要があります。 このこととRarakuで暗黙のブロックが設定されることとは少し関係がありますが、 それについて興味ある方は以下の参考をお読みください。

参考: C言語のswitch文内でブロック無しでは変数宣言ができない理由
C言語のswitch文内では以下のように(ブロックを使わずに)変数を直接宣言することはできません。

/* C言語によるswitch文 */
int ival = 0;

switch( ival ){
case 0:
	int my_var = 9; /* コンパイルエラー */
	break;
case 1:
	/* something */
	break;
default:
	/* otherwise */
	break;
}

C言語では「switch( ival ){ ... }」のように一見ブロックのようなものがついてはいますが、これは(通常の意味での)ブロックではありません。 C言語でも以下のようにして記述すれば、case内に変数を宣言することができます。

/* C言語によるswitch文 */
int ival = 0;

switch( ival ){
case 0:
{
	int my_var = 9; /* OK. */
	break;
}
case 1:
	/* something */
	break;
default:
	/* otherwise */
	break;
}

おそらくC言語がこのような仕様になっているのは、(もしcase内で変数を宣言したならば) case部を終了した時点でその変数のスコープを確実に終了させなければならないためだと思います。

仮にここでスコープを終了せず、つまりswitch文を抜けたその後の処理でもそれらの変数にアクセスできる仕様であったとしましょう。 そのような仕様では以下のような場合問題が起こります。

/* C言語によるswitch文 */
int ival = 0;

switch( ival ){
case 0:
	int my_var = 9; /* 仮にこれがコンパイルエラーにならず、case 0 部が終わった後でも使用可であると仮定する */
	break;
case 1:
	/* ここではmy_varのアクセスは不可とすべき */
	break;
default:
	/* otherwise */
	break;
}

printf( my_var );

この例では、変数 my_var が宣言されるのは case 0 を経由した場合のみです。 では case 1 を経由するとどうなるでしょう? その場合、変数 my_var の宣言を経由せずにここへ来た状況ですからこれはアクセス不可とすべきです (C言語の関数内では宣言部だけを特別に掻い摘んで実行するなどといったことも起こりません)。 スコープを case 0 部を終了した後まで引き伸ばしてしまうとここに矛盾が生じます。

かといって「{」と「}」の指定なしにここでスコープが強制的に終了するのも、(スコープの範囲が)見た目わかりにくいです。 ですからC言語の場合、case 内での変数宣言が必要ならば「{」と「}」で明示的に囲い、 case内で自然にそのスコープを終端している形になるようにルール付けしているものと思われます。 また同時にその方がスコープの範囲がコード上で可視化、明確化します。

次の「switch文における暗黙のブロック」の項で詳しく説明しますが、 Rarakuにおけるswitch文では、まさに上で述べたブロックが暗黙のうちに設定されています。 明確な「{」と「}」により可視化されてはいませんが、本当は存在すると考えてください (ブロックの範囲の可視化よりも、記述の簡易さの方を優先して「{」と「}」を書かなくてもよい仕様になっているということです)。 この暗黙のブロックを設定した目的も上で述べた通りで、 各case部やdefault部内での変数宣言のスコープをその部内で確実に終了させるためです。

Close


Rarakuのswitch文では(C言語のswitch文の意味での)break文は存在しません。 case部やdefault部での「文の集合」が終わるとswitch文自体を終了します (もう少し正確に言えば、一旦「文の集合」を実行するモードに入ると、次のcaseキーワードやdefaultキーワードや switch文の終わりの「}」を検出した時点で「文の集合」の終わりとみなし、 その位置で(C言語の意味での)break文が自動的に実行されるという挙動になります)。

仮にcase部やdefault部のブロック内でbreak文を記述した場合、それはswitch文自体に作用するものではなくなります。 つまりswitch文の外にループ文があった場合等で、そのループ文を脱出する作用しか持ちません。

これによりRarakuのcase部やdefault部ではいわゆるfall-throughのような挙動を勝手には行いません。 そのような処理を行いたい場合は後述するfallthrough文を使って明示的に指定します(これはGo言語におけるfallthrough文に似ています)。

Close


case部の指定とdefault部の指定は、不要ならばなくても構いません。 極端な話、以下のように「switch 条件文」の直後に「{}」をつければ、どちらの指定がないようなケースも許されます。 (その場合何もしませんので特に意味はない記述にはなりますが)。

varia ival = 2

switch ival {}

また、以下のようにdefault部の指定はcase部の指定よりも上にあっても構いません (ただしdefault部の指定を二つ以上書くことはできませんから、これより下はすべてcase文だけが来る形になります)。

varia ival = 2

switch ival {
default :
	Rrk_print( "when ival is other\n" )
case 0 :
	Rrk_print( "when ival is 0\n" )
case 1 :
	Rrk_print( "when ival is 1\n" )
}

if文では複数の条件式を次のようにまとめて分岐することができました。

varia ival = 2

if ival == 0 {
	Rrk_print( "when ival is 0\n" )
}: ival == 1 || ival == 2 {
	Rrk_print( "when ival is 1 or 2\n" )
}: ival == 3 || ival == 4 {
	Rrk_print( "when ival is 3 or 4\n" )
}:{
	Rrk_print( "when ival is other\n" )
}

switch文でもこれに相当する書き方は可能で、次のように「case 候補値リテラル」(ただし「:」はつかない)を必要な数だけ連続させた後に「:」を書き、 文の集合を続けます。

varia ival = 2

switch ival { 
case 0 :
	Rrk_print( "when ival is 0\n" )
case 1
case 2 :
	Rrk_print( "when ival is 1 or 2\n" )
case 3
case 4
:
	Rrk_print( "when ival is 3 or 4\n" )
default :
	Rrk_print( "when ival is other\n" )
}

あるいは「case 候補値リテラル,候補値リテラル」といった形で「候補値リテラル」を「,」区切りで必要な数だけ連続させた後に「:」を書き、 文の集合を続けます(最後の候補値リテラルの後ろのカンマはつけてもつけなくても構いません)。

varia ival = 2

switch ival {
case 0 :
	Rrk_print( "when ival is 0\n" )
case 1, 2 :
	Rrk_print( "when ival is 1 or 2\n" )
case
	3,
	4, /* 最後のカンマはあってもなくてもOK */
:
	Rrk_print( "when ival is 3 or 4\n" )
default :
	Rrk_print( "when ival is other\n" )
}

これに関して一つ注意事項があります。 Rarakuのswitch文では「case x :」と「case y :」が連続する並び(「case x : case y :」という並び)を記述することは許可されません。 例えば以下の例をご覧ください。

varia ival = 2

switch ival {
case 0 : /* Compile error. */
case 1 :
	Rrk_print( "when ival is 0 or 1\n" )
default :
	Rrk_print( "when ival is other\n" )
}

Rarakuではこれはコンパイルエラーとなります。 Rarakuでは「case 0 :」と(「:」を付けて)記述したからには、その直後に必ず何かしらの「文の集合」がなければなりませんが、 それが指定されず次の「case 1 :」が現れているためです(これは敢えてこういう仕様にしております。C言語やGo言語などに慣れた方はご注意ください)。

あるいは「case x :」と「default :」が連続する並び(「case x : default :」や「default : case x :」という並び)を記述することも同様に許可されません。 例えば以下はコンパイルエラーとなります。

varia ival = 2

switch ival {
case 0 :
	Rrk_print( "when ival is 0\n" )
case 1 :
default : /* Compile error. */
	Rrk_print( "when ival is 1 or other\n" )
}

既に述べた通り、これがもし「case 0」(「:」が付いていない)と「case 1 :」が連続する並び(「case 0 case 1 :」という並び)ならば問題ありません (その場合、「case 0,1 :」が指定されたのと同じです)。

あるいは「case 1」(「:」が付いていない)と「default :」が連続する並び(「case 1 default :」という並び)ならば問題ありません。 (その場合、1もしくはデフォルト(他のcaseで指定されてないその他の値)が候補値となります)。 ただしその逆の並び(「default case 0 :」という並び)は許可されません。switch文ではdefaultの後ろは必ず「:」が来なければならないからです。

C言語やGo言語などに慣れた方ですと一見問題ない文に見えるかもしれません。 ですが、このような並びはこれらの言語では全く別の意味で解釈されます。 C言語ではこのような並びは直後の処理に対する候補として、0と1が同時に指定されたものとみなされます。 一方、Go言語ではこのような並びはそれぞれ独立した候補値として解釈され、「case 0 :」の部分については何も処理されないものとみなされます。

それではRarakuでは、そのどちらで解釈すべきでしょうか? しかしどちらか一方に決めた瞬間、もう一方の言語に慣れたプログラマからすると意図せぬ動作になるかもしれません。 結局Rarakuでは両成敗し、このような表記を認めないのが一番安全と判断しました。 そのためコンパイルエラーになるようにしています。

代替となる表記はいくつかありますし、これを禁止しても使い勝手も特に問題ないと思います。 例えばC言語に近い形で書きたければ以下のように書くこともできます。

varia ival = 2

switch ival {
case 0   /* : をつけない */
case 1   /* : をつけない */
case 2 : /* 最後は : をつける(改行して:をつけても構いません) */
	Rrk_print( "when ival is 0 or 1 or 2\n" )
default :
	Rrk_print( "when ival is other\n" )
}

逆にGo言語に近い形(つまり何もしないような挙動)で書きたければ以下のように書くこともできます。

varia ival = 2

switch ival {
case 0 : ;   /* 明示的に ; をつける */
case 1 : {}  /* 明示的に{} をつける */
case
	2,
	3,
	: {} /* 明示的に{}をつける */
case 4 :
	Rrk_print( "when ival is 4\n" )
default : /* 一番最後が空文なら特に問題なし */
}

「;」や「{}」を付けることでそこに何も文がないことを明示して書きます。 ただし一番最後に来る「case x :」や「default :」のみ、明示的な「{}」や「;」を(書いても構いませんが)省略できます 。 もっとも「default :」の後ろが空ということは、最初から「default :」を書いてないのと実質的に同じではあります。

「候補値リテラル」にはvaria/const文で宣言した変数/定数を指定することはできません (これについてはifより明確に劣る性質になります)。 例えば以下のような記述はコンパイルエラーとなります。

varia ival = 2
const zero = 0

switch ival { 
case zero : /* (整数リテラルではなく)整数定数なのでコンパイルエラー */
	Rrk_print( "when ival is 0\n" )
default :
	Rrk_print( "when ival is other\n" )
}

switch文における暗黙のブロック


Rarakuのswitch文では、「文の集合」の前後に暗黙のうちに「{」と「}」が記述されているとみなされます。 つまりここには(字面には現れませんが本当は)ブロックが存在しています。 これにより、以下のように case 0 と case 1 において、同じ名前の(しかし本当は実体は別の)変数 x を宣言できることになります。

varia ival = 2

switch ival {
case 0  :
	varia x = 10
	Rrk_print( "when ival is 0\n" )
case 1  :
	varia x = 20 /* OK: case 0 内とはブロックが異なるため x の重複宣言とはならない */
	Rrk_print( "when ival is 1\n" )
default :
	Rrk_print( "when ival is other\n" )
}

上記では、「varia x = 10」の「x」が有効なスコープは「case 0 :」部の終了まで(次の「case 1 :」が現れるまで)です。 同様に、「varia x = 20」の「x」が有効なスコープは「case 1 :」部の終了まで(次の「default : 」が現れるまで)です。 switch文では「{」がない替わりに「:」でこのブロックを開始し、 「}」がない替わりに次に現れた「case(またはdefault)」でこのブロックが終了するとみなします。

念のために補足しますと、上記は概念的には以下のように書いたのと同じです。

varia ival = 2

switch ival {
case 0 :
{
	varia x = 10
	Rrk_print( "when ival is 0\n" )
}
case 1 :
{
	varia x = 20 /* OK: case 0 内とはブロックが異なるため x の重複宣言とはならない */
	Rrk_print( "when ival is 1\n" )
}
default :
{
	Rrk_print( "when ival is other\n" )
}
}

上記のように「文の集合」の中に無名ブロックを書いても構いません。 というより、Rarakuではわざわざ上記のように書かなくても自動的に上記のような「{」と「}」が暗黙のうちに挿入されるという理解で 通常は問題ありません(ただし中身が空の場合のみ、明示的に「{}」か「;」を書かなければなりません)。

また見方を変えれば以下のように、case 0 で x を宣言していたとしても、 case 1 ではそれはまだ宣言されていない変数とみなされることになります(この場合コンパイルエラーです)。

varia ival = 2

switch ival {
case 0  :
	varia x = 10
	Rrk_print( "when ival is 0\n" )
case 1  :
	varia y = x /* コンパイルエラー: x はまだ宣言されていない */
	Rrk_print( "when ival is 1\n" )
default :
	Rrk_print( "when ival is other\n" )
}

念のために先ほどと同様に補足しますと、上記は以下のように書いたのと同じです。

varia ival = 2

switch ival {
case 0 :
{
	varia x = 10
	Rrk_print( "when ival is 0\n" )
}
case 1 :
{
	varia y = x /* コンパイルエラー: x はまだ宣言されていない */
	Rrk_print( "when ival is 1\n" )
}
default :
{
	Rrk_print( "when ival is other\n" )
}
}

文字列に関するswitch文


ここまでのswitch文では条件式に整数変数を与えるものを見てきました。 Rarakuのswitch文では文字列を与えることもできます。

まずif 文を使って次のように文字列の値に応じて分岐する処理を考えます。

varia str = "aaa"

if str == "aaa" {
	Rrk_print( "when str is aaa\n" )
}: str == "bbb" {
	Rrk_print( "when str is bbb\n" )
}: str == "ccc" {
	Rrk_print( "when str is ccc\n" )
}: str == "ddd" {
	Rrk_print( "when str is ddd\n" )
}: str == "eee" {
	Rrk_print( "when str is eee\n" )
}:{
	Rrk_print( "when str is other\n" )
}

これをswitch文で書き換えると、整数の時と全く同様に以下のようになります。

varia str = "aaa"

switch str {
case "aaa" : Rrk_print( "when str is aaa\n" )
case "bbb" : Rrk_print( "when str is bbb\n" )
case "ccc" : Rrk_print( "when str is ccc\n" )
case "ddd" : Rrk_print( "when str is ddd\n" )
case "eee" : Rrk_print( "when str is eee\n" )
default    : Rrk_print( "when str is other\n" )
}

文字列の場合においても、基本的に整数のときと同じルールが当てはまります。 候補値リテラルで指定する文字列リテラルは同じものが重複していてはいけません。 また候補値リテラルではvaria/const文で宣言した変数/定数を指定することはできません。

文字列についても複数の値について、次のようにまとめて分岐することができます。

varia str = "aaa"

switch str {
case "aaa"
case "bbb"
	:
	Rrk_print( "when str is aaa or bbb\n" )
case
	"ccc",
	"ddd",
	"eee",
	:
	Rrk_print( "when str is ccc or ddd or eee\n" )
default :
	Rrk_print( "when str is other\n" )
}

列挙型に関するswitch文


Rarakuのswitch文では列挙型を与えることもできます。

まずif 文を使って次のように列挙型の値に応じて分岐する処理を考えます。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}
varia veg Vegetable = Onion

if veg == Tomato {
	Rrk_print( "when veg is Tomato\n" )
}: veg == Onion {
	Rrk_print( "when veg is Onion\n" )
}: veg == Lettuce {
	Rrk_print( "when veg is Lettuce\n" )
}: veg == Carrot {
	Rrk_print( "when veg is Carrot\n" )
}: veg == Potato {
	Rrk_print( "when veg is Potato\n" )
}:{
	Rrk_print( "when veg is other\n" )
}

これをswitch文で書き換えると、整数の時と全く同様に以下のようになります。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Carrot
	Potato
	Broccoli
	Burdock
	Pumpkin
}
varia veg Vegetable = Onion

switch veg {
case Tomato  : Rrk_print( "when veg is Tomato\n" )
case Onion   : Rrk_print( "when veg is Onion\n" )
case Lettuce : Rrk_print( "when veg is Lettuce\n" )
case Carrot  : Rrk_print( "when veg is Carrot\n" )
case Potato  : Rrk_print( "when veg is Potato\n" )
default      : Rrk_print( "when veg is other\n" )
}

列挙型の場合においても、基本的に整数のときと同じルールが当てはまります。 候補値リテラルではvaria/const文で宣言した変数/定数を指定することはできません。

列挙型の場合、注意しなければならないのは候補値リテラルで指定する列挙子の割り振られた整数値です。 この整数値が同じ列挙子が複数指定されていてはいけません。 例えば以下の例をご覧ください。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Pumpkin = 0
}
varia veg Vegetable = Pumpkin

switch veg {
case Tomato  : /* 値は0 */
	Rrk_print( "when veg is Tomato\n" )
case Onion   :
	Rrk_print( "when veg is Onion\n" )
case Lettuce :
	Rrk_print( "when veg is Lettuce\n" )
case Pumpkin : /* コンパイルエラー : Pumpkinに割り振られた整数値はTomatoに割り振られた整数値と同じ */
	Rrk_print( "when veg is Pumpkin\n" )
default :
	Rrk_print( "when veg is other\n" )
}

上の例では、TomatoとPumpkinは同じ 0 という整数値が割り振られています。 このとき、「case Tomato」と「case Pumpkin」も重複した指定とみなされ、コンパイルエラーとなります。

では割り振られた整数値が同じであっても名前が異なる場合は別の列挙子とみなしてswitch文を実行したい場合はどうすればよいでしょうか? Rarakuではswitchの条件式に文字列型を与えた場合、特例として候補値リテラルに列挙子も与えることが許されており、 その場合、候補値リテラルには「列挙子の名前を値とする文字列リテラル」が指定されたとみなされます。

例えば以下をご覧ください。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Pumpkin = 0
}
varia veg Vegetable = Pumpkin

switch veg->conststr { /* 列挙型変数をconststr型にキャスト */
case Tomato  : /* OK: 文字列リテラル"Tomato"が指定されたものとみなされる */
	Rrk_print( "when veg is Tomato\n" )
case Onion   : /* OK: 文字列リテラル"Onion"が指定されたものとみなされる */
	Rrk_print( "when veg is Onion\n" )
case Lettuce : /* OK: 文字列リテラル"Lettuce"が指定されたものとみなされる */
	Rrk_print( "when veg is Lettuce\n" )
case Pumpkin : /* OK: 文字列リテラル"Pumpkin"が指定されたものとみなされる */
	Rrk_print( "when veg is Pumpkin\n" )
default :
	Rrk_print( "when veg is other\n" )
}

上の例のように列挙型変数vegをconststr型にキャストすると、switch文の条件式には文字列型が指定されたことになり、 その値はvegに格納されている列挙子の名前となります。 このとき、case 候補値リテラルに指定されたすべての列挙子は強制的に文字列リテラルに変換され、 それらの値もまた各列挙子の名前になります。 結果的に上の例は、列挙子が割り振られた整数値ではなく、その名前の文字列を使ったswitch文として実行されます。 これにより、同じ整数値が割り振られたTomatoとPumpkinについても異なる処理に分岐することができるわけです。

ただしこのような列挙子の指定を行う場合でも、列挙子はすべて同じ列挙型である必要があります。 つまり一つでも異なる列挙型の列挙子が混じっている場合はコンパイルエラーとなります。

以下のようにswitchの条件式に整数型を指定し、候補値リテラルに列挙子を指定するといったことはできません。

enum Vegetable {
	Tomato
	Onion
	Lettuce
}
varia ival = 2

switch ival { /* コンパイルエラー: 整数型ivalに対して、候補値がVegetable型 */
case Tomato  : Rrk_print( "when veg is Tomato\n" )
case Onion   : Rrk_print( "when veg is Onion\n" )
case Lettuce : Rrk_print( "when veg is Lettuce\n" )
default      : Rrk_print( "when veg is other\n" )
}

このような処理を行いたい場合、switchの条件式の方をまず列挙型にキャストする必要があります。

enum Vegetable {
	Tomato
	Onion
	Lettuce
}
varia ival = 2

switch ival->Vegetable { /* OK: 整数型ivalを列挙型Vegetableにキャスト */
case Tomato  : Rrk_print( "when veg is Tomato\n" )
case Onion   : Rrk_print( "when veg is Onion\n" )
case Lettuce : Rrk_print( "when veg is Lettuce\n" )
default      : Rrk_print( "when veg is other\n" )
}


switch文において列挙型を使用し、かつそれにdefault部がついていない場合、 その列挙型のすべての列挙子はcase部により網羅されていなければなりません。 さもなければコンパイルエラーとなります。 例えば以下の例をご覧ください。

enum Vegetable {
	Tomato
	Onion
	Lettuce
	Pumpkin
}
varia veg Vegetable = Pumpkin

switch veg {
case Tomato  :
	Rrk_print( "when veg is Tomato\n" )
case Onion   :
	Rrk_print( "when veg is Onion\n" )
case Lettuce :
	Rrk_print( "when veg is Lettuce\n" )
} /* without default */

上記ではswitch文に列挙型を使用し、かつdefault部がなく、かつ列挙子「Pumpkin」がcase部でカバーされておりません。 このような場合「Pumpkinが欠けている」といった旨のコンパイルエラーが表示されます。

尚、これがコンパイルエラーとなるのは列挙型を使用したswitch文のみです。 列挙型以外を使用したswitch文の場合はこのような特別な仕様は存在せず、 default部がなくてもコンパイルエラーとはなりません。

このように書くと「要するにswitch文で列挙型を使用する場合は忘れずにdefault部を付ければよいのだな」と思われるかもしれませんが、 そうではありません。 これがコンパイルエラーとなる理由を理解しておくことが重要です。

例えばVegetableに新しい列挙子Broccoliを追加した場合、通常はこのVegetable型を使用しているswitch文においても その列挙子Broccoliに関する処理を追加しなければなりません。 しかしVegetableの宣言とこれを実際に使っているswitch文の距離が遠い場合やこれらが別ファイルになっている場合、 switch文の方において修正漏れが発生すると思われます。 switch文において敢えてdefault部を付けないようにすることによって、 コンパイルエラーという形を介してこのような修正漏れをいち早く発見することができます。 そのため、switch文でその列挙型の列挙子をすべて網羅することが極めて重要なケースでは、 むしろdefault部を付けないようにするべきです。

例えば、パーザや字句解析器などを作成する場合、トークンの種類を列挙型として宣言し、 その列挙型をswitch文に与えることでトークンの種類毎に切り分けて処理するようなことがよく行われます。 このような用途では、それを使っているswitch文でもすべての列挙子に関する記述を漏れなく網羅しなければなりません。

一方、switch文ですべての列挙子を網羅する必要のないケースもあります。 例えばOSの種類が列挙型になっているような場合、 そのすべてのOSをサポートするようなアプリでもない限り、すべての列挙子を網羅する必要はないと思われます。 そのような場合はdefault部を付けておくとよいでしょう。
C言語でもdefault部にassert(0)などを指定することで、このような修正漏れを検出する方法もあります。 しかしその方法は(assertマクロですので)デバッグモードでコンパイルしなければならない上、 実行時になって初めてそれが発覚するというものになります。

一方、Rarakuのswitch文では上記のように敢えてdefault部を付けないようにすることによって、 (列挙型の場合に限られはしますが)コンパイル時に静的にこれを検出できます。

文字整数リテラルに関するswitch文


整数型を使ったswitch文では候補値リテラルに文字整数リテラルを与えることができます。 例えば以下の通りです。

import std/str /* for RrkStr_at */

varia str = "apple"
varia ch = RrkStr_at( str, 0 )

switch ch {
case 'a' : Rrk_print( "String starting with 'a'\n" )
case 'b' : Rrk_print( "String starting with 'b'\n" )
case 'c'
case 'd' : Rrk_print( "String starting with 'c' or 'd'\n" )
default  : Rrk_print( "String starting with other\n" )
}

上の例における「'a'」は、アルファベットの「a」のASCII文字コード、すなわち整数97の別名にすぎません。 「'b'」「'c'」「'd'」なども同様です。

Jumpテーブルにより最適化される場合


switch文では状況によっては処理速度が向上する場合があります。 まずは以下のif文の例をご覧ください。

varia ival = 2

if ival == 0 {
	Rrk_print( "when ival is 0\n" )
}: ival == 1 {
	Rrk_print( "when ival is 1\n" )
}: ival == 2 {
	Rrk_print( "when ival is 2\n" )
}: ival == 3 {
	Rrk_print( "when ival is 3\n" )
}: ival == 4 {
	Rrk_print( "when ival is 4\n" )
}:{
	Rrk_print( "when ival is other\n" )
}

if文を使ったこのような処理では、一番上にある「ival == 0」の条件式は最初に評価されますが、 一番最後にある「ival == 4」については、それより上の条件式の評価を全て行った後に評価されます。 つまり ival の値が 4 のとき、このif文はRaraku VMにおける整数比較演算命令が4回行われ、一応理論上はもっとも遅くなるわけです。 この条件式がもっと増えれば後ろに来る条件式の評価が完了するのはそれだけ遅くなります。

とはいえ整数型の比較演算が高々4個程度であれば、実際は全く気にするほどのことではありません。 上で「一応理論上は」と書いたのもそのためで、ここではあくまで今回のテーマでの比較説明のためこのような「遅い」や「高速化」といった 少しオーバーな表現を使うことにします。

ではswitch文ではどうなるでしょうか?

switch文でも基本的には if文と同じように上から順番に条件式が評価されます。 すなわち上にあるcaseの条件から順に評価され、下にあるcaseの条件になればなるほどそこへ到達するための処理が増えます (これをRound-robin方式と呼びます)。

しかしswitch文の場合、条件式の型と候補値リテラルの指定方法(詳しくは後述しますが)によっては、最適化処理が行われる場合があります。 この最適化が行われた場合、すべてのcase文について同じ命令数で到達させることができ、Round-robin方式より高速化することが期待できます (これをJumpテーブル方式と呼びます)。 これにより一番下のcaseであっても一番上のcaseと同等の速度で到達できることになります。

以下は上の例と全く同じ処理をswitch文で書いたものです。

varia ival = 2

switch ival {
case 0  : Rrk_print( "when ival is 0\n" )
case 1  : Rrk_print( "when ival is 1\n" )
case 2  : Rrk_print( "when ival is 2\n" )
case 3  : Rrk_print( "when ival is 3\n" )
case 4  : Rrk_print( "when ival is 4\n" )
default : Rrk_print( "when ival is other\n" )
}

このswitch文の条件式は整数型になっています。 またこの場合の候補値リテラルの最小値と最大値に注目してください。 値は 0, 1, 2, 3, 4 が使われていますので、最小値は 0、最大値は 4 となります。 次に table_size = 最大値 - 最小値 + 1 を計算します。 今回の場合 table_size = 4 - 0 + 1 = 5 となります。 このtable_sizeの値が 256 以下の場合、Rarakuのswitch文はJumpテーブル方式による最適化を行います。 この例では 5 は 256 以下ですのでJumpテーブル方式が適用可能となります。

Jumpテーブル方式の場合、まず delta = switch文の条件式(整数) - 候補値リテラルの最小値(整数)を計算します。 これはいわばJumpすべき距離を表します。 次に dst = 現在のプログラムカウンタ(整数) + delta 候補値リテラルの値(整数)を計算します。 これはJumpすべき目的地の位置を表します。 最後に Raraku VMのJump命令に落とし込んで、目的地となるcaseへジャンプします。

switch文の条件式が列挙型の場合も同様にJumpテーブル方式を適用できる場合があります。 この場合は、列挙子に割り振られた整数値を参照して、table_size = 最大値 - 最小値 + 1 を計算します。 そしてそれが 256 以下の場合、Jumpテーブル方式による最適化を行います。

ではswitch文の条件式が文字列型の場合はどうでしょうか? この場合は整数として扱えませんのでJumpテーブル方式による最適化は行われません。 つまり文字列型のswitch文の場合、if文を使ったそれとパフォーマンスは全く同じになります。

このようなJumpテーブルの最適化は、実際のところcaseの数が高々数個のswitch文を少ない回数実行する程度の状況では必要ないと思います。 しかしcaseの数が大量に存在するswitch文をループ文で膨大な回数繰り返し実行するような状況では多少の効果があるかもしれません。 例えば以下のような文字整数リテラルで分岐するような処理を考えます(字句解析器などを自作するような状況ではあり得るかもしれません)。

for ;; {
	varia ch int = getChar() /* なんらかの文字コードを取得する関数 */
	switch ch {
	case 'a', 'A' :
		/* something about 'a' */
	case 'b', 'B' :
		/* something about 'b' */
	case 'c', 'C' :
		/* something about 'c' */

	/* …途中省略… */

	case 'z', 'Z' :
		/* something about 'z' */
	default :
		Rrk_print( "Error : Invalid character.\n" )
		break /* このbreakはfor文を抜けるためのbreak */
	}
}

この例の場合、case文に指定する候補値リテラルは(アルファベットの大文字小文字の総数ですから)52個もあります。 chの値が'z'や'Z'の場合、Jumpテーブル方式ではない場合(Round robin方式)だと毎回約50回もの評価を行う必要があり、 さらにこれをループ文で膨大な回数実行するような状況の場合その分無駄が倍増します。 そのような場合はJumpテーブルによる最適化が効いてくるかもしれません。

この例のように候補値リテラルとしてASCII文字による文字整数リテラルを指定した場合、 Jumpテーブル方式による最適化は確実に行われると見ていいでしょう。 ASCII文字コードは1byteの整数で表現可能で、その場合のtable_sizeは確実に256以下となるからです。

fallthrough文


多くのプログラミング言語におけるswitch文では、ある一つのcase/defaultを実行した直後、 そのすぐ下にあるcase/defaultを実行するようなことができます。 このような挙動は一般的にfall-throughなどと呼ばれ、極稀にこの挙動が便利なことがあります。

Rarakuでこれを行うためには、caseブロックまたはblockブロック内でfallthrough文を記述します。 例えば以下の例をご覧ください。

import std/str

varia text = @here[
	Hello world
	Thanks
]here

varia count_line   = 0
varia count_ws_ch  = 0
varia count_std_ch = 0

for i:=0u; i<RrkStr_leng(text); ++i {
	switch RrkStr_at(text,i) {
	case '\n' :
		 ++count_line;
		 fallthrough /* すぐ下にあるcase(またはdefault)を続けて実行 */
	case 
		'\r',
		'\t', ' ',
		:
		 ++count_ws_ch;
	default :
		 ++count_std_ch;
	}
}
Rrk_print( "count_line="   count_line   "\n" )
Rrk_print( "count_ws_ch="  count_ws_ch  "\n" )
Rrk_print( "count_std_ch=" count_std_ch "\n" )

上の例は3つのことを同時に行っています。 まずtextにおける行数を数え、その結果をcount_lineに格納します。 また同時にtextにおける改行コード、タブ、スペースの文字の数を数え、その結果をcount_ws_chに格納します。 また同時にそれら以外の文字の数を数え、その結果をcount_std_chに格納します。

最初のcaseブロックで ++count_line が実行され、さらにfallthrough 文によりすぐ下のcaseブロックへ移動し、 ++count_ws_ch も実行されます。 これにより、改行コード '\n' に対して、++count_line と ++count_ws_ch の二つの処理を同時に行わせることができるわけです。

この例の実行結果は以下のようになります。

count_line=2
count_ws_ch=5
count_std_ch=16

ただ、このような処理はfallthrough文を使わずに記述することができる場合もあります。 例えば上記の場合、以下のように書くこともできます。

import std/str

varia count_line   = 0
varia text = @here[
	Hello world
	Thanks
]here

varia count_ws_ch  = 0
varia count_std_ch = 0

for i:=0u; i<RrkStr_leng(text); ++i {
	const ch = RrkStr_at(text,i)
	switch ch {
	case
		'\n',
		'\r',
		'\t', ' ',
		:
		if ch == '\n' { /* ここでもう一度改行コードを判定 */
			++count_line;
		}
		++count_ws_ch;
	default :
		++count_std_ch;
	}
}
Rrk_print( "count_line="   count_line   "\n" )
Rrk_print( "count_ws_ch="  count_ws_ch  "\n" )
Rrk_print( "count_std_ch=" count_std_ch "\n" )

上記の場合、改行コード '\n' に対する判定が2度発生するため、一旦その内容を変数chに格納しています。 またif文がcaseブロック内に入り込むため一段階ブロックの階層が深くなりますが、 この程度であればさほど気にするほどのことではないようにも思えます。 もう少し複雑な例になればfallthrough文がより優位に見えることもあるかもしれません。 場合に応じて使いやすい方法を選びましょう。

参考: fallthrough文に関するその他の補足事項
fallthrough文はswitch文(のcaseブロックまたはdefaultブロック)内でのみ使うことができます。 それらに囲まれていないブロック内でこれを使った場合はコンパイルエラーとなります。 これはcontinue文やbreak文がループ文(for文、foreach文、while文)のブロック内でしか使うことができないのと同様です。

またRarakuにおけるfallthrough文はcontinue文やbreak文と同じくそれが実行された時点でそのブロック内における実行を一旦打ち切ります。 例えば次の例をご覧ください。

varia kind = 0
switch kind {
case 0 :
	Rrk_print( "case0 : begin\n" )
	fallthrough
	Rrk_print( "case0 : end\n" )
default :
	Rrk_print( "default\n" )
}

上記ではまず最初のcase 0ブロックに入り、内部でfallthroughが実行されます。 そこでcase 0ブロックの処理が中断され、すぐ下のdefaultブロックへと移動します。 つまり case 0ブロックの最後にある「Rrk_print( "case0 : end\n" )」は実行されません。 この例の実行結果は以下のようになります。

case0 : begin
default

defaultブロック内のfallthrough文からcaseブロックへの移動も可能です。 例えば以下の通りです。

varia kind = 1
switch kind {
default :
	Rrk_print( "default\n" )
	fallthrough /* すぐ下のcaseブロックへ移動 */
case 0 :
	Rrk_print( "case0\n" )
}

上の例ではまずdefaultブロックに入りますが、これはcaseブロックより上にあります。 そこでfallthrough文を実行するとそのすぐ下にあるcaseブロックへ移動します。 この例の実行結果は以下のようになります。

default
case0

switch文の最後にあるcase/defaultブロック内でfallthrough文を実行した場合、そのswitch文から離脱します。 例えば以下の通りです。

varia kind = 0
switch kind {
case 0 : {}
default :
	fallthrough /* switch文の直後へ移動 */
}
/* switch文の直後 */

上記では defaultブロックはswitch文の最後にありますから、fallthrough文を実行した瞬間にそのブロックの実行を打ち切り、 switchの直後へ移動します。

Close



switch文のまとめ


Rarakuでは単純に数値リテラルや文字列リテラル、列挙子に等しいかを地道にチェックしながら分岐処理を行うような場合で、 しかもその分岐の個数が多い場合ならば、if文よりswitch文の方がスッキリ書けます。 さらにswitch文はif文と同等かそれより高速になる状況もあります。

一方、分岐に複雑な条件式が絡む場合や、変数と変数を比較しなければならない場合や、 その他分岐が少なくif文を使う方が素直な状況ではif文の方がよいでしょう。

さて、これでRarakuの基本的な機能の説明の半分が終了しました。 ここまででもRarakuでプログラミングをすることはそれなりにできますが、 Rarakuにはさらなる機能があります。 それについてはRaraku言語マニュアル(後編)をお読み下さい。

目次に戻る

This article was written by:
none image

Mr.Moai

@znk project