Julia言語で入門するプログラミング(その1)

はじめに

この記事は、プログラミングへの入門記事だ。初めてプログラミングをしてみようという人を対象にしている。非常に長い記事になりそうなので、何回かに分ける予定で、その第一回がこの記事となる。

使用するプログラミング言語としては、「Julia」を採用した。何らかのプログラミング言語の経験がある人も歓迎だ。Juliaは強力な言語なので、何か得られるものがあるはずだ。

題材としては、簡単なゲームの作成を行う。極めてシンプルなゲームからスタートして、徐々に機能を拡張させながら、Juliaの機能やソフトウェアの原則について学んでいく。まずはこの記事でプログラミングの基本を学び、その後により難しい問題に挑戦してもらいたい。

なぜJuliaか

Juliaという言語は、あまり名前を聞かない言語かもしれない。だがそれはJuliaが劣っているからではない。単にJuliaが若い言語だからだ。なぜJuliaを採用したか、理由はいくつかある。

Juliaはインストールが簡単だ

何と言ってもこれだ。独学者にとって、これはとても大事なことだ。入門者であれば尚更だ。環境構築でつまづくと何もかも嫌になってしまうものだ。Juliaはインストールが簡単なので、間違えようがないのだ。

Juliaは親しみやすい言語だ

Juliaは書いていて楽しい。とても楽しい。最小限の指示で、期待通りのことをこなしてくれる。快適さはあらゆる言語の中でもトップクラスだ。

Juliaは強力な言語だ

親しみやすいだけでなく、Juliaは非常に強力だ。贔屓目なしに、現存するプログラミングでも屈指の強力な機能を備えている。初心者向けのオモチャの言語ではないのだ。

Juliaはシンプルな言語だ

親しみやすく強力な言語でありながら、Juliaの提供している機能は驚くほどシンプルだ。余計なものが何一つなく、研ぎ澄まされている。

Juliaは高速な言語だ

ここまでのメリットがありながら、そのうえJuliaは高速に動作する。その速度はあらゆる言語の中でもトップクラスに位置する。親しみやすい言語は、しばしば速度が遅いのが欠点だ。これらは従来はトレードオフだと考えられてきたが、Juliaはそれを覆した。

要するに、Juliaにはデメリットらしきものが何もないのである。唯一の心配は、Juliaが快適すぎて他の言語を学ぼうという意欲が失せないかどうかということくらいだ。

なぜゲームか

題材としてゲームを取り上げている理由は、単純に面白いからだ。みんなゲームをするのは好きだよね?

プログラミングをすることで、自分の考えたゲームをデザインし、動かすことができるのだ。これはゲームをするのに劣らず楽しいな体験だ。

この記事で取り扱わない内容

今のうちに注意事項を述べておく。この入門コーナーでは、以下の要素は取り扱わない。

グラフィカルユーザーインターフェース

グラフィカルユーザーインターフェース(GUI)は取り扱わない。GUIとは、「アプリ」と言われてあなたが頭に思い浮かべたものだ。LineやらTwitterやらExcelやらのアプリを起動したら表示される、あの画面のことだ。もしくは、Webブラウザで動作するWebアプリを思い浮かべてもらっても良い。ユーザーがマウスクリックや画面タッチで操作し、アプリがそれに応答するという概念だ。

今や当たり前すぎて、名前がついているのが不思議なくらいな概念かもしれない。GUIは、一般ユーザーに受けいられるにはもはや必須と言える。

一方で、GUIのアプリケーションは難しい。GUIアプリケーション作成のためのツールは整えられているのだが、ツールを使いこなすのが難しい。入門者がいきなりGUIアプリケーションに取り組むと、2つのことを同時に学ぶ必要がある。プログラミング言語の学習と、ツールの学習だ。片方だけでも難しいのだ。いきなり両方はやめておいたほうがいい。そのようなわけで、まずはGUIアプリケーションのことは忘れよう。私と一緒にプログラミング言語を学ぼう。GUIアプリに取り組むのはそれからだって大丈夫だ。結局はそのほうが近道のはずだ。

科学技術計算

Juliaは科学技術計算向け言語として生まれたので、それを期待されるかもしれない。残念ながら、この記事では科学技術計算の例題は存在しない。

Julia言語の網羅的な解説

Juliaの機能についての網羅的な解説はしない。Juliaは公式ドキュメントが非常に充実しているので、必要であれば十分な情報が手に入る。ただ、公式ドキュメントは厳密であるがゆえに少し難しい用語が使われたりしているので、そういった用語を理解するための基礎知識を付けられるようにはしようと思う。

その他の注意事項

私は話が長い。キリリと引き締まった解説が読みたい人は、別の文章を読もう!

インストール

Juliaのインストールはとても簡単だ。公式サイトに行き、インストーラを落としてくるだけだ。

公式サイト

英語のサイトだからと言ってびっくりしないように。いや、びっくりしてもいいのだが、怖気付いて退散しないように。公式サイトにジャンプすると、現時点では左上にDownloadというボタンがある。下記の画像はv1.5.3とあるが、そこは変わっていくのであまり気にしなくていい。

f:id:muuuminsan:20201118000422p:plain
Julia公式サイトトップページ

Downloadボタンを押すと、相変わらず英語のページなのだが、少し下に移動すると、WindowsとかmacOSとか書いてある、OS別のインストーラが置いてあるページにたどり着くはずだ。自分のOSにあったものをダウンロードしよう。自分のOSがわからない人はいるかな?そんなあなたはまず間違いなくWindowsだろう。macLinuxなら好きで使っているはずだ。おそらく32bitか64bitかもわからないと思うので、「Windows ビット数 調べ方」で検索しよう。いい記事がたくさん出てくるはずだ。

さてダウンロードが終わったら、インストーラを起動してインストールしよう。選択肢を聞かれるかもしれないが、デフォルトでOKだ。もちろん、こだわりのある人は変えたらいいが、よくわからなければデフォルトでOKだ。困ったことにはならない。

他のサイトを参考にしている場合、パスを通したほうがいいとか、他のテキストエディタやツール類をインストールすべしとかいう情報を見るかもしれない。したければしたらいいが、よくわからなければ一旦忘れよう。必要であれば後でできる。困ったことにはならない。しても困ることもないのだが、最初に意味もわからず色々作業すると、何となく手の内に収まらない感じがして不安になってくるものだ。

Hello World

インストールは終わっただろうか?いよいよ、Juliaでのプログラミングを始めることができる。まずは、インストールされたJuliaアプリケーションを起動しよう。

最初に強調しておきたいのは、プログラミングは本や文章を追うだけではだめで、自分で手を動かすということが非常に重要であるということだ。できれば自分の手で打ち込んで欲しいし、コードをコピーしてもいいが、必ず少しは修正してあれこれ実験してみて欲しいと思う。

REPL

Juliaを起動すると、次のような画面が表示されたと思う。これがREPLと呼ばれるものだ。名前の由来はおいおい話すが、よく使われる単語なので覚えておこう。というか、この記事でもよく使うのですぐに覚えるだろう。

f:id:muuuminsan:20201118003937p:plain
JuliaのREPL

REPLではJuliaと「対話」することができる。REPLに次のように入力しよう。

julia> println("Hello World")

すると、Juliaが応答を返してくれるはずだ。

julia> println("Hello World")
Hello World

Hello World成功だ!あっけなかったかもしれない。そう感じたら、それだけ現代のソフトウェア開発環境が良くできているということだ。え?これって一体何なのかって?Hello Worldが何なのか気になるんだね?

その昔はコンピュータのセットアップは大仕事だった。何日もかけてハードウェアを組み立てる必要があっただろうし、ソフトウェアのインストールだって一筋縄ではないかない。試行錯誤末にようやく一台組み上げたのだ。苦労に苦労を重ねてついにコンピュータが外の世界とつながり、”Hello World”としゃべってくれたのだ。その喜びはひとしおだっただろう。赤ん坊がおぎゃあと産まれるようなものだ。母性か父性か、とにかく大きく崇高な感情に包まれたことだろう。ちなみに、完全に想像で語っている。

そのようなわけで、プログラミング言語の開発環境をセットアップしたら、とりあえず伝統的に最初にこれをやることになっているのだ。インストーラ一発で開発環境が構築できてしまう時代にHello Worldという文言は少し大袈裟な気もするが、まあ現代でも地鎮祭をやっているし、それと似たようなものだろう。(ちょっと違うかな?)

Juliaとの対話

ともかくあなたはHello Worldを成し遂げたわけだし、ついでにもう少しJuliaと対話してみよう。

簡単な四則計算

いろいろな計算式を入れてエンターキーを押すと、結果を喋ってくれる。

julia> 2 + 3
5

julia> 2 - 3
-1

julia> 2 * 3
6

julia> 2 / 3
0.6666666666666666

四則計算は完璧だ。ちなみに珍しいところで、次のような計算もできる。バックスラッシュ記号で、分子と分母が逆になるのだ。

julia> 2 \ 3
1.5

次の結果がどうなるかは、ぜひ自分の手でJuliaに聞いて欲しい。

julia> 2 / 3 \ 6

優先順位づけの括弧ももちろん使うことができる。

julia> (2 / 3) \ 6
9.0

julia> 2 / (3 \ 6)
1.0

"ans"とJuliaに言うと、直前の結果だと解釈してくれる。

julia> 1 + 1
2

julia> ans
2

julia> ans + 1
3

julia> ans * 2
6

ま、このくらいでいいだろう。ここまではまだ電卓と大差ない。我々はプログラミング言語をインストールしたのだ。もっと先へ進もう。次からいよいよプログラミング言語らしくなってくるのだ。もちろん、ここらで一休みするのもいい。

基本文法

これからJuliaの基本的な文法について説明する。目的は文法について詳細まで網羅することではない。まずは最低限のゲーム作成のために最低限必要な部分だけ話していく。ゲームを作り始めたら、その中で、基本文法の中でも説明しなかったような箇所や、高度な文法にも徐々に触れていく。

変数

変数という言葉は読んで字の如く「変化する数」ということだ。プログラミング言語では、変数という仕組みで数に名前をつけることができる。たとえば、次の例は、xという名前の変数を作り、そこに入れた値を色々と変化させている。一度Juliaに変数を教えると、変数の値を覚えておいてくれるのだ。

まずJuliaにx = 1と教える。ここで重要なのは、イコール記号は数学のように「xは1に等しい」と宣言したわけではなく、あくまで「xという名前の変数に1という値を設定する」という意味だということだ。

julia> x = 1
1

次にJuliaにxってなに?と聞く。すると1と答えてくれる。

julia> x
1

次にJuliaにx = 2と教える。繰り返すが、イコール記号は数学のように「xが何かに等しい」と宣言したわけではないので、先ほどのx = 1とは矛盾しない。あくまで「xという名前の変数に2を設定する」という意味だ

julia> x = 2
2

そしてJuliaにxってなに?と聞く。すると2と答えてくれる。

julia> x
2

次にJuliaにx + 1ってなに?と聞く。すると賢いJuliaはxが2だったことを覚えてくれていて、3と答えてくれる。そして、xは相変わらず2だ。

julia> x + 1
3

julia> x
2

次にJuliaにx = x + 1と教える。しつこいようだが、イコール記号は数学のように「x が x + 1 と等しい」と宣言したわけではない。あくまで「xという名前の変数に、x + 1(=3)という値を設定する」という意味だ。

julia> x = x + 1
3

そしてJuliaにxってなに?と聞く。Juliaは3と答える。

julia> x
3

だんだんプログラミングっぽくなってきただろう。さて、x = 1は、「xという名前の変数に1という値を設定する」と表現したが、同じことを差して、「xに1を代入する」という言い方をすることもある。というか、この方が主流だ。

ところで、Juliaの特徴に、変数はアルファベットや数字に限らないというものがある。多くの言語は、変数に使えるのはアルファベットと数字と一部の記号くらいのものだが、Juliaはギリシャ文字やひらがな、漢字も使える。だから、こんなこともできる。

julia> いち = 1
1

julia> に = 2
2

julia> いち + に
3

これから先は、日本語による変数名など必要に応じて積極的に使っていく。便利な機能は何だって使っていこう。

文字列

変数について話したので、文字と文字列について話しておこう。文字とは文字のことであり、文字列とは列をなした文字のことである。人を馬鹿にしているのかというような説明なので、Hello Worldプログラムを思い出そう。

julia> println("Hello World")
Hello World

""で囲まれた部分が文字列だ。そして、それら一つ一つを構成するのが文字である。

文字を作るにはシングルクオーテーションで囲う。この場合、xという文字そのものを作っている。

julia> 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

2つ以上の文字をシングルクオーテーションで囲うとエラーになる。

julia> 'xy'
ERROR: syntax: character literal contains multiple characters

2つ以上の文字を表現したいときには、ダブルクオーテーションで囲む。そうすると、文字列となる。

julia> "xy"
"xy"

先ほど、変数はxのように文字で宣言したのだった。しかし、これは文字'x'や、文字列"x"とは違う。

変数xには値を代入することができるが、文字'x'や文字列"x"には値を代入できない。

julia> x = 1
1

julia> 'x' = 1
ERROR: syntax: invalid assignment location "Char(0x78000000)" around REPL[2]:1

julia> "x" = 1
ERROR: syntax: invalid assignment location ""x"" around REPL[3]:1

こういったエラーメッセージが出てきたとき、怖くなって画面を閉じてはいけない。エラーメッセージをきちんと読んで理解しようとする姿勢があるかないかが勝敗を分ける。英語だからといって思考停止してはいけない。最近は良い翻訳ソフトもあるのだ。現時点ではDeepL翻訳がおすすめだ。

試しに下の方のメッセージをDeepL翻訳にかけると、「ERROR: 構文: REPL[3]:1 の周りの ""x"" の代入場所が無効です。」となる。代入をしようとして怒られたことがわかるだろう。文字や文字列は、代入の右辺に来る存在だ。下記の例では変数aに文字'x'や文字列"x"を代入している。

julia> a = 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> a
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)

julia> a = "x"
"x"

julia> a
"x"

文字を連結して文字列にすることができる。文字や文字列の連結には、*記号を使う

julia> 'a' * 'b'
"ab"

julia> "x" * "y"
"xy"

julia> "xy" * 'z'
"xyz"

文字列の何番目かを指定して文字を取得することもできる。指定するには[]で囲んだ数字を使う。普通のプログラミング言語と違い、指定する数字は1から始まる。0番目というのは不正な指定でエラーである。

julia> "abc"[0]
ERROR: BoundsError: attempt to access String
  at index [0]

julia> "abc"[1]
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> "abc"[2]
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)

julia> "abc"[3]
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

配列

文字を並べたら文字列になるのだ。数字を並べたらどうなるのだろうか?これは配列と呼ばれるものになる。数字を単にずらずらと並べると、桁数の多い数字になるだけなので、区切り文字にカンマを使う。

julia> [10, 20, 30]
3-element Array{Int64,1}:
 10
 20
 30

配列の何番目かを指定して数字を取得することもできる。Juliaでは、配列も文字列と同じく、何番目かの指定は1から始まる数字で行う。

julia> [10, 20, 30][1]
10

実際には、配列の中身には、数値だけではなくて文字や文字列も入れられる。

julia> ['a', 'b', 'c']
3-element Array{Char,1}:
 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
 'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
 'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

julia> ["abc", "def", "ghi"]
3-element Array{String,1}:
 "abc"
 "def"
 "ghi"

最初に、文字を並べたら文字列になり、数値を並べると配列になると言ったが、文字の配列が文字列になるわけではない。明確に別のものだ。

julia> "abc"
"abc"

julia> ['a', 'b', 'c']
3-element Array{Char,1}:
 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
 'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
 'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

ただ、よく似た動作をすることもある。

julia> "abc"[1]
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

julia> ['a', 'b', 'c'][1]
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

数値と文字列を混在させたような配列も作れる。

julia> [2, 2.9, "abc"]
3-element Array{Any,1}:
 2
 2.9
  "abc"

さてさてここまではどうだっただろうか?楽しかっただろうか?多分楽しくなかったんじゃないだろうか?一体、いつになったらプログラミングが始まるんだよ!そう思った人が多いんじゃないだろうか?

お待たせしてしまい申し訳ない。そんな辛い辛い日々もこれで終わりだ。いよいよ次からはプログラミングらしいことをしていこう。次から説明するfor文と関数、これだけ説明したらゲームプログラミングの世界に飛び込んでいく。なお、変数名など、日本語が使えるところには日本語を使っていく。他言語を使ったことのある人は違和感があるかもしれないが、私はわかりやすくていいと思う。便利な機能は何だって使っていこう。

for文

そろそろゲームっぽい例も出せるくらいの地点まできた。連続攻撃をしてくる敵がいるとしよう。連続攻撃で与えられるダメージを合計した値を知りたい。そのようなときにどのようなプログラムを書くのだろうか?

まず、連続攻撃のダメージが配列に入っているとしよう。[10, 12, 11, 18] だ。これの合計を知りたい。

まずは変数の定義だ。ダメージの合計値はダメージ合計という変数に、連続攻撃によるダメージの配列は連続ダメージという変数に代入する。

julia> ダメージ合計 = 0
0

julia> 連続ダメージ = [10, 12, 11, 18]
4-element Array{Int64,1}:
 10
 12
 11
 18

そして、連続ダメージの合計を行うために、繰り返し、ダメージ合計の変数に連続ダメージの中身を足していく処理が、本節の主役のfor文である。

julia> for ダメージ in 連続ダメージ
         ダメージ合計 = ダメージ合計 + ダメージ
       end

forは処理を繰り返し実行する。forの行からendの行までの間がfor文の本体だ。今回であればダメージ合計 = ダメージ合計 + ダメージの1行だけだ。この処理を繰り返し実行する。繰り返し処理のことをループ処理と呼ぶこともある。for文はforループと呼ばれることもある。

何回繰り返しを行うかを決めているのが、in の後の要素だ。for文は、inの後の要素を一つずつ取り出しながら、本体の処理を実行する。

今回はinの後に連続ダメージが設定されている。連続ダメージの先頭から順番に値を取り出し、ダメージという名前をつけた変数に代入している。そして、ダメージ変数の値をダメージ合計変数に加算していっている。

細かくみていこう。まず最初の要素は10である、そのため、ダメージには10が入る。その状態で、ダメージ合計 = ダメージ合計 + ダメージの計算処理が動く。ダメージ合計は最初は0に設定しているので、その後 ダメージ合計 = 0 + 10で、ダメージ合計は10である。

次の要素12がダメージに代入される。その状態で、ダメージ合計 = ダメージ合計 + ダメージの計算処理が動く。ダメージ合計は先ほど10になったので、 ダメージ合計 = 10 + 12で、ダメージ合計は22である。

次の要素11がダメージに代入される。その状態で、ダメージ合計 = ダメージ合計 + ダメージの計算処理が動く。ダメージ合計は先ほど22になったので、 ダメージ合計 = 22 + 11で、ダメージ合計は33である。

最後の要素18が``ダメージに代入される。その状態で、ダメージ合計 = ダメージ合計 + ダメージの計算処理が動く。ダメージ合計は先ほど33になったので、ダメージ合計 = 33 + 18で、ダメージ合計```は51である。

すべてが終わったら、連続ダメージの合計値がダメージ合計変数に入っているだろう。

julia> println(ダメージ合計)
51

for文はあらゆる言語に登場すると言ってもいいほどの超重要構文である。文章で読むだけだとわかりづらいかもしれないが、動かすとすぐに慣れるだろう。for文と、この後紹介するif文をマスターしたら、かなりのプログラムが書けるようになる。

練習問題
  • 問題1

    • for文を使って、1から10までの数字の合計を求めてみよう。合計は55になるはずだ。
  • 問題2

    • 範囲オブジェクトというものを使ってみよう。1から10までの数字は、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] と書く以外にも、1:10と書くやり方もある。これを範囲オブジェクトと呼ぶ。問題1のループを範囲オブジェクトで書き直してみよう。
  • 問題3

    • 範囲オブジェクトをもう少し深掘りしよう。1から10までの数字を1つ飛ばしで合計する処理を範囲オブジェクトを使って書いてみよう。つまり、[1, 3, 5, 7, 9]を合計した結果と同じになるようにしてみよう。1つ飛ばしの範囲オブジェクトは説明していないので、何かを検索してみる必要があるだろう。

関数

for文という本格的な構文も身についたので、「関数」というものを紹介しよう。

先ほどのfor文で、配列の要素の値をすべて合計するという処理を書いた。題材としては、連続攻撃を受けた際の合計ダメージを計算する処理だったが、他の題材にも使える例であることは明らかだろう。例えば、何体かのモンスターを倒したとして、その合計の経験値を計算するというような例だ。

このようなとき、連続ダメージの計算の処理、経験値の計算処理のそれぞれで、同じような処理を書くことになる。

ダメージ計算の処理を再掲する。Juliaの応答は省略している。

julia> ダメージ合計 = 0
julia> 連続ダメージ = [10, 12, 11, 18]
julia> for ダメージ in 連続ダメージ
         ダメージ合計 = ダメージ合計 + ダメージ
       end

経験値の計算処理は次のようになるだろう。

julia> 経験値合計 = 0
julia> 経験値リスト = [22, 25, 14, 30, 44]
julia> for 経験値 in 経験値リスト
         経験値合計 = 経験値合計 + 経験値
       end

実行している処理の大枠は同じで、変数などが別なだけだ。これは面倒だ。配列の合計をするという処理が同じであれば、そのような処理をただ1つだけ書き、連続ダメージの計算の処理、経験値の計算処理のそれぞれで、その処理を共有するような仕組みが欲しい。このような処理を共通化する仕組みを「関数」と呼ぶ。

関数は次のようにして作ることができる。

julia> function 合計(配列)
         合計値 = 0
         for 要素 in 配列
           合計値 = 合計値 + 要素
         end
         return 合計値
       end
合計 (generic function with 1 method)

使うときにはこう使う。

julia> 連続ダメージ = [10, 12, 11, 18]
julia> ダメージ合計 = 合計(連続ダメージ)
julia> 経験値リスト = [22, 25, 14, 30, 44]
julia> 経験値合計 = 合計(経験値リスト)

合計という共通処理を、ダメージ計算、経験値計算で活用しているのがわかるだろう。

関数の使い方は非常に重要なので、しっかりと解説していく。

まず、関数を作りたいと思ったら、functionと書く。functionというのは関数を意味する英語だ。

function 

次に、どのような名前で呼びたいかを決める。今回の場合合計だ。これは好き勝手に決めればよく、「配列合計」「配列の合計」「合計値の計算」等々でも良い。

function 合計

次に、その処理がどのような入力を受け取るかを決める。入力は、関数名の後の括弧で囲まれた部分だ。さらに、入力は1つではなく複数になってもいい。その場合は、間まで区切る。今回は、入力が一つだけなので括弧はなく、その唯一の入力に「配列」という名前をつけている。入力の名前も好き勝手に決めて良い。

function 合計(配列)

その後は関数で行う処理本体を書いていく。

function 合計(配列)
  合計値 = 0
  for 要素 in 配列
    合計値 = 合計値 + 要素
  end

最後に、関数の処理結果を宣言するために、returnと書き、続けて何を結果とするかを書く。今回のケースだと、合計値を結果とする。最後に関数の終了を明示するために、endと書く。

function 合計(配列)
  合計値 = 0
  for 要素 in 配列
    合計値 = 合計値 + 要素
  end
  return 合計値
end

これで完成だ!難しく思えたかもしれないが、言葉で書くと冗長なだけだ。実際にはそう難しくはない。自分で何か関数を作ってみよう。

関数の定義について、形式的に書くとこうなる。

function 関数名(仮引数1, 仮引数2, ...)
  (処理本体)
end

別に、この形を覚えて欲しいわけではない。ただ、重要な用語として、「仮引数」というものは覚えて欲しいから紹介した。関数の入力を引数(ひきすう)と呼ぶ。そして、関数の定義に書かれる引数のことを「仮引数」と呼ぶのだ。関数の定義の時に、仮につける名前の引数だから仮引数だ。実際に関数呼び出しの時に与えられる引数のことを、実引数と呼ぶ。まあ、単に引数ということも多い。そんな時は文脈から判断できるはずだ。

この例では処理の共通化のために関数を作るとしているが、実際には1回しか呼ばれない処理でも関数化されることはよくある。処理の意味を明確化して名前をつけるということが目的だ。

練習問題
  • 問題1

    • 2つの引数を取り、その合計値を返す関数を作ってみよう。
  • 問題2

    • 2つの引数を取り、その平均値を返す関数を作ってみよう。問題1で作った関数を呼び出すこと。

Visual Studio Codeのインストール

長い長い間、よく我慢してもらった。ついにゲームを作っていこう。

しかし、その前に追加のツールをインストールしたい。

これまではREPLに直接コードを記述してきたが、REPLを閉じると消えてしまうし、一度作った関数を手直ししたい時も面倒だ。打ち込んだコードをファイルに保存したい。

ファイルに保存するだけであれば何か適当なテキストエディタにコードを書いて、「.jl」という拡張子で保存したら良い。だから、Windowsなら「メモ帳」、macなら「テキストエディット」でも良いのだが、やはりここはプログラミングに向いたエディタを使うべきだ。プログラミングに向いたのエディタであれば、色々と気の利いたことをやってくれる。

ここでお勧めするのが、Microsoft社によるテキストエディタVisual Studio Code」だ。無料で、どのOSでも使うことができる、軽量で拡張性の高いテキストエディタだ。エディタだと言いながら主要言語なら実行やデバッガによるステップ実行まですることができる怪物エディタだ。思想・信条上の理由がある人以外はぜひVisual Studio CodeでJuliaコードを書くべきだ。

Visual Studio Code公式サイトからダウンロードできる。ダウンロードしてインストールしたら、起動してみよう。この後する必要があるのは1つだけ。拡張機能のインストールだ。

f:id:muuuminsan:20201121153014p:plain
Visual Studio CodeへのJulia拡張機能のインストール

  1. 一番左に、四角形が4つ並んだ(1つは外れた位置にある)アイコンがあるので、そこをクリックする。
  2. 検索バーが出てくるので、juliaと入力する。
  3. おそらく一番先頭に、Juliaというシンプルなものが出てくるので、それをインストールしよう。

これでOKだ。これで準備完了だ。ゲームを作っていこう。

これからつくるゲームについて

これから作るのは極シンプルなRPGゲームだ。プレイヤーは味方キャラクターを操って、モンスターを倒していく。ひとまずプレイヤーは一人、モンスターも一体という単純なゲームだ。最初は驚くほどつまらないゲームだが、徐々に機能を追加していく。その過程で徐々に高度なプログラミングも学んでいこうという目論見だ。

最初に作るゲーム

最初に作るのは、勇者が無抵抗なモンスターをボコボコに攻撃するのをただ鑑賞するだけのゲームだ。ドン引きだ。下手したらR指定だ。何とも悪趣味なゲームだが、今作れるのはこれだけなのでしょうがない。

Visual Studio Codeで、新しいファイルを作って、適当な名前("rpg.jl"など)で保存しよう。保存先のフォルダは適当でいい。それが終わったら、次のようなコードを書こう。

function main()
    モンスターHP = 30
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    for _ in 1:3
        println("----------")
        println("勇者の攻撃!")
        モンスターダメージ = プレイヤー攻撃力
        モンスターHP = モンスターHP - モンスターダメージ
        println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
        println("モンスターの残りHP:" * string(モンスターHP))
    end

    println("戦闘に勝利した!")
end

main()

このコードは、大きく2種類の部分に大別される。1つはmainという関数の定義であり、もう1つはmain関数の呼び出しだ。

呼び出しの部分は大したことはないので、main関数の中身に注目しよう。main関数の中身では、実際に行われる戦闘の詳細が記述されている。HPというのはヒット・ポイントの略で、どのくらい攻撃に耐えられるかの略称である。攻撃力とは文字通り、相手を攻撃した時にHPをいかに減少させられるかの値である。この例では、30のHPのモンスターに、10の攻撃力で攻撃しているので1回あたり10のHPを減らすことになる。これを3回繰り返したら相手のHPは0になり、ゲームは終了する。

最初にモンスターのHPと勇者の攻撃力を設定しているのは以下の部分だ。

モンスターHP = 30
プレイヤー攻撃力 = 10

そして、初期設定が終わったらバトル開幕の合図だ。

println("モンスターに遭遇した!")
println("戦闘開始!")

その後は3回攻撃している。forの後が_になっている。

for _ in 1:3
    println("----------")
    println("勇者の攻撃!")
    モンスターダメージ = プレイヤー攻撃力
    モンスターHP = モンスターHP - モンスターダメージ
    println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
    println("モンスターの残りHP:" * string(モンスターHP))
end

普通であれば、for i in 1:3のように、繰り返しの何回目かを格納する変数名をつける。しかし、今回のケースであれば3回繰り返すことが大事であり、何回目の繰り返しかは重要ではない。その表明としてアンダーバーを用いている。iから_に変えたところで、コンピュータの実行上の違いはない。しかし、このコードを読む人間に対しては、繰り返しの何回目かは関係ないという表明になっている。これは私がそのほうがいいと思ってやっているだけなので、普通にfor i in 1:3としてもらっても全く構わない。

最後に、勝利を宣言している。

println("戦闘に勝利した!")

このコードを実行しよう。

モンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:20
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:10
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:0
戦闘に勝利した!

勝ったには勝ったが、虚しい。これではあまりに不公平だ。モンスターは攻撃する機会すら与えられていない。あなたはこのような不公平さを良しとする人物ではないはずだ。次に進むとしよう。

自動テストとリファクタリング

次に進みたいところで、横槍が入った。このアプリケーションの仕様を聞きつけた役員の一人から、防御力も考慮すべしという意見が出てきたのだ。

どうやらその役員はペットの亀を溺愛しているらしく、頑丈な甲羅を持つ亀が、その他のひよわな獣と同等に扱われるのが我慢ならないということである。

問題の箇所はここである。

モンスターダメージ = プレイヤー攻撃力

受けるダメージが攻撃力に等しいということを暗黙の前提としているのだが、それはおかしいという意見である。確かに、硬い敵と柔らかい敵というのは存在する。そのため、防御力を考慮したダメージ計算とすべしという話である。

あいわかりました、と防御力を考慮した式にするのは容易い。しかし、安易に変更してはならない。もう一度言う。安易に変更してはならない。

我々は、人間だ。人間というものは不完全なものだ。だから、何かを変更する時には、必ず何かミスをすると考えるべきなのだ。あなたが東大卒であろうが、弁護士だろうが、全国模試1位の高校生だろうが、ノーベル賞受賞者だろうが関係ない。人間である以上、必ずミスをする。いつか必ず絶対にミスをする。それが今日この場ではないと誰が保証できるだろうか。少なくともあなた自身で保証できないのは確かだ。

だから我々には、自動テストが必要だ。あなたの行った変更が、「期待通り」であることを保証してくれる自動テストが必要だ。「何も壊していない」ことを保証してくれる自動テストが必要だ。

自動テストだ。自動テストだ。自動テストだ。自動テストだ。あなたが実家の倉庫を整理していると、古びたランプが出てきたとしよう。試しに擦ってみると、ランプの魔神が現れた。ランプの魔神はあなたの願いを3つ叶えてくれるという。2つはあなたの好きにしたら良い。1000兆円の現金だろうが、生き別れの弟との再会だろうが、それはあなたの望みのままだ。だが、最後の1つは自動テストの作成にして欲しい。

自動テストとは何だろうか?それは、自動化されたテストのことである。普通のテストは人間がプログラムを動かして行う。一方、自動テストは、プログラムを動かすプログラムを作る。「このプログラムを動かすとこうなる」という入力と出力の組み合わせを事前に定めておき、変更が入るたびに実行し、結果がわかるようにしておくのだ。

変更の詳細

それでは本題に入ろう。

まず、変更の要望は、防御力というパラメータを設定して欲しい、受けるダメージは防御力を高くすると減らして欲しいと言うものだ。もう少し詳細に確認すると、ダメージ = 10 * 攻撃力 / 防御力にして欲しいと言うことらしい。防御力が平均的なモンスターの場合は防御力10に設定される。これであれば、防御力を考慮しつつ、平均的な防御力のモンスターに関しては、既存のゲームバランスを見直す必要がない。

変更を素直に実装するとこうなる。

function main()
    モンスターHP = 30
    モンスター防御力 = 10 #追加行
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    for _ in 1:3
        println("----------")
        println("勇者の攻撃!")
        モンスターダメージ = round(Int, 10 * プレイヤー攻撃力 / モンスター防御力) #変更行
        モンスターHP = モンスターHP - モンスターダメージ
        println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
        println("モンスターの残りHP:" * string(モンスターHP))
    end

    println("戦闘に勝利した!")
end

roundというのは四捨五入だ。ダメージは整数にしたい。Roundの第1引数のIntというのは、四捨五入した後に、Int、すなわち整数に変換するようにということだ。Intについてはまだ解説していないので、そんなものかとスルーして欲しい。

大事なのは、いきなりこのようなことをすべきではないということだ。

次のような戦略で向かおう。

  1. ダメージ計算を関数化する。新旧の値を自動テストで比較しながら、関数化したものに置き換える。
  2. 関数化したダメージ計算を、あるべきダメージ計算に修正する。

1. ダメージ計算を関数化する

まずは、モンスターダメージ = プレイヤー攻撃力の部分の関数化だ。「変更前の動き」と「関数呼び出しに置き換えた結果」が同じであることを保証する自動テストを作成する。自動テストが通れば、関数呼び出しに置き換えて良い。

自動テストを作成するために、プログラムの先頭に、using Testと書いておく。別に先頭でなくて、自動テストのコードが書かれる直前でも良いのだが、こだわる部分でもないので先頭に書いておく。

次に、自動テストを設定する箇所を包むように、@testset begin から endで括っておく。これは、この中にテストコードが含まれていますよという宣言で、テストコードを含んでいない今の時点では、まだ何も動作を変えない。

using Test #追加行

function main()
    モンスターHP = 30
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")
    @testset begin #追加行
        for _ in 1:3
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = プレイヤー攻撃力
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
        end #追加行
    end

    println("戦闘に勝利した!")
end

main()

次に、関数呼び出しに変更する箇所にテストコードを入れる。==というのは、両辺が等しいという意味で、この場合、ダメージ計算という関数を作っても、とりあえず以前と同じ結果になっているということを表明している。

using Test

function main()
  (略)
        @testset begin
            (略)
            モンスターダメージ = プレイヤー攻撃力
            @test プレイヤー攻撃力 == ダメージ計算(プレイヤー攻撃力)            
            モンスターHP = モンスターHP - モンスターダメージ
            (略)
        end
  (略)
end

main()

こうしてプログラムを実行すると、次のようになると思う。

モンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
test set: Error During Test at 〜
  Test threw exception
  Expression: プレイヤー攻撃力 == ダメージ計算(プレイヤー攻撃力)
  UndefVarError: ダメージ計算 not defined

ダメージ計算なんて関数は無いよと言われているのだ。そのとおりだ。作ってあげよう。

using Test

function ダメージ計算(攻撃力)
    return 攻撃力
end

function main()
  (略)
end

main()

こうすると次のような結果になるはずだ。

ンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:20
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:10
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:0
Test Summary: | Pass  Total
test set      |    3      3
戦闘に勝利した!

下の方にテストの結果が表示されている。見ると、テストの失敗は発生していない。つまり、安心して関数呼び出しに置き換えられるということだ。置き換えたのが次の形だ。

using Test

function main()
  (略)
        @testset begin
            (略)
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力) #ここを変更した
            @test プレイヤー攻撃力 == ダメージ計算(プレイヤー攻撃力)            
            モンスターHP = モンスターHP - モンスターダメージ
            (略)
        end
  (略)
end

main()

2. 関数化したダメージ計算を修正する

次は、このダメージ計算関数の変更だ。ここでモンスターの防御力を考慮した式に変更する必要がある。

function ダメージ計算(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

しかしこれも、一目散に置き換えてはいけない。テストを作って安全に置き換えるのだ。

次にすべきことは何かというと、既存の関数の修正ではなく、新しい関数を作るということだ。名前は何でも良いので、ダメージ計算2としよう。

function ダメージ計算(攻撃力)
    return 攻撃力
end

function ダメージ計算2(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

呼び出し箇所はまだ何も変えない。

function main()
  (略)
        @testset begin
            (略)
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力)
            @test プレイヤー攻撃力 == ダメージ計算(プレイヤー攻撃力)
            (略)
        end
  (略)
end

main()

実行すると、テストは通る。関数を追加しただけで呼び出し元を変更したわけではないので当たり前だ。

次は、テストコードを変更する。ダメージ計算ダメージ計算2で置き換えられるかというテストだ。

function main()
  (略)
        @testset begin
            (略)
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力)
            @test ダメージ計算(プレイヤー攻撃力) == ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
            (略)
        end
  (略)
end

main()

これは、エラーになる。これは期待通りだ。なぜなら、モンスター防御力を定義していないからだ。

----------
勇者の攻撃!
test set: Error During Test at 〜rpg1.jl:24
  Test threw exception
  Expression: ダメージ計算(プレイヤー攻撃力) == ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
  UndefVarError: モンスター防御力 not defined
  Stacktrace:
   [1] macro expansion at /Users/kenji/Documents/SourceCode/rpg/rpg1.jl:24 [inlined]
   [2] macro expansion at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115 [inlined]
   [3] main() at /Users/kenji/Documents/SourceCode/rpg/rpg1.jl:20
  
モンスターは10のダメージを受けた!
モンスターの残りHP:0
Test Summary: | Error  Total
test set      |     3      3
ERROR: LoadError: Some tests did not pass: 0 passed, 0 failed, 3 errored, 0 broken.
in expression starting at 〜rpg1.jl:34

というわけで、モンスター防御力を定義する。

function main()
    モンスターHP = 30
    モンスター防御力 = 10 #追加行
    プレイヤー攻撃力 = 10

    (略)
    
    @testset begin
        for _ in 1:3
            (略)
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力)
            @test ダメージ計算(プレイヤー攻撃力) == ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
            (略)
        end
    end

    (略)
end

すると、テストが通った!

モンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:20
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:10
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:0
Test Summary: | Pass  Total
test set      |    3      3
戦闘に勝利した!

これで、安心して置き換えることができる。

function main()
    (略)
    
    @testset begin
        for _ in 1:3
            (略)
            モンスターダメージ = ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
            @test ダメージ計算(プレイヤー攻撃力) == ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
            (略)
        end
    end

    (略)
end

元のダメージ計算関数はもう用がないので、消してしまおう。ついでにテストコードも通らなくなるので消す。

using Test

function ダメージ計算2(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end


function main()
    モンスターHP = 30
    モンスター防御力 = 10
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")
    
    @testset begin
        for _ in 1:3
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = ダメージ計算2(プレイヤー攻撃力, モンスター防御力)
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
        end
    end

    println("戦闘に勝利した!")
end

main()

ダメージ計算2という名前はダサいので、ダメージ計算という名前にしよう。これを行うには、Visual Studio Codeの機能を使うのが良い。関数名にカーソルをあてて、"Rename Symbol"という選択肢を選び、ダメージ計算に変更しよう。これで、ダメージ計算2は、関数名もその呼び出し箇所もあわせてダメージ計算になる。

ついでにusing Test 、@testset beginendを消すと、テストコードを入れる前になって、無事このような形になる。

using Test

function ダメージ計算(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

function main()
    モンスターHP = 30
    モンスター防御力 = 10
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")
    
    for _ in 1:3
        println("----------")
        println("勇者の攻撃!")
        モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
        モンスターHP = モンスターHP - モンスターダメージ
        println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
        println("モンスターの残りHP:" * string(モンスターHP))
    end

    println("戦闘に勝利した!")
end

main()

まどろっこしく感じられたかもしれない。たしかに、このくらいの規模の変更であれば、テストなしで関数化したり変更をしても、きっと正しく修正できるだろう。

しかし、このような習慣を小さな問題に対しても身につけておくことで、いざ大きな問題に遭遇しても、テストの導入や既存処理の変更に臆せずに取り組むことができるのだ。

また、今回は部分的な変更に対しての前後比較のために自動テストを作成しただけだった。本来は切り出したダメージ計算関数にもテストコードを追加すべきだ。(今日のところはお腹いっぱいだろうから次回に回す)

アプリケーションの規模が大きくなると、ある修正が思わぬ影響を及ぼすことがある。その際に強力な助けとなってくれるのが、こういった永続して残るテストコードなのだが、これは一朝一夕には作ることのできない極めて重要な資産なのだ。

なぜなら、アプリケーションがある程度大きくなってしまうと、そのままの形ではテストコードを作ること自体が難しくなってしまうからだ。そして、テストコードを作ることができる形にするための修正自体がリスクを孕んでしまうという、にっちもさっちもいかない状況になってしまう。ランプの魔人に頼るしかなくなるのだ。

だから、テストコードはアプリケーションとともに少しずつ成長させていくべきだ。コードが小さいうちから、継続的にテストコードを作っていく癖をつけていけば、これはそう難しい話ではない。それができたら、ランプの魔神への3つ目の願いもあなたに決めさせてあげよう。

その1の終わりに

ひとまず切りがいいのでここまでにしよう。次回は、よりゲームらしいアプリケーションにしていきながら、if文、while文、構造体といった概念を学んでいく予定だ。