それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

PHPでLeague/Routeを使ってルーティングを構成する

はじめに

どうもよく訓練されたJVM教のけんつです。
JVM環境でしか使い物にならないと言われてきた自分がWebサーバサイドのコードをPHPで書く機会がここ半年ほどで多くなってきた。
それでも、Webやったことないしわからんぞ!という感じなので素のPHPでオレオレTwitterクライアントを書く事にした。
とりあえず、イカしたdocker-composeを適当に引っ張ってきてイマドキのなうでヤングな環境を構築するところまではやった。

結構順調だったので、このまま最小構成を作ってしまおうとした矢先に自分を苦しめる存在が出てきた。
そうそれこそが

「素のPHPでルーティングどうやんの?」

という疑問である。


とりあえず、「PHP ルーティング」でぐぐってみると

  • apache 環境では…。いや使ってるのnginxだし!!
  • Cakeでルーティングはこうする…。 いや素のPHPで書きたいんだ!!
  • Laravelではこうする…。 だからフレームワークは使わんと言った!!
  • なかったから自作したお。 わけわからん!!

といった調子になった。

それでもいくつか、FastRouteのような使えそうなライブラリを見つけた。
しかし、以前Play Frameworkで色々やっていた自分に取ってみれば「書く事が多すぎてだるい!!」に尽きる。
それでも探し続けて、やっと「League/Route」なるものを見つけた。

色々とサンプルをみて、「これええやん!」となったが問題はPHPに浸かっている人間でなかったのでわかりにくいというか何をやっているかわからないという個人的な点にあった。
なので、ここでは自分なりに頑張って色々調べてみて「League/Route」を使うための公式にあるサンプルが何をやっているのかまとめてみようと思う。

route.thephpleague.com

環境

まずPHPのバージョンだが、Routeは5.4以降に対応しているので大抵の環境で動作するだろう。

Routeを使うにはcomposerかautoloadで持ってくるのを前提にしている。
自分はPHPを書くときはcomposerを使うのでそちらを参考にする。

$ composer require league/route

またこのライブラリはPSR-7を前提にしているのでそちらも解決する。

composer require zendframework/zend-diactoros

あとの文章に出てくるLeague\Containerでは2.x系のものを使用している。
こちらはバージョンによって仕様がかなり違うので注意が必要。

と環境はここまでで構築できるはず。

ルーティングを構成する基本要素

環境を構築したら、次のようにコードを書けばルーティングが実装できるよ。と公式には書いているが

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

$route = new League\Route\RouteCollection($container);

$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここらへんを全てフレームワークに投げていた人にはよく分からない。
なので、該当するPSRなどを読んで理解していく。

まず、先頭にあるこのコード

$container = new League\Container\Container;

League\Containerを呼び出しているのだが、ここで指すコンテナってなんぞ?とおもったので調べてみた。

Dependency Injection Container (PSR-11)

DIコンテナと言われるものでPSR-11内で決定されている。
DIコンテナと言われても分からない。なので調べた。
PSR-11については以下のページが
qiita.com

DIについては以下のページが個人的に分かりやすかった。
qiita.com

そもそもDIとは

つまりこれは何かと言われると、例えば以下の処理をしようとする。

<?php
class Sample
{
    private $engine;

    public function __construct()
    {
        $this->engine = new Engine();
    }
}

ここではnew Engineとしたがこれはテンプレートエンジンでもいいし、エンジンでなくてもいい。
依存性を直接コンストラクタやメソッドのスコープ内で生成してしまうコードを指す。

これらは上記のqiita記事でも書かれているが
上のコードだと何が嬉しくないというと

  • Engineクラスを別のクラスに変えたりするとSampleクラスを直接変更しなければならない。
  • Engineが上手く動作してくれないとそもそも、このクラスのテストが出来ない。

という点にある。

それを次のようにすると嬉しいことがある。

<?php
class Car
{
    /**
     * @var EngineInterface
     */
    private $engine;

    /**
     * @param EngineInterface $engine
     */
    public function __construct(EngineInterface $engine)
    {
        $this->engine = $engine;
    }
}

何が嬉しいかというと、依存性が外部で完結しこちらのクラスではそれを受け取っているだけなので変更も容易でテストもしやすい。

しかしこれも残念な点を抱えている。
何かというと色んな依存関係を解消しようとすると引数がめちゃくちゃ長くなりやすいという点。
それと結局依存を注入する手続きは手動で行っており,抽象ではなく具象に対して依存するコードを書くことになるという点。
それらの問題を解決するためにDIコンテナが存在する。

DIコンテナとは

DIコンテナについては以下のサイトがわかりやすくまとまっていた。
qiita.com

つまり、今まで抽象っぽくなってたけど結局具象な上に手動で行っていたものを別な場所(DIコンテナ)に突っ込んで管理しよう。というのを実現するためにある。(らしい)

今回の環境ではLeague/Containerを使っているので公式から使用例を引用してくる。
container.thephpleague.com

まずは前提として以下のようなコードがあるとする。

<?php

namespace Acme\Service;

class SomeService
{
    // ...
}

これに対してDIコンテナを次のように使うことができる

<?php

$container = new League\Container\Container;

// register the service as a prototype against an alias
$container->add('service', 'Acme\Service\SomeService');

// now to retrieve this service we can just retrieve the alias
// each time we `get` the service it will be a new instance
$service1 = $container->get('service');
$service2 = $container->get('service');

var_dump($service1 instanceof Acme\Service\SomeService); // true
var_dump($service1 === $service2); // false

使い方としてはコンテナにDIしたいサービスのクラスを渡すこと。
それをgetすることで実際にそれらを利用することができる。
しかし、add/getではgetするたびに新しいインスタンスになるため

$service1 === $service2

という、評価式がfalseを返す。この点には注意したい。

実際にルーティングを組むときにはgetではなくshareメソッドが呼ばれているが実装は以下のようになっている。

<?php

    /**
     * {@inheritdoc}
     */
    public function share($alias, $concrete = null)
    {
        return $this->add($alias, $concrete, true);
    }

addメソッドに$shareというフラグをtrueにして呼び出しているだけである。
公式ドキュメントでは$shareがtrueになっているもの関しては先程の評価式がtrueになる。
つまり同一のインスタンスが返ることになる。
同一インスタンスが返る場合としては、addメソッドをコールした場合でもクラスインスタンスを直接渡す場合もなると公式に書いていた。


ここまでの内容を理解すれば、ルーティング設定時のこの部分を理解できる。

<?php

$container = new League\Container\Container;

$container->share('response', Zend\Diactoros\Response::class);
$container->share('request', function () {
    return Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES
    );
});

$container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);

まずDIコンテナを作り、そこにサービスをshareメソッドを使って追加している。
具体的にはPSR-7に準拠したサービスである、ResponseとRequestにEmitterを追加している。

が、しかしPSR-7とは何者か知らなかったので次はそれをまとめる。

HTTP message interfaces (PSR-7)

PSR-7は何を決めているかというと、題名の通りPHPでHTTP通信を扱うときのリクエストとレスポンスの標準となる仕様を決定している。
これに関しては公式と以下のサイトが役に立った。

www.php-fig.org
qiita.com

全てのHTTPリクエストには特定の形式が存在しRequestは以下のような

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

Responseは次のような形式をとる。

HTTP/1.1 200 OK
Content-Type: text/plain

This is the response body

これをPSR-7では次の2つのインターフェースを使って宣言している

Psr\Http\Message\RequestInterface
Psr\Http\Message\ResponseInterface

これらを使うことで、自分でああだこうだしないでHTTPリクエストをPHPで扱うことができる。
ここをまとめようと思うと、DIコンテナの比にならないくらい文章量が多くなってしまうのでここでは割愛。後日別記事にする。

しかし、何故これが必要なのかというとルーティングを構成する次の部分で使っているからである。

<?php
$route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('<h1>Hello, World!</h1>');

    return $response;
});

そう、この部分をみてわかるようにルーティングを構成して呼び出すメソッドはPSR-7準拠のインターフェースを引数に持ち$responseを返すことになっているからだ。
今上記のコードだと、HTTPレスポンスのボディにHTMLを書き込みそれを返しているのでHello,World!が表示されるといった状況になっている。
これはこの後、クラスを指定する場合でも同様に呼び出すメソッドには関係してくる。

Dispatchから先

残りはこの部分。

<?php

$response = $route->dispatch($container->get('request'), $container->get('response'));

$container->get('emitter')->emit($response);

ここではdispatchに先ほど設定したDIコンテナの値を使っている。
それをdispatchメソッドに渡している。dispatchメソッドの実装は次の通り。

<?php
    /**
     * Dispatch the route based on the request.
     *
     * @param \Psr\Http\Message\ServerRequestInterface $request
     * @param \Psr\Http\Message\ResponseInterface      $response
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
    {
        $dispatcher = $this->getDispatcher($request);
        $execChain  = $dispatcher->handle($request);
        foreach ($this->getMiddlewareStack() as $middleware) {
            $execChain->middleware($middleware);
        }
        try {
            return $execChain->execute($request, $response);
        } catch (Exception $exception) {
            $middleware = $this->getStrategy()->getExceptionDecorator($exception);
            return (new ExecutionChain)->middleware($middleware)->execute($request, $response);
        }
    }

dispatcherを取得した後にDispatcherクラスのhandleメソッドをコールしてミドルウェアを適用しそれらを実行している。
最後に今までのあれこれをemit(うまく表現できる日本語がわからなかった)することでルーティングで設定したあれこれが実行される。

ルーティング周りをもう少し頑張ってみる

これまでまとめたところが実は公式で言うところのHello Worldでしかない。
PHPをやったこともない人間が雑に触ろうとした結果PSR-7,11を調べたりしないと行けないのですごく大変だった。
ただ、ここまでやった段階だけでは無名関数を毎回書いて呼び出す必要があったり、もしルーティング内でワイルドカードを使用したい場合などどうするんだとなるのでそのあたりを公式サイトをみてまとめていく。

ワイルドカードを使いたい場合

これが自分自身このライブラリを使いたかった理由のひとつでもあるが、ルーティングとしてpathを設定するときにワイルドカードは次のように使える。

<?php

$router = new League\Route\RouteCollection;

$router->map('GET', '/user/{id}/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

もっと値を限定して使うなら次のようにする。

<?php

$router = new League\Route\RouteCollection;

// this route will only match if {id} is numeric and {name} is a alpha
$router->map('GET', '/user/{id:number}/{name:word}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

ここで指定できる種別には以下のものがある。

  • number
  • word
  • alphanum_dash
  • slug
  • uuid

ここまででも十分便利なのに正規表現を適用することもできる。

<?php

$router = new League\Route\RouteCollection;

$router->addPatternMatcher('wordStartsWithM', '(m|M)[a-zA-Z]+');

$router->map('GET', 'user/mTeam/{name:wordStartsWithM}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
    // $args = [
    //     'id'   => {id},  // the actual value of {id}
    //     'name' => {name} // the actual value of {name}
    // ];

    return $response;
});

HTTPメソッドでルーティングを設定

今まではmapを使った例が多かったがもう少し便利なものがある。

<?php
$route = new League\Route\RouteCollection;

$route->get('/acme/route', 'Acme\Controller::getMethod');
$route->post('/acme/route', 'Acme\Controller::postMethod');
$route->put('/acme/route', 'Acme\Controller::putMethod');
$route->patch('/acme/route', 'Acme\Controller::patchMethod');
$route->delete('/acme/route', 'Acme\Controller::deleteMethod');
$route->head('/acme/route', 'Acme\Controller::headMethod');
$route->options('/acme/route', 'Acme\Controller::optionsMethod');

直接指定することもできるし、HTTPメソッドごとに処理を分けることもできる。

ホストやスキームを指定する

なんてこともできる。

<?php

$route = new League\Route\RouteCollection;

// this route will respond to http://example.com/acme/route
// or https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setHost('example.com');

// this route will only respond to https://example.com/acme/route
$route->map('GET', '/acme/route', 'AcmeController::method')->setScheme('https')->setHost('example.com');

ルーティングをグループとしてまとめる

これが一番使いたかった機能だと思う。
ルーティングをグループ化して追加できるのはスゴク便利。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
});

もちろんこれらも個別にホストやスキームを設定できる。

<?php

$route = new League\Route\RouteCollection;

$route->group('/admin', function ($route) {
    $route->map('GET', '/acme/route1', 'AcmeController::actionOne');
    $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setScheme('https');
    $route->map('GET', '/acme/route3', 'AcmeController::actionThree');
})
    ->setScheme('http')
    ->setHost('example.com')
;

そして実際のリクエストは次のようになる

GET http://example.com/admin/acme/route1
GET https://example.com/admin/acme/route2
GET http://example.com/admin/acme/route3

これスゴク便利。

さいごに

とりあえず、ここまでが自分の使いたかった機能を一通り網羅した。
ルーティングについて雑に書くはずがDIコンテナやPSRについてまで触れるとは思わなかったけどかなり理解が深まった。
関連するPSRやその他についてもこの後まとめていきたい。

あと自分はWebあたりの知識が割と不足していてわからないことだらけだったので、単純に語句とか調べてなるほどなとなるより
コードをみてこういうふうになっているのかと理解する方がよいということがわかったのは収穫だった

KLabの技術系インターンに参加してきた話

はじめに

人生初のインターンということでKLabさんの技術系インターンに参加してきました。
www.klab.com

今回、期間中お世話になったメンターは山本雅也さんです。
twitter.com


進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

インターンに行くまで

まず、何があったかというと昨年の9月頃にこんなことがありました。




はい、つまりはそういうことです。

この前に、chikuwait君と話していて
「ネットワーク、興味あるから勉強したいんだよねぇ」
と、雑に話した途端にこれです。

というわけで、この後に山本さんに直接連絡を取りリモートで面談を行い
すごくあっさりと決まってしまいました。

ただ、ネットワーク周りを本当にやったことがなかったのでインターンまでに
Linux カーネルモジュールで最速のEchoサーバを作る」

DHCPの仕様をざっくりと理解してくる」
といったことを事前に行いました。

進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

目標

今回のインターンでの目標はメンターである山本さんが作っているユーザ空間で動作するTCP/IPプロトコル・スタックである「microps」にDHCPクライアント機能を追加実装する。
ただ、それだけです。

micropsとは

micropsは前述の通り、ユーザ空間で動作するTCP/IPプロトコル・スタックで、Ethernetフレームの送受信を行います。
加えてカーネル内のプロトコル・スタックを使用していません。
僕が機能追加する前の段階でSocketライクなAPIを提供する機能もありました。

github.com

インターン(0日目)

飛行機代を抑えるために早朝の便をとり、なんと前日の昼前に東京着という意味不明なスケジュールで行動していました。
初の東京だったのでまずどこに行こうかと思い、やっぱりアキバにいってしまいました。
その後、紆余曲折を経てホテルに到着。初日からの戦闘に向けて美味しいものも食べてきました。

インターン(1日目)

この日は、10:30ごろにKLabさんが入っている六本木ヒルズ森タワーに行きました。
気合が入りすぎて5時ごろには起床していたので、何故か江東区から歩いていくという強行に出ました。

この日はまず
インターンのガイダンス説明から始まり
メンターさんの紹介やその他諸々の手続きを行い
その後すぐに会議室に移りmicropsの仕様とDHCPに関する説明を30分ほど受けました。

それからはひたすらに開発環境の構築やmicropsのビルドなどを行い、その日は終了。

インターン(2日目)

この日はDISCOVERパケットを構築すること、またそれを送信し返ってくるOFFERパケットを解析することが目標だった。
DHCPクライアントがどのようにIP等を手に入れているかというと、ざっくりと以下の4プロセスを踏む

  • DISCOVER パケットをクライアントがブロードキャストで送信
  • OFFER パケットが返ってくるので情報を解析
  • 必要な情報を取り出しREQUEST パケットに組み込み送信
  • ACK or NACKが返ってきて終了

2日目はこの前半2つを行った。

まずはRFCや事前資料からDISCOVERパケットを構築するところから始めた。
パケット自体は固定形式部分が234byte、可変形式のオプションが64byteという構成になっている。
またIPを取得していない状態でsocket等を使用するため、送受信は全てブロードキャストを使用する。
そのため、ブロードキャストフラグも使用し明示的にブロードキャストを指定。

オプションは4byteのマジックコードに始まり、パケットの種別を示すものが3byte続く。
その後はクライアントIDやMACアドレス、サーバ固有情報などが含まれ最終的には0xFFをStopperとしている。
Stopperから末尾まではデータが入っているはずがないので0x00で埋めてある。

ここまでがパケット構成となる。
その結果を以下に示す。

========== DHCP Message Debug Print ==========
    op: 1
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 0.0.0.0
siaddr: 0.0.0.0
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 01
option[3d] 01 08 00 27 0c a1 27
option:[ff]
total 300 bytes (padding 47 bytes)

次に構築したパケットを送信、レスポンスを受信する処理を追加した。
ここで使用したものはmicropsに始めから搭載されていたudp関連のsocketライクなAPI

レスポンスデータは形式こそオプション以外では変化が無いもののオペコードは必ず2になって(送信時は1を指定)返ってくる。
またオプション部分でマジックコードの次もOFFERパケットを示すものに変わっている。

以下が受信したOFFERパケットの情報となる。

========== DHCP Message Debug Print ==========
    op: 2
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 02
option[36] c0 a8 00 01
option[33] 00 00 02 58
option[01] ff ff ff 00
option[03] c0 a8 00 01
option[06] c0 a8 00 01
option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c
option:[ff]
total 304 bytes (padding 0 bytes)

二日目はここで終了。

インターン(3日目)

3日目はOFFERパケットから必要な情報を取り出し、REQUESTパケットを構築し送信
返ってくるACK(or NACK)を解析して、micropsを使用したEchoサーバの設定に必要な情報を組み込むところまでが目標。

まずは、yiaddrから使えるIPを取り出してREQUESTパケットとして構築。
構築したパケット自体は2日目に行ったものとほとんど同様なので概要は省略。

========== DHCP Message Debug Print ==========
    op: 1
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 03
option[3d] 01 08 00 27 0c a1 27
option[32] c0 a8 00 66
option:[ff]
total 300 bytes (padding 41 bytes)

次にACK(or NACK)を受け取る。
これもOFFERパケットの受信と同様に行った。

========== DHCP Message Debug Print ==========
    op: 2
 htype: 1
  hlen: 6
  hops: 0
   xid: 5abda168 (1522377064)
  secs: 0
 flags: 8000
ciaddr: 0.0.0.0
yiaddr: 192.168.0.102
siaddr: 192.168.0.1
giaddr: 0.0.0.0
chaddr: 08:00:27:0c:a1:27
 sname: 
  file: 
 magic: 63 53 82 63
option[35] 05
option[36] c0 a8 00 01
option[33] 00 00 02 58
option[01] ff ff ff 00
option[03] c0 a8 00 01
option[06] c0 a8 00 01
option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c
option:[ff]
total 304 bytes (padding 0 bytes)

ここまでは前日に行っていたこととあまり大差なかったのですんなりいった。
問題はこのあと、実際にACKから必要なIPアドレスサブネットマスクデフォルトゲートウェイを取り出して設定として組み込む点だった。
IPアドレスなどはすぐに見つかり、RFCなどにオプション種別も記載されていたので比較的容易に発見できた。

それを組み込む際にip_init 関数が存在するがこいつを単に呼び出すとethernetフレームにipプロトコルが二重に追加されてしまう問題があった。
またルーティングテーブル周りの初期化や、このmicropsを利用する人が任意でDHCPクライアント機能を利用できるようにするなど
やることが山積みだった。

このあたりはメンターの山本さんの力を借りて(もちろんここまで来るのに絶大な支援があった)、実装することができた。

ここで3日目が終了。

インターン(4日目)

この日は、最低限の処理しか追加していなかったコードにオプション解析などを組み込みデータがちゃんと正しいものか、こちらが想定しているものかチェックする処理などを追加
その後はひたすら鬼のペアプロコードリファクタリングを行った。(多分これが一番きつかった)

インターン(5日目)

この日は最終成果発表に向けて資料の準備とPRの作成を行った。
期間中、多大なご迷惑をお掛けした分プルリクがマージされた時はすごく嬉しかったというか、やったぞという気持ちしかなかった。

その後の成果発表も無事に終わり、Klabの方々とご飯を食べに行きこの日は終了。
そしてインターンも終了。

インターン(6日目)

帰りの飛行機は例によって飛行機代を抑えるためにほぼ最終便をとっていた。
そのため、馬鹿みたいに時間があったので大門->スカイツリー->浅草->上野、アキバと観光してきた。
余談ではあるが、人生経験の一環としてメイドカフェに行こうか本気で迷った。

さいごに

というわけで長かったようで短く感じた怒涛の初インターンは終了した。
インターンでやったことあったことを雑にまとめてみたが、実際にはスケジュールがかつかつで8時間はコードを書いている日の連続だった。
技術的な面でも未熟であったことがわかったが、その他にも色々と自分には知らないことが多かった。
成果発表で、色々なエンジニアの方に今回何をやったかということを報告し「わからない事にわからないなりに挑戦し、完成させたという点は評価できる」との評価を頂いたが
やはり実際に作っている時などに、「これはきいたことがあるぞ」「これは知っているかもしれない」と思っていたこともあったがそれをいざ実行しようとなると「お、なんか上手くいかないぞ」といったことが多くあり
わかっている事とできる事は全く別物であると痛感した。
また、自分は今までコードばかりを書いていてそれが楽しかったからというのもあるが
もっと自分が作るものの基盤的な知識を身につけなければいけないということも実感した。

そういった、自身の弱点のようなものを見続けたので今回のインターンはすごく辛く感じる面が多かった。

しかし、それ以上にものすごいレベルのエンジニアの方たちに囲まれて
今回のようにコードを書いて、何かを実現してという環境に身を置けたのはすごく楽しかった。
辛いと感じることもあったが、それでもわからないことをわからないと認めてその上で色々と試行錯誤を繰り返し一つの問題を解決していく。そういった時間を過ごせたことはこの上なく幸せな時間だった。

学ぶべきことが多く分かった反面で、学んだことも非常に多かった。
一週間ではあったが自分自身が以前に比べて成長できたことを感じることができたのでインターンに参加して本当によかったと思った。


最後に、今自分がいる大学ではあまり情報系に熱心な同期がいない。
そして家でもコードを書いて、それを楽しいと5年も言い続けている自分はそんな普通な人からみるとキモいという対象らしい。
そんな人たちに流されて普通の大学生活を送ろうとしていたが、今回のインターンでお昼休みなどに山本さんとキャリアについて話していて
自分のやりたいことがはっきりと見えたので周りを気にしないで残りの学生生活を自分なりに有効活用しようと思う。

本当に今回インターンに参加できてよかったとおもう。
今回、お世話になったKLabの皆さん、そしてなによりメンターとして1週間指導してくださった山本雅也さん
本当にありがとうございました。

無限インターン行きたい!!

プログラマのためのSQL第4版を読んで。〜 データベース VS ファイルシステム 〜

はじめに

訳あって今、カーネルからWebまでという非常に広い範囲を日常的に触っている。
しかし、一日の時間は24時間と決まっており自分にはコードを書く事以外にもしなければならないことがある。
だが、コードを書かないという日はなるべく作りたくない。
我儘を言うなら、限られた時間でC言語で低レイヤーをやったりPHPを使って高レイヤーをいじったり、ScalaJavaを使ってその中間も色々やってみたい。

そう思った時に、その全てを叶えるのがデータベースだと思った。

NoSQLであるRedisはC言語ベースで作られていてソースコードGithub上で公開されている。
PHPではサーバサイドとして実際にDBを操作する時がある。
Scala/Javaにはjdbcなどがある。

つまり、先ほどの我儘を全て叶えてくれる非常に魅力的な領域だった。
というわけで、そんなふわふわした理由のもとにデータベースを勉強してみようと思った。

単にデータベースを勉強するといっても、実際にDBを使うことに始まり
それらを利用するためのドライバやDB自体をソースコードレベルで理解することまでを目標にする。

その上でまずDBの使い方、特にSQLについて深く勉強したいと思ったので「プログラマのためのSQL」を購入した。
ここではその書籍を読んで勉強したことをまとめていく。

データベース VS ファイルシステム

データベース、特にRDBMSにおいては普段利用するプログラミング言語が操作するファイルシステムとはかなり勝手が違う。
データベースを操作するのにはSQLを使用し、SQLプログラミング言語と異なり独自のI/Oシステムを持たずデータの宣言、操作、制御のみを行う。

しかし、プログラミング言語にもSQLにもモデルは存在する。
そのモデルを理解することで対象を理解することが容易になる。
プログラミング言語におけるモデルとは、あまり詳しくないのではっきりとは言えないが「いくつかのプログラミング言語は数学をベースとしているため数学で実際に使用する記号群や構成を使用することができる。」といったことを指す。
SQLにおけるモデルは数学でいうところの集合(Set)としてのデータである。
数学でいう集合は、それに含まれる要素は特定のタイプに属していて順序をもたない。そして、集合に対する操作は一度に全ての要素に対して行われる。

わかりやすい例が参考文献に記述してあった。

正の整数の集合から奇数の部分集合を求める場合、答えはすべての奇数を含む単一の集合として得られる。奇数を1つずつ調べて順番に集合を作っていくようなことはしない。ただ、奇数を「2で割ったときの余りが1になる」という条件を満たす数として定義するだけだ。

このような背景があるために、分類の条件を変更したとしてもテスト可能で、かつ分類そのものも可能である。
それ故、集合を対象とする集合指向モデルは並列処理に向いている。


SQLは3つのサブ言語から構成されている

  • DDL: データ宣言言語(Data Declaration Language)
  • DML: データ操作言語(Data Manipulation Language)
  • DCL: データ制御言語(Data Control Language)

DDLはデータベースに含まれる中身を定義し、そこに含まれるデータの整合性を確保する。
前述のファイルシステムでは整合性やデフォルト値、他のテーブルとの関連性は一切定義されない上に厳密にデータの論理的一貫性を保つ機能はない。
しかしデータベースにおいてはDDLがそれらの役割持ち、DMLとDCLと共に動作するためSQLは統合された全体として扱われファイルの様に分離された一部分とは扱われない。

DMLSQLを一度でも書いたことがある人ならわかる。
SQLにおける次の操作を示す。

  • SELECT
  • INSERT
  • UPDATE
  • DELETE

DCLはここではまとめない。かなり深い概念らしくあまり解説されていなかったから。


ここまでのまとめ

  • SQLは集合をモデルにしている
  • 3つのサブ言語から構成されている
  • データベースにおけるスキーマはファイルの集合体ではなく、関連性を持つ。
  • テーブルはファイルではなく、スキーマの一部である。

エンティティとしてのテーブル

データベース、特にリレーショナルデータベースにおいてエンティティとは属性によって定義される。そしてそのインスタンスはテーブルにおける1行1行を指す。
属性とはテーブルでいうところの列であり、値はそれ以上分解不能な原始的な値であるスカラ値をもつ。
よってエンティティとしての役割をデータベースのテーブルは持つことになる。

完全に同じ構成のテーブルが2つあるとき集合的な観点からみるとそれらは同じ種類の要素を持つ集合でしかない。
これはファイルであれば許されることである。なぜならファイルはそれら自体は物理的に分類された単位であるために同一情報を異なるファイルとして保持することが許容されている。
しかし、前述の内容よりSQLにおいては集合であり区別が付かないのでそのような物は一つにまとめてしまうのがよい。

関連としてのテーブル

これは非常に簡単なことで、テーブルにおける関連とは列が一つ以上のエンティティテーブルを参照することによって成立している。
これはファイルとフィールドにはない特性になっている。

行 VS レコード

行はレコードではない。レコードはそれを読み込むアプリケーション側で定義されるものであって、それ自体はスキーマで定義される。
フィールドの名前はアプリケーション側で定義されるのに対して、行はデータベーススキーマ中で定義されるということになる。


ここで空のファイルを考える。
空のファイルは0バイト長という状態であるが、空のテーブルに関しては行は空としても列を持っていて理論的制約などを保持している。
つまり数学的意味合いの集合における空集合とは少し異なり、上記の構造をとるためたとえ空であっても別の集合として扱うべきである。

次の行にある特徴としてはテーブル内の全ての行は構造上同一な形式を取る点であり、これはファイルにはない。
ファイルであれば内部に含まれるデータの構造などは全て一致しているわけでもなく文字列であったり数値であったり情報もサイズもバラバラにしてしまうこともできるからだ。

列 VS フィールド

レコード内のフィールドはそれを読み込むアプリケーション側で定義されるが行における列はデータベーススキーマで定義される。
そして列が保持するデータ型は前述の通りスカラ値を持つ。

そして、SQLでは列は列名によってのみアクセスされる。
厳密に列名によってのみというわけではなく

SELECT *
INSERT INTO <table name>

の様な省略形は存在する。しかしこれは列名のリストを単にテーブル定義で列が定義された物理順序で展開しているだけにすぎないので内部的には列名でのみアクセスされる。
SQLにおけるNULLの使用もこれに近いものとなっている。

列があるが故に、ファイルとの違いがある。
ファイルは受け入れる内容や吐き出す内容に特に制約は無いがSQLにはその制約が存在する。
それにより、SQLが扱うものには一定の整合性が保証される。

つまりはそれが指す関係の整合性を示すことになる。
これはファイルごとに独立しているファイルシステムには無い特徴となっている。

おわりに

SQLとしての基盤的な事象をファイルシステムと比較してまとめてあったのはSQLを理解する上で非常に大事な情報であると感じた。
次はDBMSが内部的に行っているトランザクションと同時実行制御に関する章を読んでまとめていきたい。

参考文献

最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part4

はじめに

前回、実装したはずの機能で特にacceptでコケる問題がなかなか解決出来なかったので

毎回おなじみになりつつあるこのサイト
linux/include - Elixir - Free Electrons
さらに今回は、この本も参考にして書いていく
www.oreilly.co.jp

まず色々、調べたりしたことをまとめてその後に本題に入ろうと思う

色々やる前のメモ

実際に動いたであろうサンプルソースを参考にああだこうだ考えながら書いていたがacceptでコケる。
省略していた待ち行列が関係してるのではと思っている。

そもそも、エラーコード返ってきてるのにそれを見てなかったから見てみる。
見てみた。

$dmesg
....
....
[524524.095520] Fastest Echo Server Start!!
[524524.095544] accept error no:-11
$

なんか、acceptでエラーが返ってきている。
それを上記のサイトで調べる。
まずはacceptの実装から。
acceptの実装は次のようになっていた。

static int accept(struct socket *sock, struct socket *new_sock, int flags)
{
	struct sock *sk = sock->sk;
	struct sk_buff *buf;
	int res;

	lock_sock(sk);

	if (sock->state != SS_LISTENING) {
		res = -EINVAL;
		goto exit;
	}

	while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

	buf = skb_peek(&sk->sk_receive_queue);

	res = tipc_create(sock_net(sock->sk), new_sock, 0, 0);
	if (!res) {
		struct sock *new_sk = new_sock->sk;
		struct tipc_sock *new_tsock = tipc_sk(new_sk);
		struct tipc_port *new_tport = new_tsock->p;
		u32 new_ref = new_tport->ref;
		struct tipc_msg *msg = buf_msg(buf);

		lock_sock(new_sk);

		/*
		 * Reject any stray messages received by new socket
		 * before the socket lock was taken (very, very unlikely)
		 */

		reject_rx_queue(new_sk);

		/* Connect new socket to it's peer */

		new_tsock->peer_name.ref = msg_origport(msg);
		new_tsock->peer_name.node = msg_orignode(msg);
		tipc_connect2port(new_ref, &new_tsock->peer_name);
		new_sock->state = SS_CONNECTED;

		tipc_set_portimportance(new_ref, msg_importance(msg));
		if (msg_named(msg)) {
			new_tport->conn_type = msg_nametype(msg);
			new_tport->conn_instance = msg_nameinst(msg);
		}

		/*
		 * Respond to 'SYN-' by discarding it & returning 'ACK'-.
		 * Respond to 'SYN+' by queuing it on new socket.
		 */

		if (!msg_data_sz(msg)) {
			struct msghdr m = {NULL,};

			advance_rx_queue(sk);
			send_packet(NULL, new_sock, &m, 0);
		} else {
			__skb_dequeue(&sk->sk_receive_queue);
			__skb_queue_head(&new_sk->sk_receive_queue, buf);
		}
		release_sock(new_sk);
	}
exit:
	release_sock(sk);
	return res;
}

この中で、一通り関係しそうなものをerror.hとかerror-base.hから探してみる。
そうすると

#define EWOULDBLOCK EAGAIN

が返っていることがわかり今度はこのEAGAINを調べた。
すると、こいつが例の11番のエラーを返していた。
ただ

#define EAGAIN 11 //try again

とあり、何をtry againすればいいのかわからなかった。
そこでaccept関数の実装をみて、どこでこのエラーが返ってきてるか調べてみることにした。

するとここの処理が影響していることがわかった。

while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

skb_queue_emptyが返す値が真であるならO_NONBLOCKをflagsとして渡しているので
今回のgotoで該当の処理がスキップされエラーとなってしまうためskb_queue_emptyが返す値が偽である必要がある。

skb_queue_empty関数に渡しているものは

&sk->sk_receive_queue

という構造体の値である。
そもそもこのskが何かというとaccept関数の先頭で宣言、初期化されている次のものである。

struct sock *sk = sock->sk;

右辺のsockはaccept関数に渡している第一引数である。
そして問題のskb_queue_empty関数は次のようになっている。

static inline int skb_queue_empty(const struct sk_buff_head *list)
{
	return list->next == (struct sk_buff *)list;
}

これはソケットバッファに関するキューの様でリストの次の要素と先頭要素が等価であることを調べている(っぽい)。

これとはまた別にO_NONBLOCK、ノンブロッキングIOについてもよくわかっていなかったのでそれも別途記述していく。

ノンブロッキングI/O

カーネルモジュールで、TCPソケットを使おうとしたらほとんどのソースにO_NONBLOCKが出てきてなんだこれはと調べると
次のサイトに色々まとまっていた。

blog.takanabe.tokyo

ノンブロッキングI/OではI/O対象のファイルディスクリプタの準備完が了していないことをアプリケーション側に伝えるため即座にエラーが返る(errnoにEGAINが格納されて返ってくる)。一般に、O_NONBLOCKフラグを利用してノンブロッキングモードを宣言するが、この時プロセスはブロック状態にならず、CPUを他の処理に回すことができるためI/O待ち時間を有効活用できる。

ノンブロッキングI/Oはソケットなどのネットワークに用いられるI/OモデルでディスクI/Oには使わない。

これに関連していたのがO_NONBLOCKを使用していたacceptであった。
次のManPageを参照すると色々わかった。
Man page of ACCEPT

キューに保留となっている接続要求がなく、 かつソケットが非停止になっていないときは、 accept() は接続が発生するまで呼び出し元を停止 (block) する。 ソケットが非停止になっていて、 待ち状態の接続要求がキューに無いときは、 accept() はエラー EAGAIN か EWOULDBLOCK で失敗する。

これが最も自分を苦しめたあれである。
そもそも今回は、このキューを使っていなかった。
そのため、待ち状態の接続要求がキューになく(そもそもキューを用意していなく)、acceptがEAGAIN,EWOULDBLOCKを返していた・

おわりに

そんなこんなで、tcpクライアントからメッセージを受け取り表示するところまではできた。
コードは以下のレポジトリにある。
github.com
使ったtcpクライアントはpythonで書いたものを使用した。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

target = "localhost"
port = 8888

#using IPv4 and TCP/IP
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#connecting
client_socket.connect( (target, port) )

#data send
client_socket.send("hello,world")

#get data
response_data = client_socket.recv(1024) 

print response_data

ただ、データの送信が出来ないことに加えて、時折メッセージを受信出来なかったりするのでそのあたりのバグとリファクタリングは今後も行っていく必要がある

最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part3

はじめに

前回まで
rabbitfoot141.hatenablog.com
rabbitfoot141.hatenablog.com

この二回でカーネルモジュールで標準出力や、カーネルスレッドを動かすところまでやった。
今回はこれをTCPサーバー化するのを目標にやっていく。

ネットワーク周り

さくっと実装したいが、カーネルモジュールでのネットワーク周りがわからないので
そこあたりから色々調べていく。

ソケット周り

これが使えないとネットワーク通信ができないのでこれがどういう仕様で実装されているか調べてみる。


見つかった。

http://elixir.free-electrons.com/linux/v3.4/source/include/linux/net.h#L138

これは

#include <linux/net.h>

というヘッダーにあることがわかった。

これの138行目にsocketという名前の構造体があり。
こいつがBSDのソケットとして使えるようなので使うことにする。

/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
struct socket {
	socket_state		state;

	kmemcheck_bitfield_begin(type);
	short			type;
	kmemcheck_bitfield_end(type);

	unsigned long		flags;

	struct socket_wq __rcu	*wq;

	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;
};

色々なコードや、文献を漁っているとこの中でも次に重要になるのが

const struct proto_ops	*ops;

という構造体。
この構造体の実装は次のようになっている。

struct proto_ops {
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags);
	int		(*getname)   (struct socket *sock,
				      struct sockaddr *addr,
				      int *sockaddr_len, int peer);
	unsigned int	(*poll)	     (struct file *file, struct socket *sock,
				      struct poll_table_struct *wait);
	int		(*ioctl)     (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
#ifdef CONFIG_COMPAT
	int	 	(*compat_ioctl) (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
#endif
	int		(*listen)    (struct socket *sock, int len);
	int		(*shutdown)  (struct socket *sock, int flags);
	int		(*setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, unsigned int optlen);
	int		(*getsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int __user *optlen);
#ifdef CONFIG_COMPAT
	int		(*compat_setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, unsigned int optlen);
	int		(*compat_getsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int __user *optlen);
#endif
	int		(*sendmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len);
	int		(*recvmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len,
				      int flags);
	int		(*mmap)	     (struct file *file, struct socket *sock,
				      struct vm_area_struct * vma);
	ssize_t		(*sendpage)  (struct socket *sock, struct page *page,
				      int offset, size_t size, int flags);
	ssize_t 	(*splice_read)(struct socket *sock,  loff_t *ppos,
				       struct pipe_inode_info *pipe, size_t len, unsigned int flags);
	void		(*set_peek_off)(struct sock *sk, int val);
};

bind,listen,acceptなどなど、どこかで見たことのあるような単語が並んでいる。
実際にtcpサーバを構築するときは、これらの関数に必要な情報を渡して呼び出す必要がある。


なんやかんや、C言語でネットワークプログラミングをする時と同じ様なあれこれはあるので
それと似たような感じでコードを書けそう。


この後で実装するコードは、カーネルスレッドを多用するので
kernel_lock関数を使った方がいいとかはまた別途考える必要がある。

文字列周り

文字列周りの扱いはどうしようかと調べていたら、linux/socket.hにちょうどいい構造体があった。

/*
 *	As we do 4.4BSD message passing we use a 4.4BSD message passing
 *	system, not 4.3. Thus msg_accrights(len) are now missing. They
 *	belong in an obscure libc emulation or the bin.
 */
 
struct msghdr {
	void	*	msg_name;	/* Socket name			*/
	int		msg_namelen;	/* Length of name		*/
	struct iovec *	msg_iov;	/* Data blocks			*/
	__kernel_size_t	msg_iovlen;	/* Number of blocks		*/
	void 	*	msg_control;	/* Per protocol magic (eg BSD file descriptor passing) */
	__kernel_size_t	msg_controllen;	/* Length of cmsg list */
	unsigned	msg_flags;
};

kernelでネットワークのレスポンスを扱うにはなにやらこれがいいらしい。

実装の前に

全体の流れを整理する。

  • 必要なsocket類を構造体などで宣言する。
  • listenをするスレッドを作る
  • 上記のスレッド内でsocketを作成、bind、listenなどを行う
  • それが完了したら更にacceptを行うスレッドを作る
  • 文字列が送信されてきたら、それを送り返す

実装する

実装してみた、結果はこちら。

github.com


ヘッダーファイルに直書きしているのは後で直す。

ただし、実行してみるとaccept errorと表示されている。

[  689.884212] accept_thread the csock is :474605440,474606080
[  689.884216] accept start
[  689.884220] accept error

なるはやで解決しないといけないものは
accept errorと表示されるので、上記opsを見なおしてなぜそこでコケたのか調べる。
あとrmmodを実行した際に

rmmod: ERROR: ../libkmod/libkmod-module.c:793 kmod_module_remove_module() could not remove 'fastecho': Device or resource busy
rmmod: ERROR: could not remove module fastecho.ko: Device or resource busy

といったエラーが出てinsmodするために再起動しないといけない。
それはだるいのでどうにかしたい。

以上、学生アドベントカレンダー25日目の記事でした。

全てのプログラマに捧げるScala入門

はじめに

Qiitaの方に次のようないくつかのScala入門記事を掲載していた。

java経験者のScala入門メモ [基礎知識] - Qiita
java経験者のScala入門メモ [関数、クラス] - Qiita
java経験者のScala入門メモ [ケースクラス、オブジェクト] - Qiita
java経験者のScala入門メモ [トレイト、メソッド] - Qiita

しかし、これらは自身がScalaを学習し始めた直後に書いたもので
内容の正確性と充実性がいまいちなのでこれを書き直すことにした。

対象は、Javaを使っていてScalaの使用を考えている人。を想定しているが
Java以外の言語を使っている人や単に興味がある人にも楽しんで貰えればと思っている。

対応するバージョンは2.11.xと2.12.xとなる。

ただし、この記事群はあくまで入門編なので
理解が難しい点や現状で解説しても混乱を招くことが想定されるものは除外しているので
その点はご理解のほどをお願いしたい。

Scala入門

Scalaがある程度使えるぐらいまででいいという方はコレクションまで
もう少し使ってみたいという方は最後まで読んで頂けるとある程度の入門にはなるだろう。

以下の記事は本アドベントカレンダー用の記事として作成した。

rabbitfoot141.hatenablog.com

  • クラスとオブジェクト、ケースクラス

rabbitfoot141.hatenablog.com

  • トレイト

rabbitfoot141.hatenablog.com

rabbitfoot141.hatenablog.com

  • コレクション

rabbitfoot141.hatenablog.com

  • パターンマッチ

rabbitfoot141.hatenablog.com

  • 暗黙の型変換
  • 型パラメータ
  • 例外

これら3つに関してはまた別の機会に書くことにする。

参考文献

  • Scala スケーラブルプログラミング 第3版

books.google.co.jp

Introduction · Scala研修テキスト

  • Scala 公式ドキュメント

A Scala Tutorial for Java Programmers | Scala Documentation

Scalaメモ(Hishidama's Scala Memo)

  • Scalaでリスト処理

Scalaでリスト処理

  • 第18章 Scalaのパターンマッチ

第18章:Scalaのパターンマッチ - Qiita



書いた人

twitter.com

普段はScalaを使って何かしら作っている。
Scala以外の言語ではJava,PHP,C,Pythonあたりを使っている。
カーネルモジュールからWebまで興味があれば大体なんでもする。

さいごに

この記事は Muroran Institute of Technology Advent Calendar 2017 24日目の記事となる。
今回は特に書くことがなかったのでScalaの入門記事を更新することにした。
いくらアドベントカレンダーといえど7個も記事書くのしんどかった。

全てのプログラマに捧げるScala入門 パターンマッチ

はじめに

前回はコレクションについてさらっと触れたが今回は、パターンマッチについて触れる。
しかし、例によってパターンマッチは用途がかなり多いのでここでは基礎的なことにのみ限定して紹介する。

パターンマッチ

match文というのがScalaには存在する。
例えば、次のコードを例に考えてみよう。

scala> def judge(n: Int): String = n match {
     |     case 1 => "one"
     |     case 2 => "two"
     |     case _ => "error"
     | }
judge: (n: Int)String

scala> (1 to 5).toList.foreach( num => println(judge(num)) )
one
two
error
error
error

match文は直前に渡される変数に束縛され実行する。実際に今回はnという変数に束縛されている。
今回は定数パターンでのみ分類したが最初の2つがnが1 or 2のとき最後のものがそれ以外の時を指す。

この程度であれば、if式などを使った方が断然早く楽に感じる。
しかし、このパターンマッチは単に値を格納した変数だけでなく型やその他もろもろのものに対して適用できるのでif式などよりも汎用性が高い。

これ以降はほかにどんな状況が想定できるか解説していく。

型パターン

型についてもパターンマッチを適用できる。

scala> val list = List(1, "hello", true, 1.0)
list: List[Any] = List(1, hello, true, 1.0)

scala> list.foreach(value => value match {
     |     case number: Int => println(number)
     |     case str: String => println(str)
     |     case bool: Boolean => println(bool)
     |     case _ => println("error")
     | })
1
hello
true
error

このように、case後の変数名後にデータ型を書くことができて
それについて処理を分けることもできる。

ただし、最後のcaseで指定しているワイルドカードパターンがなければ
リストの末尾にある、Double型の変数がマッチしないためにエラーが発生する。
つまりパターンマッチは、なにかしらのパターンにマッチさせなければいけないということになる。

変数パターン

これは先程のものと同じかと思うが少し違う。

scala> val num = 2
num: Int = 2

scala> val res = num match {
     | case num => num * 2
     | }
res: Int = 4

これは計算できるかぎりの全てのパターンにマッチする。
そしてマッチしたものは変数に束縛させて処理を行っている。

シーケンスパターン

お次はシーケンス型のパターンにマッチするものである。

scala> val list = List(1,2,3,4)
list: List[Int] = List(1, 2, 3, 4)

scala> val res = list match {
     | case List(1, a, _*) => a
     | case _ => -1
     | }
res: Int = 2

先頭要素が1で要素が2個以上のシーケンス型のコレクションにマッチするものに成る。

タプルパターン

これは個人的に、ネットで情報を探していて一番おもしろかったので紹介する。
有名なFizzBuzzを解くプログラムを例にする。これはタプルを上手に使った面白い解法だった。

object Sample{
  def main(args: Array[String]):Unit = {

    (1 to 30).foreach( num => println(judge(num)) )

  }

  def judge(n: Int): String = (n%3, n%5) match {
    case (0, 0) => "FizzBuzz"
    case (0, _) => "Fizz"
    case (_, 0) => "Buzz"
    case _ => n.toString
  }
}

割ったあまりをタプルとして格納して、それを元にマッチさせている非常にスマートな解法だった。

パターンガード

これは定数パターンに通じるものだが、定数や変数を扱う際にそのマッチに更に条件をつけることができるものである。

scala> val n: Int = 5
n: Int = 5

scala> val res = n match {
     |     case n if n%2 == 0 => true
     |     case n if n%2 != 0 => false
     | }
res: Boolean = false

おわりに

この他にもまだコンストラクタパターンなどがあるが実際にライブラリ等を使用した際によく使うものであるので
興味があれば他のパターンマッチについても学習して欲しい