環境構築のためのDocker

はじめに

これはMuroran Institute of Technology Advent Calendar 2018 - Adventarの記事です.

  • ホストマシンのOSはWindowsmacOSを使いたい.
  • Linuxも使いたい.(GUIは必要ない.)
  • ホストマシンの環境をできるだけ変更したくない.
  • 同じ開発環境を再現できるようにしたい.
  • できれば賢い誰かが構築した環境を利用したい.

そんな時,ホストマシンにDockerをインストールしておくと,

  • CUIだけの軽量なLinux仮想環境を構築できる.
  • 環境構築の手順はテキストファイルに記述できる.
  • そのファイルから開発環境を自動的に構築できる.
  • Docker Hubにアップロードされた環境を利用できる.

なんて,素晴らしい.

インストール

それぞれのOSにおけるDockerのインストール方法を以下に示します.

Windows 10 64bit : Pro, Enterprise or Education

Install Docker Desktop on Windows | Docker Documentation

それ以外のWindows

Redirecting…

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に従い以下のように作成されます.

  1. ubuntuのイメージをダウンロードする.
  2. 作業ディレクトリを/home/appに設定する.
  3. apt update -y && apt upgrade -y && apt install -y g++を実行し,アプリケーションのビルドの準備をする.
  4. g++ -o main main.cppを実行し,アプリケーションをビルドする.
  5. コンテナ起動時に./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に従い以下のように起動されます.

  1. python:3のイメージをダウンロードする.
  2. 作業ディレクトリを/home/appにする.
  3. ホストマシンの./appをコンテナの/home/appにマウントする.
  4. コンテナを起動して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学生部総大会 ヤバい同人誌執筆しようぜというイベントに参加しました.

connpass.com

これはOSC Hokkaidoや技術書展への出展を目指して,LaTeXで同人誌を書くというイベントでした. LaTeXはテキストファイルをPDFファイルに変換する組版システムです. LaTeXチューリング完全であるため,どんなアルゴリズムでも実装できます. そこで,ゲームを作りたいと思いました. ただ,正直,LaTeXでプログラムを書きたくはないし,インタラクティブなゲームを作るビジョンも浮かばないため, 実際にはリンク機能だけを利用したノベルゲームを作成しました. 一応,技術要素として

  • LaTeX によってノベルゲームを作成すること,
  • プログラミングによって効率的に論理クイズを解くこと

がコンセプトとなっています. 物語は説法系推理アドベンチャシリーズ

の外伝となっています. 個人的には,外伝のように後からストーリーが追加される形式を好まないのですが, イベント期間が睡眠,食事を含めて31時間と短いため,全く新しい物語を考えるのではなく,外伝という形式にしました.

このイベントの成果物はLOCAL学生部の「情報ボーイズの寄稿ノート」にまとめられ,技術書展4で紙媒体で頒布されます.

techbookfest.org

当然,紙媒体ではリンク機能を使えないのですが,これを考慮していなかったため,技術書展4で購入しても本ゲームをプレイできないという事態が発生しました. 本ゲームをプレイするにはPDFファイルを手に入れる必要があります. 「情報ボーイズの寄稿ノート」のソースファイルはGitHubリポジトリにあります.

github.com

ここからクローンしてきて,コメントアウトを解除してPDFにコンパイルすればプレイできますが, 手間がかかります. そこで,私の章だけをコンパイルしたPDFを用意しました.

世界と孤独の説法(エピローグ)_v1.pdf - Google ドライブ

「情報ボーイズの寄稿ノート」には私の他にもたくさんの著者がいて, それぞれが自分の得意分野の技術記事を書いています. 是非,そちらも読んでください.

リンク

室工ドライブ Drive around Muroran-IT

f:id:Jumpaku:20180226095745p:plain f:id:Jumpaku:20180226095726p:plain

"安全第一"

概要

本ゲームは室蘭工業大学(室工大)の周囲を安全にドライブするゲームです. 室工大の周囲には路上駐車,道路を横断する歩行者などの様々な障害があります. ドライバはこれらの障害を避けながら安全に室工大を一周します.

情報

  • タイトル : 室工ドライブ
  • 読み : むろこうどらいぶ
  • 作者 : Jumpaku
  • ジャンル : カーアクション
  • プレイ時間 : 1分程度
  • プラットフォーム : macOS
  • リリース : 2018年2月26日
  • 言語 : 日本語
  • 開発環境 : Unity
  • バージョン : 1.1

遊び方

  1. MurokouDrive_v1.appを実行します.
  2. 設定ウィンドウが開くので解像度,画質を設定します.
  3. Playボタンを押してゲームを起動します.
  4. STARTボタンを押してゲームを開始します.
  5. 反時計回りに道路を進みます.
  6. 運転に使用するキーは以下の通りです.
    • アクセルを踏む:上矢印
    • バックする:下矢印
    • ハンドルを右に切る:右矢印
    • ハンドルを左に切る:左矢印
  7. 道路から外れるとGame Overです.
  8. 室工大を反時計回りに一周するとClearです.

ダウンロード

実行ファイルは以下のリンクからダウンロードできます.
室工ドライブ_v1-1.zip - Google ドライブ

ソースコード

ソースコードは以下のリンクから参照できます. https://github.com/Jumpaku/MurokouDrive

開発

前からUnityに興味があり,今回初めてUnityで3Dゲームを作ってみました. 3Dモデル,当たり判定,当たり処理は既存のものを使いました. 工夫した点として,逆走を防ぐためにプッシュダウンオートマトンで状態を管理している点,地形や建造物をできるだけ実物に忠実に作成した点が挙げられます. 現在はmacOS向けのものしかリリースしてません.

生存報告ヴァルキュリア

adventar.org

シンデレラガールズ Advent Calendar 2017

Dec. 19

導入

アドベントカレンダーにおいて,デレステをきっかけに人生が完全に変わった事例が報告されている. muscle-keisuke.hatenablog.com 本報告には関係が無いが,もしまだ上の記事を読んでいないなら,是非読んでいただきたい. これは,その記事の筆者がサークルの共用タブレットにインストールされていたデレステをきっかけにして,

  1. デレマスにハマり,
  2. 赤城みりあ担当のプロデューサとなり,
  3. LINEを通じて会話することを目指して,
  4. プロデューサおよびアイドルが技術的,機械学習的に成長する

という話を書いた記事であり,次元の壁を超えたドラマとも言える.

一方,その裏で別のドラマが進行していたことを知る者は少ない. そこで,本報告ではそのもう一つのドラマの報告を目的として, もう一つのデレステをきっかけに人生が完全に変わった話について解説する. さらに,その上で,私が学んだことについて考察し, 最後に,現状の課題と今後の展望について述べる.

もう一つのデレステをきっかけに人生が完全に変わった話

上で紹介した記事と同様に, 私の人生もデレステをきっかけに完全に変わってしまった. 以下では,

  1. デレステと出会い,
  2. 超えられない壁に直面し,
  3. アイドル達の協力によってこれを超え,
  4. ユニットおよびプロデューサ自身を強化し,
  5. 「生存本能ヴァルキュリア」をマスターする

という話について,時系列に沿って説明する.

デレステとの出会い

今から約1年前,私が,自分の所属するサークルの共用タブレットにインストールされていた 「アイドルマスター シンデレラガールズ スターライトステージ(デレステ)」 を発見するところから,物語は始まる. 私はゲーム制作は好きだが,プレイにはあまり興味が無かった. また,アイドルも知らなかった. しかし,誰かがインストールしたデレステがふと気になった私はデレステを起動し,チュートリアルを始めた. 慣れないUI,うろ覚えのルール,それでもなんとか選曲まで辿り着いた. 私が知る曲など当然存在しないが,格好良い名前に惹かれて「生存本能ヴァルキュリア」を選ぶ. 私はクリアしたが,それどころではなかった. 初めて遊ぶリズムゲーム,その後ろで動く3Dモデル達,そして素晴らしい楽曲. 私はその全てに感動し,「生存本能ヴァルキュリア」を遊び続けた.

その時のユニットを以下に示す.

「生存本能ヴァルキュリア」MASTERクリアの壁

しばらく「生存本能ヴァルキュリア」で遊んでいた私は,難易度の存在に気が付いた. DEBUT, REGULARおよびPROの3つがあり,それまで遊んでいたのはDEBUTだった. すぐにREGULARに挑戦してこれをクリアし,PROに挑戦した. PROは簡単にはクリアできなかった. 私は,最高難易度なだけあってPROは難しいな,と感じつつ,何度もトライした. やっとの思いでクリアし,達成感に浸っていた私は, 最高難易度だと思っていたPROの横にMASTERが増えていることに気がついた.
「あんなに難しかったPROの更に上の難易度があるのか」

ここから数か月,孤独な苦難の時代が続く. 私は毎日「生存本能ヴァルキュリア」MASTERを練習した. しかし,何度MASTERに挑戦しても,サビ前でライフが尽きる. スタミナ,スタミナドリンクおよびリハーサルチケット等の練習のために使えるものは全て使ったが上達しない. また,ライブを最後まで見ることもできない. 追い込まれた私は

  1. スタージュエルでスタミナを回復できること
  2. スタージュエルでライブをコンティニューできること
  3. 今までもらったスタージュエルがたくさん余っていること

を思い出した. 私は,時間のある時はスタミナを回復して練習し, ライフが尽きた時はライブをコンティニューすることにした. その結果,ものすごい勢いでスタージュエルが消費されていった.

アイドル達の協力

スタージュエルはすぐに無くなり,私は行き詰まった. そんな時にふとアイドル編成のおすすめ編成機能が気になり,これでユニットを編成してみた. すなわち,事務所に所属していたアイドル達がもっと自分達を頼っても良いと協力を申し出てくれたのだ. 途方に暮れていた私は藁にもすがる思いで彼女達を頼った. 結果として,彼女らはライブを成功させた. 今まで不可能とも思えた「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしてしまった.

その時のユニットを以下に示す.

ユニットおよびプロデューサの強化

友人でもある同僚に「生存本能ヴァルキュリア」MASTERをコンティニュー無しでクリアしたことを報告すると,
「ゲーム下手な君が遂にMASTERをクリアしたか,でも,フルコンはしていないんだね.」 と言われた. そこで,私は「生存本能ヴァルキュリア」のマスター,すなわち「生存本能ヴァルキュリア」MASTERのフルコンを決意した.

前節の経験より,この目標のためには,自分一人ではなくアイドル達の協力が不可欠と考えた. そして,事務所内のユニットを強化するために

  • アイドルの強化
  • 他の楽曲のライブ
  • オーディション
  • イベントへの参加
  • ストーリーコミュ

などを行い,活動の幅を広げていった.

アイドルの強化では,

  • ライフ回復の特技のスキルレッスン,
  • ライフ上昇のためのレッスン,特訓およびポテンシャル解放

を行なった. そして,他の楽曲のライブを行うことで, プロデューサおよびアイドルの視野が広がった. また,オーディションやイベントへ参加がきっかけとなって, 事務所に新たなアイドルが増えた. 特に,ライフ回復を特技とするアイドルはすぐにユニットへ加えた. さらに,ストーリーコミュによって,事務所のアイドルへの理解を深めた.

一方で,プロデューサ自身はライブ成功に貢献するためにリズムアイコンのスピードを調整した. 具体的には,リズムアイコンのスピードを9.8とした. まず,私が画面上のリズムアイコンを全て把握するためには9.5程度以上でなければならなかった. スピードがこれより低いとリズムアイコンが多すぎて把握できなくなる. 次に,9.9程度というスピードは,私がリズムアイコンの出現を確認してから親指で対応する位置をタップできる限界の速度である. したがって,この時,指を最速で動かせばタイミングを計らずとも,タイミング良くタップできることになる. そして,私の目がリズムアイコンの動きを捉えられる限界が9.8程度である. リズムアイコンのスピードを9.8とすることで画面上のリズムアイコン全てに反応し,それらの動きを捉え,タイミング良くタップできる. 実際,この設定により,コンボ数が格段に上がった. 様々な楽曲でライブを安定してクリアできるようになり,私のライブ力も向上した.

この頃活躍していたユニットの例を以下に示す.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [花園の春風]西園寺琴歌+
  • [シュガーリーボディ]榊原里美+
  • [寡黙の女王]高峯のあ+

このようにして,元々アイドルにもソシャゲにも興味の無かった私はデレステにどんどんハマっていった.

「生存本能ヴァルキュリア」のマスター

他の簡単な楽曲のMASTERをいくつかフルコンできるようになって, アイドル達および私のライブ力の向上を実感してきたところで, 私は「生存本能ヴァルキュリア」のマスターのためのチャレンジに本腰を入れた. そして,何度もトライして遂にフルコンを達成した. 以下の画像はその時のものである. f:id:Jumpaku:20171219155830j:plain この画像から分かるようにフルコンするまでに実に323回のクリアがあった. 最初の2,3ヶ月の度重なるライブ失敗を考慮すれば, フルコンするまでの失敗を含めた総ライブ数は400-500回程度に上ると予想される.

また,フルコン時のユニットを以下に示す.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [ようせいのこ]遊佐こずえ+
  • [Sweet Witches' Night]森久保乃々+
  • [スクールデビル]小関麗奈+

この編成において,森久保乃々および小関麗奈がコンボを継続する一方で, 喜多見柚,岸部彩香および遊佐こずえがライフを回復するため, ライフ1でのコンボ切れによるライブ失敗を避けることが可能となっている. したがって,フルコンが途切れても,最後までライブを楽しむことができる.

すなわち,ユニット内のアイドル達のチームワークによって, ファンを最後まで楽しませるという最低限の仕事を放棄することなく, 「生存本能ヴァルキュリア」のマスターを目指すことが可能となる.

実際,フルコンするまでトライを続けることができたのは,例え途中でコンボが切れても, リズムゲーム,3Dモデルによる素晴らしいライブおよび好きな楽曲を最後まで楽しめたからである.

考察

以上の経験より, 私はプロデューサとアイドルの信頼関係の重要性について学んだ. プロデューサが超えられない壁に直面したとき,一人でできる努力には限界がある. それで嫌になって投げ出しては本末転倒である. そんな時は,プロデューサはアイドルに頼っても良いのである. 一方で,アイドルが成長するためには,プロデューサが視野を広く保ち, アイドル達に様々な経験をさせることも大切である.

課題と展望

MASTERより上の難易度ができた.MASTER+である. 「生存本能ヴァルキュリア」MASTER+も追加された. 以下のユニットによってなんとかクリアはできたが,まだフルコンはできていない.

  • [ハイテンションスマッシュ]喜多見柚+
  • [ゴージャスチアー]岸部彩香+
  • [花園の春風]西園寺琴歌+
  • [シュガーリーボディ]榊原里美+
  • [ようせいのこ]遊佐こずえ+

現在はクリア数66回にしてコンボCすら達成できていない. リズムアイコンのスピードを9.9としても画面上のリズムアイコンが多すぎて把握できず, 10.0とすると速すぎてリズムアイコンの動きを捉えられず,指も追いつかないという状況である. クリアするのがやっとの状態なので,コンボ継続を特技とするアイドルをユニットに加える余裕もない.

今後は「生存本能ヴァルキュリア」MASTER+のフルコンに向けて, アイドル達と相談しながら作戦を練りつつ,動体視力と指の移動力を鍛えていきたい.

謝辞

本報告は「生存本能ヴァルキュリア」をマスターするまでの過程を報告するものである. 以下に,その過程で協力していただいたアイドル達への感謝を表する.

はじめのうち私とライブの練習をしていただいたイヴ・サンタクロース氏,星輝子氏,塩見周子氏,横山千佳氏および双葉杏氏に感謝します.

行き詰まっていた時に頼らせていただいた前川みく氏,栗原ネネ氏,大沼くるみ氏,双葉杏氏および城ヶ崎莉嘉氏に感謝します.

様々なイベントまたは楽曲のライブで活躍していただいた喜多見柚氏,岸部彩香氏,西園寺琴歌氏,榊原里美氏および高峯のあ氏に感謝します.

タイプ別のユニットで活躍していただいたアナスタシア氏,篠原礼氏,渋谷凛氏,川島瑞樹氏, 村上巴氏,佐藤心氏,片桐早苗氏,日野茜氏, 関裕美氏,櫻井桃華氏および大原みちる氏に感謝します.

付録 JumpakuUnit

本報告には直接関係はないが, JumpakuUnitを以下に示す.

  • [ハッピーホーリーナイト]イヴ・サンタクロース+
  • [Tulip]塩見周子+
  • [Nothing but You]アナスタシア+
  • [∀NSWER]星輝子+
  • [寡黙の女王]高峯のあ+

Kotlinの良いところ

adventar.org

Muroran Institute of Technology Advent Calendar 2017

Dec. 18

Kotlin のここが良い

Kotlinという名前

「Kotlinは可愛い.」

初めてその名を見た時,私はそれがプログラミング言語であるとは気付かなかった. Kotlinを書き始めて数週間,私は,Kotlinは実はキメラなのではないかと思い始めた. 現在,私はKotlinの魔力に囚われている.

実行または開発環境

  • JVM上で動く
  • 環境構築が楽
  • Javaのライブラリを利用できる
  • Androidアプリも開発できるらしい

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!")とシンプルです.

ここまでの内容をまとめると,

  1. Javaには記述の無駄が多い.
  2. Kotlinはそれらの無駄を削ぎ落としたものとなっている.

と言えるのではないでしょうか.

不変性のサポート
  • val
  • デフォルトの修飾子

不変クラスはクラスの理想の姿と言えます. スレッドセーフであり,意図しない変更を受けず,矛盾した状態となることもないからです. 例えば,不変クラスのインスタンスへの参照を保持する読み取り専用の変数は, いつでもどこでも初期化時と同じ値を保つため,取り扱いが非常に楽です. プログラムを書くときは,不変クラスを作成したり,変数を読み取り専用としたりことによって, できるだけ不変とすることが望ましいと思います.

しかし,Javaにおいて,これは生易しい話ではありません. 全ての変数にfinalを付けていく作業は心の折れる作業です. また,不変クラスを作成する際は,そのクラスのインスタンスが保持するオブジェクトが変更されないことを保証しなければいけません. これを実現するためには

  • 継承不可とする.
  • フィールドを読み取り専用とする.
  • 可変なオブジェクトを変更しない.
  • 可変なオブジェクトの参照を共有しない.

といったことに注意しなければいけません.

ここで,不変クラスは推奨されていますが,Javaでの実装が困難であるという問題があります. これに対して,Kotlinには不変性を意識したプログラミングを援護する文法があり, 不変クラスの実装を楽に行えます.

例えば,変数を宣言する時にval a = 1と書くと,変数aは読み取り専用となります. これはfinal int a = 1;をとするより,シンプルです.

また,クラスおよびメソッドはデフォルトでfinalが付いた状態となっております. 継承可能とするためまたはオーバーライド可能とするためにはopenを付けます. そして,C++とは逆に,アクセス指定はデフォルトでpublicとなります. はじめのうち,私は,カプセル化に反する感じがして慣れませんでした. しかし,よく考えると, フィールドはプロパティによってラップされているので, オブジェクトが不変である場合には困る事はありませんでした.

以上のように,Kotlinは言語として不変性をサポートしてくれます.

その他の便利機能
  • 演算子オーバーロード
  • 分解宣言
  • data class
  • 関数のデフォルト引数と名前付き引数
  • when式, if else式, throw catch式
  • Iterableの拡張関数

ここでは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

Javaソースコード

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の無駄を削ぎ落とし, 推奨される書き方をサポートし, 様々な言語の良い部分を寄せ集めた言語であると思います.

Javaを使っている人,特に,室蘭工業大学情報系のあなた,是非Kotlinを使ってみてください.