環境構築のためのDocker
はじめに
これはMuroran Institute of Technology Advent Calendar 2018 - Adventarの記事です.
- ホストマシンのOSはWindowsやmacOSを使いたい.
- Linuxも使いたい.(GUIは必要ない.)
- ホストマシンの環境をできるだけ変更したくない.
- 同じ開発環境を再現できるようにしたい.
- できれば賢い誰かが構築した環境を利用したい.
そんな時,ホストマシンにDockerをインストールしておくと,
- CUIだけの軽量なLinux仮想環境を構築できる.
- 環境構築の手順はテキストファイルに記述できる.
- そのファイルから開発環境を自動的に構築できる.
- Docker Hubにアップロードされた環境を利用できる.
なんて,素晴らしい.
インストール
それぞれのOSにおけるDockerのインストール方法を以下に示します.
Windows 10 64bit : Pro, Enterprise or Education
Install Docker Desktop on Windows | Docker Documentation
それ以外のWindows
macOS
Install Docker Desktop on Mac | Docker Documentation
Ubuntu
sudo apt install docker sudo apt install docker-compose
Docker
Dockerはイメージをもとにコンテナを生成し,コンテナ内の環境でアプリケーションを実行します. イメージはアプリケーションとその実行環境の情報をまとめたもので,Docker Hubからpullしてきたり,Dockerfileから作成したりすることで用意します. コンテナはイメージを実体化したもので,実行中のアプリケーションの状態を保持します. Docker Hubは利用可能なイメージが集まったDockerのサービスです. Dockerfileはイメージを作成する手順を記述したテキストファイルです.
Docker Hubからpullしてきたり,Dockerfileから作成したりしたイメージは,ダウンロードしてきたり,ソースコードからビルドしたりしてSSDに保存したプログラムに例えることができると思います. また,イメージをもとに実体化され,実行されるコンテナは,SSDからメモリにロードされ実行されるプログラムに例えることができると思います.
dockerのコマンド
docker --help
: dockerのヘルプを表示する.docker build -t イメージ名 ディレクトリ
: 指定されたディレクトリ
のDockerfileから指定されたイメージ名
のイメージを作成する.docker run イメージ [コマンド]
: 指定されたイメージ
を基にコンテナを起動し,コンテナ内で指定されたコマンド
を実行する.docker stop コンテナ
: 指定された実行中のコンテナ
を停止する.docker ps
: 実行中のコンテナ一覧を表示する.docker images
: ホストマシンにあるイメージ一覧を表示する.docker rm コンテナID
: 指定されたコンテナID
を持つコンテナを削除する.docker rmi イメージID
: 指定されたイメージID
を持つイメージを削除する.
Dockerfile
Dockerfileはイメージを作成する手順を記述したテキストファイルで,以下のように記述します.
# 基にするイメージを指定する.Docker Hubから探してくることが多い. FROM ベースイメージ # 作業ディレクトリを指定する. WORKDIR /workdir/path # ホストマシンのファイルやディレクトリをコンテナ内に複製する. COPY host/path/ container/path # Shellコマンドを実行する.アプリケーション実行のための準備を行う. RUN shell command # コンテナ実行時に実行されるアプリケーション実行コマンドを指定する. CMD ["アプリケーション実行コマンド", "コマンドライン引数1", "コマンドライン引数2", ...]
使用例
Hello World
次のコマンドを実行すると,
docker run hello-world
dockerはホストマシンに保存されているhello-world
イメージからコンテナを生成し,Hello Worldを出力するアプリケーションをコンテナ内で実行します.
ホストマシンにhello-world
イメージがないときは,dockerはDocker Hubからhello-world
イメージをpullしてきてホストマシンに保存します.
Bash
次のコマンドを実行すると,
docker run -i -t ubuntu bash
dockerはホストマシンに保存されているubuntu
イメージから(無いときはDocker Hubからpullして保存する.)コンテナを生成し,コンテナ内でBashを実行します.
-i
, -t
オプションはホストマシンのの標準入出力とコンテナの標準入出力を繋いぐためのオプションです.
C++
次の内容のファイルを用意します.
- ./Dockerfile
FROM ubuntu WORKDIR /home/app COPY ./src/main.cpp /home/app RUN apt update -y && apt upgrade -y && apt install -y g++ RUN g++ -o main main.cpp CMD ["./main"]
- ./src/main.cpp
#include<iostream> int main(int argc, char *argv[]) { std::cout << "hello docker-cpp" << std::endl; }
次のコマンドを実行すると,
docker build -t docker-cpp ./
カレントディレクトリのDockerfile
からdocker-cpp
という名前のイメージが作成されホストマシンに保存されます.
docker-cpp
は具体的にはDockerfile
に従い以下のように作成されます.
- ubuntuのイメージをダウンロードする.
- 作業ディレクトリを
/home/app
に設定する. apt update -y && apt upgrade -y && apt install -y g++
を実行し,アプリケーションのビルドの準備をする.g++ -o main main.cpp
を実行し,アプリケーションをビルドする.- コンテナ起動時に
./main
というコマンドでアプリケーションを実行するように設定する.
イメージ作成後に次のコマンドを実行すると,
docker run docker-cpp
ホストマシンに保存されたdocker-cpp
という名前のイメージからコンテナが生成された後に./main
が実行されて次の実行結果が得られます.
hello docker-cpp
Docker Compose
Docker Composeはdocker-compose.ymlに記述された設定に従って,1つ以上のコンテナを連携させて起動するものです. docker-compose.ymlはコンテナ起動時の設定を記述するテキストファイルです.
docker-composeのコマンド
docker-compose --help
: docker-composeのヘルプを表示する.docker-compose build
:docker-compose.yml
に従ってイメージを作成し,保存する.docker-compose up [-d]
:docker-compose.yml
に従ってコンテナを起動する.-d
オプションを付けるとコンテナをバックグラウンドで実行する.docker-compose run コンテナ名 コマンド
:コンテナ名
を持つコンテナを起動し,コンテナ内でアプリケーションの代わりにコマンド
を実行する.docker-compose exec コンテナ名 コマンド
:コンテナ名
を持つ起動中のコンテナ内でコマンド
を実行する.docker-compose stop
: 起動したコンテナを停止する.
docker-compose.yml
# docker-compose.ymlのバージョンを指定する.'3'を指定する. version: '3' # コンテナごとの設定を記述する. services: コンテナ名: # コンテナの名前をコンテナ名に設定する. container_name: 'コンテナ名' # imageまたはbuildでコンテナのイメージを指定する. # イメージを直接指定するときはimageで指定する. image: 'イメージ' # イメージをDockerfileで指定するときはbuildで指定する. build: # Dockerfileがあるディレクトリを指定する. context: 'Dockerfileのディレクトリ' # Dockerfileのファイル名を指定する. dockerfile: 'Dockerfile名' # ホストマシンのディレクトリをコンテナにマウントする. volumes: - 'ホストマシンのディレクトリ:コンテナのディレクトリ' # ホストマシンのポートを開放し,コンテナのポートに接続する. ports: - 'ホストマシンの開放ポート:コンテナの開放ポート' # コンテナ起動時に実行されるコマンドを指定する. command: ["コマンド", "コマンドライン引数1", ... ]
使用例
Python
次のファイルを用意します.
- ./docker-compose.yml
version: '3' services: docker-py: container_name: 'docker-py' image: 'python:3' working_dir: '/home/app' volumes: - './app:/home/app' command: ["python", "main.py"]
- ./app/main.py
print("hello docker-py")
次のコマンドを実行すると,
docker-compose up
カレントディレクトリのdocker-compose.yml
の設定に従って,docker-py
という名前のコンテナが起動し,次の実行結果が得られます.
hello docker-py
docker-py
は具体的にはdocker-compose.yml
に従い以下のように起動されます.
- python:3のイメージをダウンロードする.
- 作業ディレクトリを
/home/app
にする. - ホストマシンの
./app
をコンテナの/home/app
にマウントする. - コンテナを起動して
python main.py
を実行する.
また,次のコマンドを実行して,
docker-compose run docker-py bash
コンテナ内でpython
を実行するとPythonが対話モードで起動します.
Gnuplot
次のファイルを用意します.
- ./Dockerfile
FROM ubuntu WORKDIR /home/files COPY ./files /home/files RUN apt update -y && apt upgrade -y && apt install -y gnuplot CMD ["gnuplot", "plot-cos.plt"]
- ./docker-compose.yml
version: '3' services: gnuplot: build: context: './' dockerfile: 'Dockerfile' container_name: 'gnuplot' volumes: - './files:/home/files/'
- ./files/plot-cos.plt
set terminal pdfcairo set output 'plot-cos.pdf' set xrange [-2*pi:2*pi] set yrange [-1.5:1.5] set samples 500 plot cos(x)
- ./files/plot-sinc.plt
set terminal pdfcairo set output 'plot-sinc.pdf' set xrange [-10*pi:10*pi] set yrange [-1:1.2] set samples 500 plot sin(x)/x
次のコマンドを実行すると./files/plot-cos.pdf
が生成されます.
docker-compose build docker-compose up
次のコマンドを実行して,
docker-compose build docker-compose run gnuplot bash
コンテナ内でgnuplot ./plot-sinc.plt
を実行すると./files/plot-sinc.pdf
が生成されます.
PHP
次のファイルを用意します.
- ./docker-compose.yml
version: '3' services: docker-php: container_name: 'docker-php' image: 'php:7.2-apache' volumes: - './html:/var/www/html' ports: - '8080:80'
- ./html/index.php
<?php echo "hello docker-php" ?>
次のコマンドを実行して,
docker-compose up -d
ブラウザでhttp://localhost:8080
にアクセスするとhello docker-php
と表示されます.
まとめ
- DockerとDocker Composeを利用すると,ホストマシンのOSに依らずに,再現性のある軽量な仮想Linux環境を自動的に構築できます.
- Dockerはイメージからコンテナを実体化し,アプリケーションを実行します.
- Docker Composeはdocker-compose.ymlの設定に従ってコンテナを起動します.
- Dockerfileにはイメージの作成手順を書き,docker-compose.ymlにはコンテナ起動時の設定を書きます.
世界と孤独の説法(エピローグ)
説法系推理アドベンチャシリーズ外伝
ゲーム情報
本ゲームは登場人物の説法を聞くことと,論理クイズを組み合わせた説法系推理アドベンチャシリーズの外伝の推理リンクノベルです.
- タイトル : 世界と孤独の説法(エピローグ)
- 読み : せかいとこどくのえぴろーぐ
- 作者 : Jumpaku
- ジャンル : 説法系推理リンクノベル
- プレイ時間 : 30分程度
- プラットフォーム : PDFビューア
- リリース : 2018年4月22日
- 言語 : 日本語
- 開発環境 : LaTeX
経緯
第9回LOCAL学生部総大会 ヤバい同人誌執筆しようぜというイベントに参加しました.
これはOSC Hokkaidoや技術書展への出展を目指して,LaTeXで同人誌を書くというイベントでした. LaTeXはテキストファイルをPDFファイルに変換する組版システムです. LaTeXはチューリング完全であるため,どんなアルゴリズムでも実装できます. そこで,ゲームを作りたいと思いました. ただ,正直,LaTeXでプログラムを書きたくはないし,インタラクティブなゲームを作るビジョンも浮かばないため, 実際にはリンク機能だけを利用したノベルゲームを作成しました. 一応,技術要素として
- LaTeX によってノベルゲームを作成すること,
- プログラミングによって効率的に論理クイズを解くこと
がコンセプトとなっています. 物語は説法系推理アドベンチャシリーズ
- 愛と血の修羅場(サスペンス) jumpaku.hatenablog.com
- 恋と友情の常識(ファイト) jumpaku.hatenablog.com
- 罪と幸せの四苦八苦(ノアズアーク) jumpaku.hatenablog.com
の外伝となっています. 個人的には,外伝のように後からストーリーが追加される形式を好まないのですが, イベント期間が睡眠,食事を含めて31時間と短いため,全く新しい物語を考えるのではなく,外伝という形式にしました.
このイベントの成果物はLOCAL学生部の「情報ボーイズの寄稿ノート」にまとめられ,技術書展4で紙媒体で頒布されます.
当然,紙媒体ではリンク機能を使えないのですが,これを考慮していなかったため,技術書展4で購入しても本ゲームをプレイできないという事態が発生しました. 本ゲームをプレイするにはPDFファイルを手に入れる必要があります. 「情報ボーイズの寄稿ノート」のソースファイルはGitHubのリポジトリにあります.
ここからクローンしてきて,コメントアウトを解除してPDFにコンパイルすればプレイできますが, 手間がかかります. そこで,私の章だけをコンパイルしたPDFを用意しました.
世界と孤独の説法(エピローグ)_v1.pdf - Google ドライブ
「情報ボーイズの寄稿ノート」には私の他にもたくさんの著者がいて, それぞれが自分の得意分野の技術記事を書いています. 是非,そちらも読んでください.
リンク
- 第9回LOCAL学生部総大会 ヤバい同人誌執筆しようぜ https://connpass.com/event/74025/
- 愛と血の修羅場(サスペンス) https://jumpaku.hatenablog.com/entry/2016/04/14/002437
- 恋と友情の常識(ファイト) https://jumpaku.hatenablog.com/entry/2016/07/24/032632
- 罪と幸せの四苦八苦(ノアズアーク) https://jumpaku.hatenablog.com/entry/2017/07/24/044918
- 技術書展4 情報ボーイズの寄稿ノートhttps://techbookfest.org/event/tbf04/circle/14720002
- GitHub hyoiutu/techbookfest_localstudents_2017 https://github.com/hyoiutu/techbookfest_localstudents_2017
- 本ゲームをコンパイルしたPDF https://drive.google.com/open?id=1sDJou37nh-sm2PvUP6e-kcsKhzhu1WHE
室工ドライブ Drive around Muroran-IT
"安全第一"
概要
本ゲームは室蘭工業大学(室工大)の周囲を安全にドライブするゲームです. 室工大の周囲には路上駐車,道路を横断する歩行者などの様々な障害があります. ドライバはこれらの障害を避けながら安全に室工大を一周します.
情報
- タイトル : 室工ドライブ
- 読み : むろこうどらいぶ
- 作者 : Jumpaku
- ジャンル : カーアクション
- プレイ時間 : 1分程度
- プラットフォーム : macOS
- リリース : 2018年2月26日
- 言語 : 日本語
- 開発環境 : Unity
- バージョン : 1.1
遊び方
MurokouDrive_v1.app
を実行します.- 設定ウィンドウが開くので解像度,画質を設定します.
Play
ボタンを押してゲームを起動します.START
ボタンを押してゲームを開始します.- 反時計回りに道路を進みます.
- 運転に使用するキーは以下の通りです.
- アクセルを踏む:上矢印
- バックする:下矢印
- ハンドルを右に切る:右矢印
- ハンドルを左に切る:左矢印
- 道路から外れるとGame Overです.
- 室工大を反時計回りに一周するとClearです.
ダウンロード
実行ファイルは以下のリンクからダウンロードできます.
室工ドライブ_v1-1.zip - Google ドライブ
ソースコード
ソースコードは以下のリンクから参照できます. https://github.com/Jumpaku/MurokouDrive
開発
前からUnityに興味があり,今回初めてUnityで3Dゲームを作ってみました. 3Dモデル,当たり判定,当たり処理は既存のものを使いました. 工夫した点として,逆走を防ぐためにプッシュダウンオートマトンで状態を管理している点,地形や建造物をできるだけ実物に忠実に作成した点が挙げられます. 現在はmacOS向けのものしかリリースしてません.
生存報告ヴァルキュリア
シンデレラガールズ Advent Calendar 2017
Dec. 19
導入
本アドベントカレンダーにおいて,デレステをきっかけに人生が完全に変わった事例が報告されている. muscle-keisuke.hatenablog.com 本報告には関係が無いが,もしまだ上の記事を読んでいないなら,是非読んでいただきたい. これは,その記事の筆者がサークルの共用タブレットにインストールされていたデレステをきっかけにして,
という話を書いた記事であり,次元の壁を超えたドラマとも言える.
一方,その裏で別のドラマが進行していたことを知る者は少ない. そこで,本報告ではそのもう一つのドラマの報告を目的として, もう一つのデレステをきっかけに人生が完全に変わった話について解説する. さらに,その上で,私が学んだことについて考察し, 最後に,現状の課題と今後の展望について述べる.
もう一つのデレステをきっかけに人生が完全に変わった話
上で紹介した記事と同様に, 私の人生もデレステをきっかけに完全に変わってしまった. 以下では,
という話について,時系列に沿って説明する.
デレステとの出会い
今から約1年前,私が,自分の所属するサークルの共用タブレットにインストールされていた 「アイドルマスター シンデレラガールズ スターライトステージ(デレステ)」 を発見するところから,物語は始まる. 私はゲーム制作は好きだが,プレイにはあまり興味が無かった. また,アイドルも知らなかった. しかし,誰かがインストールしたデレステがふと気になった私はデレステを起動し,チュートリアルを始めた. 慣れないUI,うろ覚えのルール,それでもなんとか選曲まで辿り着いた. 私が知る曲など当然存在しないが,格好良い名前に惹かれて「生存本能ヴァルキュリア」を選ぶ. 私はクリアしたが,それどころではなかった. 初めて遊ぶリズムゲーム,その後ろで動く3Dモデル達,そして素晴らしい楽曲. 私はその全てに感動し,「生存本能ヴァルキュリア」を遊び続けた.
その時のユニットを以下に示す.
「生存本能ヴァルキュリア」MASTERクリアの壁
しばらく「生存本能ヴァルキュリア」で遊んでいた私は,難易度の存在に気が付いた.
DEBUT, REGULARおよびPROの3つがあり,それまで遊んでいたのはDEBUTだった.
すぐにREGULARに挑戦してこれをクリアし,PROに挑戦した.
PROは簡単にはクリアできなかった.
私は,最高難易度なだけあってPROは難しいな,と感じつつ,何度もトライした.
やっとの思いでクリアし,達成感に浸っていた私は,
最高難易度だと思っていたPROの横にMASTERが増えていることに気がついた.
「あんなに難しかったPROの更に上の難易度があるのか」
ここから数か月,孤独な苦難の時代が続く. 私は毎日「生存本能ヴァルキュリア」MASTERを練習した. しかし,何度MASTERに挑戦しても,サビ前でライフが尽きる. スタミナ,スタミナドリンクおよびリハーサルチケット等の練習のために使えるものは全て使ったが上達しない. また,ライブを最後まで見ることもできない. 追い込まれた私は
- スタージュエルでスタミナを回復できること
- スタージュエルでライブをコンティニューできること
- 今までもらったスタージュエルがたくさん余っていること
を思い出した. 私は,時間のある時はスタミナを回復して練習し, ライフが尽きた時はライブをコンティニューすることにした. その結果,ものすごい勢いでスタージュエルが消費されていった.
アイドル達の協力
スタージュエルはすぐに無くなり,私は行き詰まった. そんな時にふとアイドル編成のおすすめ編成機能が気になり,これでユニットを編成してみた. すなわち,事務所に所属していたアイドル達がもっと自分達を頼っても良いと協力を申し出てくれたのだ. 途方に暮れていた私は藁にもすがる思いで彼女達を頼った. 結果として,彼女らはライブを成功させた. 今まで不可能とも思えた「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしてしまった.
その時のユニットを以下に示す.
ユニットおよびプロデューサの強化
友人でもある同僚に「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしたことを報告すると,
「ゲーム下手な君が遂にMASTERをクリアしたか,でも,フルコンはしていないんだね.」
と言われた.
そこで,私は「生存本能ヴァルキュリア」のマスター,すなわち「生存本能ヴァルキュリア」MASTERのフルコンを決意した.
前節の経験より,この目標のためには,自分一人ではなくアイドル達の協力が不可欠と考えた. そして,事務所内のユニットを強化するために
- アイドルの強化
- 他の楽曲のライブ
- オーディション
- イベントへの参加
- ストーリーコミュ
などを行い,活動の幅を広げていった.
アイドルの強化では,
- ライフ回復の特技のスキルレッスン,
- ライフ上昇のためのレッスン,特訓およびポテンシャル解放
を行なった. そして,他の楽曲のライブを行うことで, プロデューサおよびアイドルの視野が広がった. また,オーディションやイベントへ参加がきっかけとなって, 事務所に新たなアイドルが増えた. 特に,ライフ回復を特技とするアイドルはすぐにユニットへ加えた. さらに,ストーリーコミュによって,事務所のアイドルへの理解を深めた.
一方で,プロデューサ自身はライブ成功に貢献するためにリズムアイコンのスピードを調整した. 具体的には,リズムアイコンのスピードを9.8とした. まず,私が画面上のリズムアイコンを全て把握するためには9.5程度以上でなければならなかった. スピードがこれより低いとリズムアイコンが多すぎて把握できなくなる. 次に,9.9程度というスピードは,私がリズムアイコンの出現を確認してから親指で対応する位置をタップできる限界の速度である. したがって,この時,指を最速で動かせばタイミングを計らずとも,タイミング良くタップできることになる. そして,私の目がリズムアイコンの動きを捉えられる限界が9.8程度である. リズムアイコンのスピードを9.8とすることで画面上のリズムアイコン全てに反応し,それらの動きを捉え,タイミング良くタップできる. 実際,この設定により,コンボ数が格段に上がった. 様々な楽曲でライブを安定してクリアできるようになり,私のライブ力も向上した.
この頃活躍していたユニットの例を以下に示す.
- [ハイテンションスマッシュ]喜多見柚+
- [ゴージャスチアー]岸部彩香+
- [花園の春風]西園寺琴歌+
- [シュガーリーボディ]榊原里美+
- [寡黙の女王]高峯のあ+
このようにして,元々アイドルにもソシャゲにも興味の無かった私はデレステにどんどんハマっていった.
「生存本能ヴァルキュリア」のマスター
他の簡単な楽曲のMASTERをいくつかフルコンできるようになって, アイドル達および私のライブ力の向上を実感してきたところで, 私は「生存本能ヴァルキュリア」のマスターのためのチャレンジに本腰を入れた. そして,何度もトライして遂にフルコンを達成した. 以下の画像はその時のものである. この画像から分かるようにフルコンするまでに実に323回のクリアがあった. 最初の2,3ヶ月の度重なるライブ失敗を考慮すれば, フルコンするまでの失敗を含めた総ライブ数は400-500回程度に上ると予想される.
また,フルコン時のユニットを以下に示す.
- [ハイテンションスマッシュ]喜多見柚+
- [ゴージャスチアー]岸部彩香+
- [ようせいのこ]遊佐こずえ+
- [Sweet Witches' Night]森久保乃々+
- [スクールデビル]小関麗奈+
この編成において,森久保乃々および小関麗奈がコンボを継続する一方で, 喜多見柚,岸部彩香および遊佐こずえがライフを回復するため, ライフ1でのコンボ切れによるライブ失敗を避けることが可能となっている. したがって,フルコンが途切れても,最後までライブを楽しむことができる.
すなわち,ユニット内のアイドル達のチームワークによって, ファンを最後まで楽しませるという最低限の仕事を放棄することなく, 「生存本能ヴァルキュリア」のマスターを目指すことが可能となる.
実際,フルコンするまでトライを続けることができたのは,例え途中でコンボが切れても, リズムゲーム,3Dモデルによる素晴らしいライブおよび好きな楽曲を最後まで楽しめたからである.
考察
以上の経験より, 私はプロデューサとアイドルの信頼関係の重要性について学んだ. プロデューサが超えられない壁に直面したとき,一人でできる努力には限界がある. それで嫌になって投げ出しては本末転倒である. そんな時は,プロデューサはアイドルに頼っても良いのである. 一方で,アイドルが成長するためには,プロデューサが視野を広く保ち, アイドル達に様々な経験をさせることも大切である.
課題と展望
MASTERより上の難易度ができた.MASTER+である. 「生存本能ヴァルキュリア」MASTER+も追加された. 以下のユニットによってなんとかクリアはできたが,まだフルコンはできていない.
- [ハイテンションスマッシュ]喜多見柚+
- [ゴージャスチアー]岸部彩香+
- [花園の春風]西園寺琴歌+
- [シュガーリーボディ]榊原里美+
- [ようせいのこ]遊佐こずえ+
現在はクリア数66回にしてコンボCすら達成できていない. リズムアイコンのスピードを9.9としても画面上のリズムアイコンが多すぎて把握できず, 10.0とすると速すぎてリズムアイコンの動きを捉えられず,指も追いつかないという状況である. クリアするのがやっとの状態なので,コンボ継続を特技とするアイドルをユニットに加える余裕もない.
今後は「生存本能ヴァルキュリア」MASTER+のフルコンに向けて, アイドル達と相談しながら作戦を練りつつ,動体視力と指の移動力を鍛えていきたい.
謝辞
本報告は「生存本能ヴァルキュリア」をマスターするまでの過程を報告するものである. 以下に,その過程で協力していただいたアイドル達への感謝を表する.
はじめのうち私とライブの練習をしていただいたイヴ・サンタクロース氏,星輝子氏,塩見周子氏,横山千佳氏および双葉杏氏に感謝します.
行き詰まっていた時に頼らせていただいた前川みく氏,栗原ネネ氏,大沼くるみ氏,双葉杏氏および城ヶ崎莉嘉氏に感謝します.
様々なイベントまたは楽曲のライブで活躍していただいた喜多見柚氏,岸部彩香氏,西園寺琴歌氏,榊原里美氏および高峯のあ氏に感謝します.
タイプ別のユニットで活躍していただいたアナスタシア氏,篠原礼氏,渋谷凛氏,川島瑞樹氏, 村上巴氏,佐藤心氏,片桐早苗氏,日野茜氏, 関裕美氏,櫻井桃華氏および大原みちる氏に感謝します.
付録 JumpakuUnit
本報告には直接関係はないが, JumpakuUnitを以下に示す.
Kotlinの良いところ
Muroran Institute of Technology Advent Calendar 2017
Dec. 18
Kotlin のここが良い
Kotlinという名前
「Kotlinは可愛い.」
初めてその名を見た時,私はそれがプログラミング言語であるとは気付かなかった. Kotlinを書き始めて数週間,私は,Kotlinは実はキメラなのではないかと思い始めた. 現在,私はKotlinの魔力に囚われている.
実行または開発環境
KotlinはJetBrainsで開発されたJVM向けの言語です. IntelliJ IDEAをインストールするとKotlinをすぐに使うことができます. したがって,環境構築は非常に楽です.
また,Kotlinで書いたコードはコンパイルされた後,Javaと同様にJVM上で実行されます. さらに,JavaのライブラリをKotlinのプログラムから利用することもできます. Gradleのプラグインも存在しており,これを使用したMavenのライブラリの利用も可能です.
ちなみに,私は試していませんが,Androidアプリも開発できるようです.
文法に関して
Javaの無駄を排除
null
は無駄- セミコロンは無駄
new
は無駄- 変数宣言時の型名は無駄
- main関数にとってクラスは無駄
Javaによるプログラミングにおいて,null
は諸悪の根源であり,
これを駆逐するためにはnull
チェックを欠かしてはいけません.
しかし,これほど面倒臭いことがありましょうか?
そして,そもそもnull
は必要なのでしょうか?
Haskellを見れば,変数が無くても,プログラムを作成することが可能であると分かります.
読み取り専用の変数をnull
で初期化することに意味は無く,
したがって,読み取り専用の変数はnull
以外の値で初期化されます.
また,全ての変数が読み取り専用でもプログラムは正しく動くことが可能です.
すなわち,null
を用いずともプログラムは正しく動くのです.
null
は不要です.
それどころか,バグの根またはnull
チェックの手間以外の何物でも無いのです.
Kotlinにはnonnull型とnullable型があります.
nonnull型の変数にnull
を代入し,またはnull
で初期化しようとするとコンパイルエラーとなります.
このnull
安全な文法により,プログラマは憎っくきnull
に触ることなくコードを書くことができます.
このnull
安全性は私がScalaではなくKotlinを選ぶ決め手となりました.
1行に複数の文が存在する事は稀で,そもそも,そういう書き方は好まれません. 実際,Pythonは改行で文を区切ることとなっています. すなわち,文末のセミコロンは不要なのです.
Java, C++等では,当たり前のように何千何万何億ものセミコロンを書いてきましたが, Kotlinではその無駄な作業をせずに済むのです.
Javaにおいて,オブジェクト生成時にはコンストラクタの呼び出しおよびnew
を記述しなければいけません.
コンストラクタの呼び出しを見れば,生成するオブジェクトのクラスおよび呼び出すコンストラクタに渡す引数といった,
オブジェクト生成に必要な全ての情報が分かります.
では,new
を見るとどんな情報が得られるでしょうか,いいえ,どんな情報も得られません.
Javaにおいて,new
は不要なはずなのです.
Kotlinでは,この無駄も排除されるのです.
私はこの点においてKotlinはJavaおよびScalaより良いと思っています.
変数宣言時の型名を省略しても良い言語が多く存在します.
Kotlinもその一つです.
C++にもauto
ができましたね.
Javaにおいて,なぜ1つの変数宣言で同じ型名を2度も書かなければならないのかと思った事はありませんか?
明らかに無駄な型名を書かなくてはいけないというJavaの苦しみから,型推論があなたを解放します.
Hello World!を書くためだけになぜクラスを作らなければいけないのか,なぜ標準出力 (System.out.println("Hello World!");
)がめんどくさいのか?
KotlinはC++のように関数をクラス外に書けます.
ついでに言うと,標準出力もprintln("Hello World!")
とシンプルです.
ここまでの内容をまとめると,
- Javaには記述の無駄が多い.
- Kotlinはそれらの無駄を削ぎ落としたものとなっている.
と言えるのではないでしょうか.
不変性のサポート
- val
- デフォルトの修飾子
不変クラスはクラスの理想の姿と言えます. スレッドセーフであり,意図しない変更を受けず,矛盾した状態となることもないからです. 例えば,不変クラスのインスタンスへの参照を保持する読み取り専用の変数は, いつでもどこでも初期化時と同じ値を保つため,取り扱いが非常に楽です. プログラムを書くときは,不変クラスを作成したり,変数を読み取り専用としたりことによって, できるだけ不変とすることが望ましいと思います.
しかし,Javaにおいて,これは生易しい話ではありません.
全ての変数にfinal
を付けていく作業は心の折れる作業です.
また,不変クラスを作成する際は,そのクラスのインスタンスが保持するオブジェクトが変更されないことを保証しなければいけません.
これを実現するためには
- 継承不可とする.
- フィールドを読み取り専用とする.
- 可変なオブジェクトを変更しない.
- 可変なオブジェクトの参照を共有しない.
といったことに注意しなければいけません.
ここで,不変クラスは推奨されていますが,Javaでの実装が困難であるという問題があります. これに対して,Kotlinには不変性を意識したプログラミングを援護する文法があり, 不変クラスの実装を楽に行えます.
例えば,変数を宣言する時にval a = 1
と書くと,変数a
は読み取り専用となります.
これはfinal int a = 1;
をとするより,シンプルです.
また,クラスおよびメソッドはデフォルトでfinal
が付いた状態となっております.
継承可能とするためまたはオーバーライド可能とするためにはopen
を付けます.
そして,C++とは逆に,アクセス指定はデフォルトでpublic
となります.
はじめのうち,私は,カプセル化に反する感じがして慣れませんでした.
しかし,よく考えると,
フィールドはプロパティによってラップされているので,
オブジェクトが不変である場合には困る事はありませんでした.
以上のように,Kotlinは言語として不変性をサポートしてくれます.
その他の便利機能
ここではKotlinの文法に関するその他の良いところを挙げていきます. まず,演算子のオーバーロードです. やはり,ベクトル,点その他の数学的なクラスは数値型の演算子を同じように使いたくなります. そんなときに,演算子のオーバーロードができる言語はありがたいです.
KotlinのクラスはcomponentN()
を実装することで,
オブジェクトの値を分解して,複数の変数を同時に初期化することができます.
class Vector(val x: Double, val y: Double, val z: Double) { operator fun component1(): Double = x operator fun component2(): Double = y operator fun component3(): Double = z } fun main(vararg args: String) { val (a, b, c) = Vector(1.0, 2.0, 3.0) println("$a, $b, $c") //1.0, 2.0, 3.0 }
さらに,Kotlinにはdata class
というものがあり,
単に
data class Vector(val x: Double, val y: Double, val z: Double)
と書くだけで,Javaのよくある次のようなクラスに
public final class Vector { private final double x; private final double y; private final double z; public Vector(double x, double y, double z){ this.x = x; this.y = y; this.z = z; } public double getX() { return x; } public double getY() { return y; } public double getZ() { return z; } @Override public String toString() { return "Vector(x="x + ", y=" + y + ", z=" + z + ")"; } }
を実装したクラスに更に
operator fun component1(): Double = x
, operator fun component2(): Double = y
およびoperator fun component3(): Double = z
を実装したこととなります.
Javaにおいて,例えば,コンストラクタなどで設定すべき引数が多くなってくると, 引数の順番が分からない,デフォルトの設定がある,といった事態が起きます. そんなときは組み合わせ爆発によりオーバーロードに限りがありません. その結果として,Builderを作ることとなるのではないでしょうか? Kotlinでは,以下のように,関数のデフォルト引数および名前付き引数を利用することができます.
data class Vector(val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0) fun main(vararg args: String) { println(Vector(1.0, 2.0, 3.0)) //Vector(1.0, 2.0, 3.0) println(Vector(1.0, 2.0)) //Vector(1.0, 2.0, 0.0) println(Vector()) //Vector(0.0, 0.0, 0.0) println(Vector(y = 2.0)) //Vector(0.0, 2.0, 0.0) println(Vector(1.0, z = 3.0)) //Vector(1.0, 0.0, 3.0) println(Vector(z = 3.0, y = 2.0)) //Vector(0.0, 2.0, 3.0) }
そして,Kotlinでは条件分岐のためのwhen
およびif else
ならびに例外処理のためのtry catch
が全て式となり値を持ちます.
これらを値を持つ式として扱えば条件漏れなどを無くせるのではないでしょうか?
他に,Kotlinには拡張関数という機能があり,これによってIterable
のメソッドが便利になっています.
以下にその例を挙げます.
filter
map
zip
take
drop
reduce
fold
sortedBy
zipWithNext
find
ここでは私が気に入っている機能について紹介させていただきました.
Bezier 曲線を描くならどっち?
KotlinとJavaで,どちらの方が楽にプログラムを書けそうか比べてみます. ここでは,Bezier曲線クラスの実装を例にします.
Bezier曲線とは
\(n\)次のBezier曲線 \( \boldsymbol{B} : [0, 1] \to \mathrm{E} \) はパラメータ\(t \in [0, 1]\)に対応する点\(\boldsymbol{B}(t) \in \mathrm{E}\)を返す関数で, \(n + 1\)個の制御点\(\boldsymbol{p}_{i} \ (0 \leq i \leq n)\)を用いて,
$$\boldsymbol{B}(t) = \boldsymbol{B}_{0}^{n}(t)$$
と評価します.ただし,
$$\boldsymbol{B}_{i}^{0}(t) = \boldsymbol{p}_{i} \ (0 \leq i \leq n)$$ $$\boldsymbol{B}_{i}^{j}(t) = (1-t) \boldsymbol{B}_{i}^{j-1}(t) + t \boldsymbol{B}_{i+1}^{j-1}(t) \ (1 \leq j \leq n, 0 \leq i \leq n - j)$$
です.
Intervalクラス
package jumpaku.kotlin data class Interval(val begin: Double, val end: Double){ operator fun contains(t: Double): Boolean = t in begin..end }
package jumpaku.java; public final class Interval { public Interval(final double begin, final double end) { this.begin = begin; this.end = end; } private final double begin; private final double end; public double getBegin() { return begin; } public double getEnd() { return end; } public boolean contains(final double t) { return begin <= t && t <= end; } @Override public String toString() { return String.format("jumpaku.kotlin.Interval(begin=%.1f, end=%.1f)", begin, end); } }
Pointクラス
package jumpaku.kotlin data class Point(val x: Double = 0.0, val y: Double = 0.0) { fun divide(t: Double, that: Point): Point { return Point(x.divide(t, that.x), y.divide(t, that.y)) } private fun Double.divide(t: Double, that: Double): Double { return (1-t)*this + t*that } }
package jumpaku.java; import java.util.Objects; public final class Point{ public Point(final double x, final double y) { this.x = x; this.y = y; } public static Point ofX(final double x) { return new Point(x, 0.0); } public static Point ofY(final double y) { return new Point(0.0, y); } public static Point origin() { return new Point(0.0, 0.0); } private final double x; private final double y; public double getX() { return x; } public double getY() { return y; } public Point divide(final double t, final Point that) { Objects.requireNonNull(that); return new Point(divide(t, x, that.x), divide(t, y, that.y)); } private double divide(final double t, final double d0, final double d1) { return (1-t)*d0 + t*d1; } @Override public String toString() { return String.format("jumpaku.kotlin.Point(x=%.1f, y=%.1f)", x, y); } }
BezierCurveクラス
package jumpaku.kotlin class BezierCurve(vararg controlPoints: Point){ val controlPoints: List<Point> init { require(controlPoints.isNotEmpty()) { "control point is empty" } this.controlPoints = listOf(*controlPoints) } val domain: Interval = Interval(0.0, 1.0) fun evaluate(t: Double): Point { require(t in domain) { "t($t) is out of domain($domain)" } return decasteljau(t, controlPoints) } private fun decasteljau(t: Double, controlPoints: List<Point>): Point { return when { controlPoints.size == 1 -> controlPoints.first() else -> decasteljau(t, controlPoints.zipWithNext { a, b -> a.divide(t, b) }) } } }
package jumpaku.java; import java.util.*; public final class BezierCurve { public BezierCurve(Point... controlPoints) { Objects.requireNonNull(controlPoints); if (controlPoints.length == 0) { throw new IllegalArgumentException("control point is empty"); } if (Arrays.stream(controlPoints).anyMatch(Objects::isNull)){ throw new IllegalArgumentException("control point contains null"); } this.controlPoints = Collections.unmodifiableList(Arrays.asList(controlPoints)); } private final List<Point> controlPoints; private final Interval domain = new Interval(0.0, 1.0); public List<Point> getControlPoints() { return new ArrayList<>(controlPoints); } public Interval getDomain() { return domain; } public Point evaluate(double t) { if (!domain.contains(t)) { throw new IllegalArgumentException("t($t) is out of domain($domain)"); } return decasteljau(t, controlPoints); } private Point decasteljau(final double t, final List<Point> points) { if (points.size() == 1) { return points.get(0); } else { ArrayList<Point> result = new ArrayList<>(); for (int i = 0; i < points.size() - 1; i++) { result.add(points.get(i).divide(t, points.get(i + 1))); } return decasteljau(t, result); } } }
動作実験
Kotlinのソースコード
package jumpaku.kotlin fun main(vararg args: String) { println("Jumpaku") val bezierCurve = BezierCurve( Point(-1.0, -1.0), Point(-1.0, 1.0), Point(1.0, -1.0), Point(1.0, 1.0)) (0..4).map { i -> i/4.0 } .map { bezierCurve.evaluate(it) } .map { (x, y) -> "$x, $y" } .forEach(::println) }
実行結果
-1.0, -1.0 -0.6875, -0.125 0.0, 0.0 0.6875, 0.125 1.0, 1.0
package jumpaku.java; import java.util.stream.IntStream; public class Main { public static void main(String... args) { System.out.println("Jumpaku"); BezierCurve bezierCurve = new BezierCurve( new Point(-1.0, -1.0), new Point(-1.0, 1.0), new Point(1.0, -1.0), new Point(1.0, 1.0)); IntStream.rangeClosed(0, 4) .mapToDouble(i -> i/4.0) .mapToObj(t -> bezierCurve.evaluate(t)) .map(p -> p.getX() + ", " + p.getY()) .forEach(System.out::println); } }
実行結果
-1.0, -1.0 -0.6875, -0.125 0.0, 0.0 0.6875, 0.125 1.0, 1.0
JavaとKotlinで同じ実行結果を出力するプログラムを書きましたが, Kotlinの方がシンプルで良いと思います. また,ソースコードはどちらも同じ設計に基づいていることも感じられると思います.
まとめ
Kotlinについて思ったことをひたすら書きました. 私は,KotlinはJavaと同じパラダイムの言語でありながら, Javaの無駄を削ぎ落とし, 推奨される書き方をサポートし, 様々な言語の良い部分を寄せ集めた言語であると思います.