SQL から参照しているテーブルを抽出 - FoundationDB SQL Parser

FoundationDB SQL Parser を使って SQL から参照しているテーブル (from 句で使われているテーブル) を抽出する方法をご紹介します。

これを応用すれば SQL から CRUD 図を生成するような処理も比較的容易に実装できると思います。

今回使用したソースは http://github.com/fits/try_samples/tree/master/blog/20140920/

(1) SQL のパース

まずは、SQL をパースしてその結果を出力してみます。

パースは SQLParser オブジェクトの parseStatement メソッドSQL 文字列を渡すだけです。

また、parseStatement メソッドの戻り値である CursorNode オブジェクトの treePrint メソッドを呼び出せばパース結果を整形して出力してくれます。

sql_parse.groovy
@Grab('com.foundationdb:fdb-sql-parser:1.4.0')
import com.foundationdb.sql.parser.SQLParser

def parser = new SQLParser()
// SQL のパース
def node = parser.parseStatement(new File(args[0]).text)
// パース結果の出力
node.treePrint()

実行結果は下記の通りです。

実行結果
> groovy sql_parse.groovy sample1.sql

com.foundationdb.sql.parser.CursorNode@46271dd6
name: null
updateMode: UNSPECIFIED
statementType: SELECT
resultSet:  
    com.foundationdb.sql.parser.SelectNode@11bb571c
    isDistinct: false
    resultColumns:
        com.foundationdb.sql.parser.ResultColumnList@7c51f34b
        ・・・
    fromList:
        com.foundationdb.sql.parser.FromList@2a225dd7
        ・・・
        leftResultSet:
            com.foundationdb.sql.parser.FromBaseTable@125290e5
            tableName: orders
            ・・・
            correlation Name: od
            null
        rightResultSet:
            com.foundationdb.sql.parser.FromBaseTable@6fa34d52
            tableName: order_items
            ・・・
            correlation Name: oi
            null
        joinClause:
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@57576994
            operator: =
            ・・・
        [1]:
        com.foundationdb.sql.parser.FromBaseTable@1205bd62
        tableName: users
        ・・・
        correlation Name: us
        us
    whereClause: 
        com.foundationdb.sql.parser.AndNode@7ef27d7f
        operator: and
        methodName: and
        type: null
        leftOperand: 
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@490caf5f
            operator: >
            ・・・
        rightOperand: 
            com.foundationdb.sql.parser.BinaryRelationalOperatorNode@31920ade
            operator: =
            ・・・

上記で使用した SQL は下記の通り。

sample1.sql
select
    *
from
    orders od
    join order_items oi on
        od.order_id = oi.order_id
    ,
    users us
where
    od.ordered_date > ? and
    od.user_id = us.user_id

(2) テーブルの抽出

それでは、SQL 内で参照しているテーブルを抽出してみます。

テーブルの抽出には com.foundationdb.sql.parser.Visitor インターフェースを使うのが簡単です。 (いわゆる Visitor パターン)

具体的には、下記処理を実装した Visitor インターフェースの実装オブジェクトを用意し、パース結果 (CursorNode) の accept メソッドへ渡します。

  • (a) visit メソッドの引数が FromBaseTable オブジェクトだった場合にテーブル名を取得して保持する
  • (b) visit メソッドの戻り値は引数をそのまま返す

skipChildren メソッド等の Visitor インターフェースのその他メソッドは、全ノードを処理するように false を返すようにしておきます。

テーブル名 (スキーマ名付き) を文字列で取得するには FromBaseTable オブジェクトの getOrigTableName メソッドで取得した TableName オブジェクトの getFullTableName メソッドを使います。

ここで、テーブル名取得系のメソッドにはいくつかバリエーションがありますが、主なものは下記の通りです。

クラス メソッド 内容 戻り値の型
FromBaseTable getOrigTableName テーブル名 TableName
FromBaseTable getTableName テーブルの別名 (別名が無ければテーブル名) TableName
FromBaseTable getCorrelationName テーブルの別名 (別名が無ければ null) String
TableName getFullTableName スキーマ名付きテーブル名 String
TableName getTableName スキーマ名なしテーブル名 String

また、下記サンプルでは、テーブル名の重複を排除しテーブル名でソートするように TreeSet へテーブル名を登録するようにしています。

listup_tables.groovy
@Grab('com.foundationdb:fdb-sql-parser:1.4.0')
import com.foundationdb.sql.parser.FromBaseTable
import com.foundationdb.sql.parser.SQLParser
import com.foundationdb.sql.parser.Visitor

def tables = [] as TreeSet

// FromBaseTable (参照しているテーブル)を処理するための Visitor 実装
def tableSelector = [
    visit: { node -> 
        if (node instanceof FromBaseTable) {
            // テーブル名(スキーマ名付き)を取得して tables へ追加
            tables << node.origTableName.fullTableName
        }
        node
    },
    skipChildren: { node -> false },
    stopTraversal: { -> false },
    visitChildrenFirst: { node -> false}
] as Visitor

def parser = new SQLParser()

def node = parser.parseStatement(new File(args[0]).text)
// テーブル名の抽出
node.accept(tableSelector)
// テーブル名の出力
tables.each {
    println it
}

「(1) SQL のパース」 で使用した sample1.sql からテーブル名を抽出すると下記のような結果になりました。

実行結果1
> groovy listup_tables.groovy sample1.sql

order_items
orders
users

更に、下記のような SQL からテーブル名を抽出してみます。

sample2.sql
select
    us.user_id,
    us.name,
    (select count(*) from REFUNDS rf where rf.user_id = us.user_id) as refund_count1,
    (select count(*) from SCH1.REFUNDS rf where rf.user_id = us.user_id) as refund_count2
from
    Users us
where
    us.user_id in (
        select
            ou.user_id
        from (
            select
                od.user_id,
                count(*) ct
            from
                orders od
                join order_items oi on
                    od.item_id = oi.item_id
            where
                oi.item_id in (
                    select item_id from special_items
                ) and
                od.ordered_date > ?
            group by
                od.user_id
        ) ou
        where
            ou.ct >= 5
    )

結果は下記の通り、テーブル名は全て小文字になりましたが正常に抽出されています。

getFullTableName でテーブル名を取得しているので、スキーマ名を付けたテーブルと付けなかったテーブルは別途出力されています。

実行結果2
> groovy listup_tables.groovy sample2.sql

order_items
orders
refunds
sch1.refunds
special_items
users

Apache FtpServer で FTPS サーバーを組み込み実行

Apache FtpServer を使って Groovy で FTPS (FTP over SSL/TLS) サーバーの組み込み実行を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140916/

FTP サーバーの組み込み実行

まずは、普通の FTP サーバーを組み込み実行してみます。

簡単にするため、ユーザー定義をプロパティファイルから読み出すようにし、平文のパスワードを設定するようにします。

プロパティファイルは ftpserver.user.<ユーザー名>.<プロパティ>=<値> のフォーマットで定義します。

一応、下記のような設定を定義すれば動作するようです。 (書き込み不可でよければ writepermission の設定は不要)

user.properties (プロパティファイル)
ftpserver.user.u1.userpassword=p1
ftpserver.user.u1.homedirectory=./data
ftpserver.user.u1.writepermission=true

プロパティファイルからユーザー定義を読み込むには PropertiesUserManagerFactory を使います。

PropertiesUserManagerFactory の passwordEncryptor プロパティへ ClearTextPasswordEncryptor を設定する事で平文のパスワードが使えます。

ftp_server.groovy (FTP サーバー組み込み実行スクリプト
@Grab('org.apache.ftpserver:ftpserver-core:1.0.6')
@Grab('org.slf4j:slf4j-api:1.7.7')
@Grab('org.slf4j:slf4j-simple:1.7.7')
import org.apache.ftpserver.FtpServerFactory
import org.apache.ftpserver.usermanager.ClearTextPasswordEncryptor
import org.apache.ftpserver.usermanager.PropertiesUserManagerFactory

def factory = new FtpServerFactory()

// user.properties プロパティファイルを使用
factory.userManager = new PropertiesUserManagerFactory(
    file: new File('user.properties'),
    passwordEncryptor: new ClearTextPasswordEncryptor()
).createUserManager()

// サーバー起動
factory.createServer().start()

実行すると FTP サーバーが起動します。

実行例
> groovy ftp_server.groovy

[main] INFO org.apache.ftpserver.impl.DefaultFtpServer - FTP server started

cURL を使って動作確認してみます。

FTP 接続
$ curl -u u1:p1 ftp://localhost/

dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b

特に問題なく FTP サーバーへ接続できました。

FTPS サーバーの組み込み実行

次に、本題の FTPS (FTP over SSL/TLS) サーバーを組み込み実行してみます。

FTPS 実行にはキーストアが必要となりますので、JDK 付属の keytool コマンドを使って作成しておきます。

keytool -genkey -keypass <パスワード> -storepass <パスワード> -keystore <ファイル名> で最低限のキーストアファイルを作成できます。

「姓名」とか聞かれますが、テスト用に使うだけなら全て Unknown のままで構いません。

キーストアの作成例
> keytool -genkey -keypass sample -storepass -keystore sample.jks

姓名は何ですか。
  [Unknown]:
組織単位名は何ですか。
  [Unknown]:
組織名は何ですか。
  [Unknown]:
都市名または地域名は何ですか。
  [Unknown]:
都道府県名または州名は何ですか。
  [Unknown]:
この単位に該当する2文字の国コードは何ですか。
  [Unknown]:
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknownでよろしいで
すか。
  [いいえ]:  はい

上記でカレントディレクトリへ sample.jks ファイルが作成されます。

FTPS を使えるようにするには SslConfiguration (上記で作成した sample.jks を keystoreFile へ設定) を設定した ListenerFtpServerFactoryaddListener します。

ftps_server.groovy (FTPS サーバー組み込み実行スクリプト
・・・
import org.apache.ftpserver.listener.ListenerFactory
import org.apache.ftpserver.ssl.SslConfigurationFactory
・・・

def factory = new FtpServerFactory()

def ssl = new SslConfigurationFactory(
    keystoreFile: new File('sample.jks'),
    keystorePassword: 'sample'
)

def listenerFactory = new ListenerFactory(
    sslConfiguration: ssl.createSslConfiguration()
)

factory.addListener('default', listenerFactory.createListener())

factory.userManager = new PropertiesUserManagerFactory(
    ・・・
).createUserManager()

factory.createServer().start()

実行すると FTP・FTPS のどちらでも接続できるサーバーが起動します。

実行例
> groovy ftps_server.groovy

[main] INFO org.apache.ftpserver.impl.DefaultFtpServer - FTP server started

まずは FTP で動作確認してみます。

FTP で接続
$ curl -u u1:p1 ftp://localhost/

dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b

特に問題なく FTP で接続できました。

次に --ssl -k オプションを追加指定して FTPS 接続してみます。 (正式な証明書を使っていれば -k オプションは不要)

今回は -v オプションも指定して詳細出力してみました。

FTPS で接続
$ curl --ssl -k -u u1:p1 -v ftp://localhost/

・・・
> AUTH SSL
・・・
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server key exchange (12):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / DHE-DSS-AES128-GCM-SHA256
* Server certificate:
*        subject: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=Unknown
*        start date: 2014-09-15 11:46:38 GMT
*        expire date: 2014-12-14 11:46:38 GMT
*        issuer: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=Unknown
*        SSL certificate verify result: self signed certificate (18), continuing anyway.
・・・
dr-x------   3 user group            0 Sep 15 20:18 a
dr-x------   3 user group            0 Sep 15 20:18 b
・・・

SSL 接続を行っている事が確認できました。

Vagrant で VirtualBox 上の CentOS 7 へ固定 IP を設定

はじめに

Vagrant を使って VirtualBox 上で CentOS 7 を起動する際に、固定 IP を設定しようとするとエラーが発生しました。

Vagrantfile
・・・
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.box = "centos7"
    # ゲスト OS 間の通信用に固定 IP アドレスを設定
    config.vm.network "private_network", ip: "192.168.100.11", virtualbox__intnet: "intnet"
end

CentOS 7 の Box を centos7 という名称で Vagrant へ登録済みです。

なお、virtualbox__intnet の設定が無いとホスト OS と通信するための IP アドレス設定 (ホストオンリーアダプター) になるようです。 (この場合、その IP でゲスト OS 間通信はできない模様)

vagrant up 時のエラー内容
> vagrant up

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos7'...
・・・
==> default: Configuring and enabling network interfaces...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

ARPCHECK=no /sbin/ifup eth1 2> /dev/null

Stdout from the command:

ERROR    : [/etc/sysconfig/network-scripts/ifup-eth] Device eth1 does not seem to be present, delaying initialization.


Stderr from the command:

CentOS 7 自体の起動は成功しており (SSH でログイン可能)、固定 IP アドレスの設定に失敗しています。

エラーの原因

原因は下記の通りです。

  • CentOS 7 では NIC の名称がデフォルトで eth<番号> (例 eth0) では無くなっている
  • VagrantRedHat 系 OS 設定用のプラグインでは今のところ eth<番号> 以外の NIC 名を処理できない

要するに、VirtualBox 上で CentOS 7 を実行すると NIC 名が下記のようになりますが、VagrantRedHat 系 OS のネットワーク設定を担うスクリプトplugins/guests/redhat/cap/configure_networks.rb) では、今のところ下記 NIC 名に対応していません。

  • enp0s3 (アダプター1)
  • enp0s8 (アダプター2)

実は、Fedora のネットワーク設定を担うスクリプトplugins/guests/fedora/cap/configure_networks.rb) では biosdevname -d コマンドを使って NIC 名を取得しており、上記 NIC 名に対応できているようです。

回避方法

本件のエラーを回避するには下記のような方法が考えられます。 (プラグインを用いる等、他の方法も色々あると思います)

  • (a) CentOS の設定を変更して古い NIC 名 (eth<番号>) を使うようにする
  • (b) RedHat 用の configure_networks.rb へ Fedora 用の configure_networks.rb の処理内容を適用する
  • (c) プロビジョニングで固定 IP を設定する

個人的に、本件は Vagrant 側で対応すべき問題だと思うので、今回は (b) と (c) の対応方法をご紹介します。

(b) RedHat 用の configure_networks.rb へ Fedora 用の configure_networks.rb の処理内容を適用する

Fedora の configure_networks.rb の実装内容を RedHat の configure_networks.rb へ適用する方法です。

configure_networks.rb の内容を書き換えてしまうので、あまり望ましい方法では無いかもしれませんが、対応手順は下記のようになります。

  • (1) plugins/guests/redhat/cap/configure_networks.rb のバックアップをとる
  • (2) Fedora 用の configure_networks.rb (plugins/guests/fedora/cap/configure_networks.rb) を RedHat 用の configure_networks.rb (plugins/guests/redhat/cap/configure_networks.rb) へ上書きコピー
  • (3) (2) で更新した RedHat 用の configure_networks.rb ファイルを編集し、モジュール名を GuestFedora から GuestRedHat へ変更
plugins/guests/redhat/cap/configure_networks.rb の内容
・・・
module VagrantPlugins
  module GuestRedHat
  ・・・

なお、Windows 用の Vagrant の場合は embedded\gems\gems\vagrant-1.6.3 に plugins ディレクトリがあります。

Fedora 用の configure_networks.rb を適用して問題ないかを十分検証したわけではありませんが、一応 vagrant up でエラーが発生せず固定 IP アドレスを設定できました。

実行例
> vagrant up

・・・
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
・・・

enp0s8 へ IP アドレスが設定されている事を確認。

> vagrant ssh
Last login: Mon Aug 25 06:50:04 2014 from 10.0.2.2
[vagrant@localhost ~]$ ip addr
・・・
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP
qlen 1000
    ・・・
    inet 192.168.100.11/24 brd 192.168.100.255 scope global enp0s8
    ・・・

(c) プロビジョニングで固定 IP を設定する方法

Vagrant のプロビジョニング機能を使い ip addr add コマンド等で IP アドレスを設定するだけなので、(b) に比べると安全な方法だと思います。

まずは、Vagrantfile へ下記のように provision の定義を追加し、:inline に対して固定 IP アドレスを enp0s8 へ設定し有効化するコマンドを記載します。

Vagrantfile
・・・
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.box = "centos7"

    config.vm.network "private_network", ip: "192.168.100.11", virtualbox__intnet: "intnet"
    # プロビジョニングによる固定 IP の設定
    config.vm.provision :shell, :inline => "sudo ip addr add 192.168.100.11/24 dev enp0s8; sudo ip link set enp0s8 up"
end

vagrant up のネットワーク設定でエラーが発生し途中終了するので、その後に vagrant provision を実行すると固定 IP アドレスを設定します。

実行例1
> vagrant up

・・・
==> default: Configuring and enabling network interfaces...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

ARPCHECK=no /sbin/ifup eth1 2> /dev/null

Stdout from the command:

ERROR    : [/etc/sysconfig/network-scripts/ifup-eth] Device eth1 does not seem to be present, delaying initialization.
・・・

vagrant provision でプロビジョニング機能を実行します。

> vagrant provision
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: RTNETLINK answers: File exists

enp0s8 へ IP アドレスが設定されている事を確認。

> vagrant ssh
・・・
[vagrant@localhost ~]$ ip addr
・・・
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    ・・・
    inet 192.168.100.11/24 brd 192.168.100.255 scope global enp0s8
    ・・・

vagrant up でエラーを発生させないようにする

vagrant up の後に vagrant provision を別途実行するのが面倒な場合。

plugins/guests/redhat/cap/configure_networks.rb を下記のように編集し、ifup が失敗してもエラーを発生させないようにすれば vagrant up 時にプロビジョニングも適用できるようになります。 (ただし、マウントエラーのような他のエラーを発生させない事が前提)

plugins/guests/redhat/cap/configure_networks.rb 編集例 - ifup の実行処理へ error_check: false を追加 (61 行目)
machine.communicate.sudo("ARPCHECK=no /sbin/ifup eth#{interface} 2> /dev/null", error_check: false)
実行例2 - configure_networks.rb を編集した場合
> vagrant up --provision

Bringing machine 'default' up with 'virtualbox' provider...
・・・
==> default: Configuring and enabling network interfaces...
・・・
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: RTNETLINK answers: File exists

なお、vagrant up でプロビジョニングが適用されるのは初回起動時のみのようですので、2回目以降は vagrant up --provision でプロビジョニングを強制適用して起動する必要があります。

成功するまで次を試すような処理へ Either モナドを適用 - FunctionalJava

成功するまで次の処理を試していくような処理に対して Either モナドを適用してみました。

使用した環境は下記の通りです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140825/

はじめに

Either モナドは 2つの異なる値 (Left と Right) を扱う場合に使用し、一般的には失敗(エラー)を伴う処理に対して下記のような使い方をします。

Leftの値 Rightの値
失敗時のエラー内容(例外) 成功時の値

ただし今回は、下記のように処理が成功するまで元の値が Left へ保持されるような使い方をしてみました。

Leftの値 Rightの値
元の値 成功時の値

具体的には、下記のような処理を試します。

  • 実行時引数で指定した日付文字列に対して、Date オブジェクトへのパースが成功するまで次のパース処理を試していく

Either モナドを使わなかった場合

まずは Either モナドを使わず、普通に Java で実装してみました。

下記 (1) ~ (5) のパース処理を順に試して、例外が発生せず null では無い値を返した時点でパース処理を終了します。

  • (1) ISO-8601 タイムゾーン無しの日付文字列(例 2014-08-25T13:20:00)をパース
  • (2) ISO-8601 の日付文字列(例 2014-08-25T13:20:00+09:00)をパース
  • (3) ISO-8601 タイムゾーン付きの日付文字列(例 2014-08-25T13:20:00+09:00[Asia/Tokyo])をパース
  • (4) "yyyy-MM-dd HH:mm:ss" をパース
  • (5) "now" の場合に Date オブジェクトを返す

java.time のクラスを使う必要も無かったのですが、ついでに試してみました。

DateParse.java
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.function.Function;

public class DateParse {
    public static void main(String... args) {
        // SimpleDateFormat を使ったパース
        Function<String, Function<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Date res = parseDate(
            args[0],
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), //(1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.apply("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }

    // 日付文字列のパース
    @SafeVarargs
    public static Date parseDate(String date, Function<String, Date>... funcList) {
        for (Function<String, Date> func : funcList) {
            try {
                Date res = func.apply(date);

                if (res != null) {
                    return res;
                }
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
            }
        }
        return null;
    }
}
実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java DateParse "2014-08-25T13:20:00"
------
Mon Aug 25 22:20:00 JST 2014
実行結果2 - (2) で成功
> java DateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Mon Aug 25 13:20:00 JST 2014
実行結果3 - (3) で成功
> java DateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Mon Aug 25 13:20:00 JST 2014
実行結果4 - (4) で成功
> java DateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Mon Aug 25 13:20:00 JST 2014
実行結果5 - (5) で成功
> java DateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Mon Aug 25 11:10:42 JST 2014
実行結果6 - 全失敗
> java DateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
null

Either モナドを使った場合

同様の処理を FunctionalJava の Either を使って実装します。

Left にパース前の値(String)、Right にパース後の値(Date)を格納できるように Either の型を Either<String, Date> とします。

今回は Left の値 (String) に対して順次パース処理を試すようにしたいので、Either.left() で取得した Either.LeftProjection オブジェクトへパース処理を bind しています。

bind の引数には 「普通の値を取って Either を返す」 処理 (下記では F<String, Either<String, Date>>) を与える必要があるので、下記では eitherK というメソッドを定義し、通常のパース処理 (文字列を取って Date を返す F<String, Date>) を変換してから bind するようにしています。

なお、java.util.function.Function は使わず、全面的に FunctionalJava の fj.F を使うようにしました。

EitherDateParse.java
import fj.F;
import fj.data.Either;

import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.util.Date;

public class EitherDateParse {
    public static void main(String... args) {
        F<String, F<String, Date>> simpleDate = df -> s -> {
            try {
                return new SimpleDateFormat(df).parse(s);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        Either<String, Date> res = parseDate(
            Either.left(args[0]),
            s -> Date.from(LocalDateTime.parse(s).toInstant(ZoneOffset.UTC)), // (1)
            s -> Date.from(OffsetDateTime.parse(s).toInstant()), // (2)
            s -> Date.from(ZonedDateTime.parse(s).toInstant()), // (3)
            simpleDate.f("yyyy-MM-dd HH:mm:ss"), // (4)
            s -> "now".equals(s)? new Date(): null // (5)
        );

        System.out.println("------");

        System.out.println(res);
    }


    @SafeVarargs
    public static Either<String, Date> parseDate(Either<String, Date> date, F<String, Date>... funcList) {
        for (F<String, Date> func : funcList) {
            date = date.left().bind( eitherK(func) );
        }
        return date;
    }

    // F<S, T> の処理が例外発生か null であれば Left、そうでなければ Right を返す処理へ変換
    private static <S, T> F<S, Either<S, T>> eitherK(final F<S, T> func) {
        return s -> {
            try {
                T res = func.f(s);
                return (res == null)? Either.left(s): Either.right(res);
            } catch (Exception ex) {
                System.out.println("* " + ex.getMessage());
                return Either.left(s);
            }
        };
    }
}

実行結果は、Left や Right に包まれている部分を除けば Either を使わなかった場合と基本的に同じです。 (全失敗の場合は異なります)

実行結果1 - (1) で成功(オフセット指定無しのため JST では +9時間される)
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00"
------
Right(Mon Aug 25 22:20:00 JST 2014)
実行結果2 - (2) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00"
* Text '2014-08-25T13:20:00+09:00' could not be parsed, unparsed text found at index 19
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果3 - (3) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25T13:20:00+09:00[Asia/Tokyo]"
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 19
* Text '2014-08-25T13:20:00+09:00[Asia/Tokyo]' could not be parsed, unparsed text found at index 25
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果4 - (4) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(Mon Aug 25 13:20:00 JST 2014)
実行結果5 - (5) で成功
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "now"
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* Text 'now' could not be parsed at index 0
* java.text.ParseException: Unparseable date: "now"
------
Right(Mon Aug 25 11:59:01 JST 2014)
実行結果6 - 全失敗
> java -cp .;functionaljava-4.2-beta-1.jar EitherDateParse "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
------
Left(2014-08-25)

Either モナドを使った場合2 - 機能追加

次に下記のような機能を追加してみました。

  • (a) パース全失敗の場合は RuntimeException を throw する
  • (b) パース成功の場合は、その次の日の 0時 0分 0秒 の Date オブジェクトと共に 2要素の vectorV2) へ格納する

Either モナドを使えば if 文などでいちいち条件判定しなくても変換処理等を合成できるのが利点だと思います。

今回は、Either の結果出力に System.out.println() を直接使わず fj.Show.println() を使うようにしてみました。

EitherDateParse2.java
・・・
import fj.data.vector.V;
import fj.data.vector.V2;
import fj.Show;

import org.apache.commons.lang3.time.DateUtils;
・・・
public class EitherDateParse2 {
    public static void main(String... args) {
        ・・・
        Either<String, Date> res = parseDate(
            ・・・
        );

        // (a) パースが全失敗の場合は RuntimeException を throw
        res.left().bind( s -> {
            throw new RuntimeException("failed parse");
        });

        // (b) パース成功の場合は、次の日の 0時0分0秒 の結果と共に V2 へ格納して返す
        Either<String, V2<Date>> res2 = res.right().bind( d ->
            Either.right(
                V.v(d, DateUtils.truncate(DateUtils.addDays(d, 1), Calendar.DATE))
            )
        );

        System.out.println("------");

        Show.<String, V2<Date>>eitherShow(Show.anyShow(), Show.v2Show(Show.anyShow())).println(res2);
    }
    ・・・
}
実行結果 - 成功した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25 13:20:00"
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
* Text '2014-08-25 13:20:00' could not be parsed at index 10
------
Right(<Mon Aug 25 13:20:00 JST 2014,Tue Aug 26 00:00:00 JST 2014>)
実行結果 - 全失敗した場合
> java -cp .;functionaljava-4.2-beta-1.jar;commons-lang3-3.3.2.jar EitherDateParse2 "2014-08-25"
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* Text '2014-08-25' could not be parsed at index 10
* java.text.ParseException: Unparseable date: "2014-08-25"
Exception in thread "main" java.lang.RuntimeException: failed parse
        at EitherDateParse2.lambda$main$19(EitherDateParse2.java:37)
        ・・・

Java用 SSH クライアントライブラリ - ganymed-ssh2, sshj, JSch, Apache SSHD

主要な Java 用の SSH クライアントライブラリを使って簡単なサンプルを作成してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140814/

はじめに

Vagrant で実行中のゲスト OS に対して SSHls -al を実行し、その結果を出力するだけの単純な処理を Groovy で実装してみる事にします。

今回の実装のポイントとなる点は下記のようなところだと思います。

  • (1) Vagrant秘密鍵ファイル insecure_private_key で認証
  • (2) 未知のホスト鍵をチェックしないようにする (ssh における StrictHostKeyChecking=no と同等の処理)

Vagrant秘密鍵ファイル insecure_private_keyスクリプトファイルと同じディレクトリへコピーしておく事とします。

また、本件のサンプルコードではエラー処理や文字コード等を配慮していない点にご注意ください。

Ganymed SSH-2

元のオリジナルはかなり以前にメンテナンスが停止されていたり、Orion SSH や Trilead SSH のような同系統のライブラリが他にあったりと、どれを使えばよいのか分かり難い印象がありますが、とりあえず https://code.google.com/p/ganymed-ssh-2/ でメンテナンス継続されているものを使えば良さそうです。

ホスト鍵のチェックは connect メソッドで制御できるようです。(チェックしたければ ServerHostKeyVerifier を渡す)

ganymed_sample.groovy
@Grab('ch.ethz.ganymed:ganymed-ssh2:262')
import ch.ethz.ssh2.Connection

def con = new Connection('127.0.0.1', 2222)
con.connect()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
if (con.authenticateWithPublicKey('vagrant', new File('insecure_private_key'), null)) {
    def session = con.openSession()

    session.execCommand('ls -al')

    println session.stdout.text

    session.close()
}

con.close()
実行結果
> groovy ganymed_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

sshj - SSHv2 library for Java

authPublickeyconnect の後に実行する必要があります。

また、未知のホスト鍵をチェックしないようにするには、addHostKeyVerifierPromiscuousVerifier を設定します。 (デフォルトではチェックするようになっています)

sshj_sample.groovy
@Grab('net.schmizz:sshj:0.10.0')
@Grab('org.slf4j:slf4j-nop:1.7.7')
import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.common.IOUtils
import net.schmizz.sshj.transport.verification.PromiscuousVerifier

def client = new SSHClient()
// (2) 未知のホスト鍵をチェックしない
client.addHostKeyVerifier(new PromiscuousVerifier())

client.connect('127.0.0.1', 2222)
// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
client.authPublickey('vagrant', client.loadKeys('insecure_private_key'))

def session = client.startSession()

def cmd = session.exec('ls -al')

println IOUtils.readFully(cmd.inputStream)

session.close()
client.disconnect()
実行結果
> groovy sshj_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

JSch - Java Secure Channel

未知のホスト鍵をチェックしないようにするには setConfigStrictHostKeyCheckingno にします。

jsch_sample.groovy
@Grab('com.jcraft:jsch:0.1.51')
import com.jcraft.jsch.JSch

def jsch = new JSch()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
jsch.addIdentity('insecure_private_key')
// (2) 未知のホスト鍵をチェックしない
jsch.setConfig('StrictHostKeyChecking', 'no')

def session = jsch.getSession('vagrant', '127.0.0.1', 2222)

session.connect()

def ch = session.openChannel('exec')
ch.setCommand('ls -al')

ch.connect()

println ch.inputStream.text

ch.disconnect()
session.disconnect()
実行結果
> groovy jsch_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

Apache SSHD

Apache MINA をベースに SSH サーバーとクライアントの両方の API を備えたライブラリです。 他のライブラリと比べると多少面倒なように思います。

close メソッドに false を指定すると graceful にクローズします。

sshd_sample.groovy
@Grab('org.apache.sshd:sshd-core:0.12.0')
@Grab('org.apache.mina:mina-core:3.0.0-M2')
@Grab('org.bouncycastle:bcpkix-jdk15on:1.51')
@Grab('org.slf4j:slf4j-nop:1.7.7')
import org.apache.sshd.ClientChannel
import org.apache.sshd.SshClient
import org.apache.sshd.common.keyprovider.FileKeyPairProvider

def client = SshClient.setUpDefaultClient()
client.start()

def session = client.connect('vagrant', '127.0.0.1', 2222).await().getSession()

// (1) Vagrant の秘密鍵ファイル insecure_private_key で認証
def keyProvider = new FileKeyPairProvider(['insecure_private_key'] as String[])
session.addPublicKeyIdentity(keyProvider.loadKeys().iterator().next())

def auth = session.auth()
// verify の実行が必要
auth.verify()

if (auth.isSuccess()) {
    def ch = session.createExecChannel('ls -al')
    def baos = new ByteArrayOutputStream()

    ch.setOut(baos)

    ch.open()
    // コマンド完了まで待機
    ch.waitFor(ClientChannel.CLOSED, 0)
    ch.close(false)

    println baos.toString()
}

session.close(false)
client.stop()
実行結果
> groovy sshd_sample.groovy

total 24
drwx------. 3 vagrant vagrant 4096 Aug  1 09:05 .
drwxr-xr-x. 3 root    root      20 Aug  1 08:36 ..
-rw-------. 1 vagrant vagrant  202 Aug 10 05:28 .bash_history
-rw-r--r--. 1 vagrant vagrant   18 Jun 10 00:31 .bash_logout
-rw-r--r--. 1 vagrant vagrant  193 Jun 10 00:31 .bash_profile
-rw-r--r--. 1 vagrant vagrant  231 Jun 10 00:31 .bashrc
drwx------. 2 vagrant vagrant   28 Aug  1 09:05 .ssh
-rw-------. 1 vagrant vagrant  644 Aug  1 09:05 .viminfo

Arrow (Kleisli) で List モナド - Haskell, Frege, Scalaz

Scalaz でリストモナド - Kleisli による関数合成 」等で試してきた List モナドを使ったチェスのナイト移動の処理を Arrow (Kleisli) を使って実装し直してみました。

Arrow は計算のための汎用的なインターフェースで、モナドを扱うための Arrow として Kleisli があります。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140810/

Haskell の場合

Haskell の Arrow は >>><<< で合成できるようになっています。

Kleisli はモナドを扱うための Arrow なので、下記では List モナドを返す関数 moveKnight を Kleisli へ包んで合成しています。

Kleisli から包んだ関数を取り出すには runKleisli を使います。

3手版

まずは 3手版です。

以前の List モナド版との違いは in3canReachIn3 関数を Arrow で実装し直した点です。

Kleisli を使えば、モナド値が無くてもモナドを返す関数 (通常の値を取ってモナドを返す関数) を簡単に合成できるので in3 はポイントフリースタイルで定義しました。 (このため canReachIn3 関数の引数の順序が 以前のもの と異なっています)

また、通常の関数は Arrow のインスタンスなので、canReachIn3 関数の部分は単純に canReachIn3 end = runKleisli in3 >>> elem end とする事も可能です。

move_knight.hs
import Control.Arrow

type KnightPos = (Int, Int)

moveKnight :: KnightPos -> [KnightPos]
moveKnight (c, r) = filter onBoard
    [
        (c + 2, r - 1), (c + 2, r + 1),
        (c - 2, r - 1), (c - 2, r + 1),
        (c + 1, r - 2), (c + 1, r + 2),
        (c - 1, r - 2), (c - 1, r + 2)
    ]
    where onBoard (c', r') = c' `elem` [1..8] && r' `elem` [1..8]

-- 3手先の移動位置を列挙
in3 :: Kleisli [] KnightPos KnightPos
in3 = Kleisli moveKnight >>> Kleisli moveKnight >>> Kleisli moveKnight

-- 指定位置に3手で到達できるか否かを判定
canReachIn3 :: Arrow a => KnightPos -> a KnightPos Bool
canReachIn3 end = arr (runKleisli in3) >>> arr (elem end)
-- 以下でも可
-- canReachIn3 :: KnightPos -> KnightPos -> Bool
-- canReachIn3 end = runKleisli in3 >>> elem end

main = do
    print $ runKleisli in3 $ (6, 2)

    print $ canReachIn3 (6, 1) $ (6, 2)
    print $ canReachIn3 (7, 3) $ (6, 2)
実行結果
> runghc move_knight.hs

[(8,1),(8,3),・・・
・・・
,(3,4),(3,8)]
True
False

N手版

3手版と同様に inManycanReachInMany 関数を Arrow で実装し直してみました。

move_knight_many.hs
・・・
-- N手先の移動位置を列挙
inMany :: Int -> Kleisli [] KnightPos KnightPos
inMany x = foldr (>>>) returnA (replicate x (Kleisli moveKnight))

-- 指定位置にN手で到達できるか否かを判定
canReachInMany :: Arrow a => Int -> KnightPos -> a KnightPos Bool
canReachInMany x end = arr (runKleisli (inMany x)) >>> arr (elem end)
-- 以下でも可
-- canReachInMany :: Int -> KnightPos -> KnightPos -> Bool
-- canReachInMany x end = runKleisli (inMany x) >>> elem end

main = do
    print $ runKleisli (inMany 3) $ (6, 2)

    print $ canReachInMany 3 (6, 1) $ (6, 2)
    print $ canReachInMany 3 (7, 3) $ (6, 2)
実行結果
> runghc move_knight_many.hs

[(8,1),(8,3),・・・
・・・
,(3,4),(3,8)]
True
False

Frege の場合

Frege は Haskell とほとんど同じ実装になりますが、下記の点が異なります。

  • >>> の代わりに . で Arrow を合成
  • runKleisli の代わりに run を使用

なお、.>>> と合成の向きが異なります。

3手版

3手版です。

move_knight.fr
package sample.MoveKnight where

import frege.control.Arrow
import frege.control.arrow.Kleisli

type KnightPos = (Int, Int)

moveKnight :: KnightPos -> [KnightPos]
moveKnight (c, r) = filter onBoard
    [
        (c + 2, r - 1), (c + 2, r + 1),
        (c - 2, r - 1), (c - 2, r + 1),
        (c + 1, r - 2), (c + 1, r + 2),
        (c - 1, r - 2), (c - 1, r + 2)
    ]
    where onBoard (c', r') = c' `elem` [1..8] && r' `elem` [1..8]

-- 3手先の移動位置を列挙
in3 :: Kleisli [] KnightPos KnightPos
in3 = Kleisli moveKnight . Kleisli moveKnight . Kleisli moveKnight

-- 指定位置に3手で到達できるか否かを判定
canReachIn3 :: Arrow a => KnightPos -> a KnightPos Bool
canReachIn3 end = arr (elem end) . arr in3.run
-- 以下でも可
-- canReachIn3 :: KnightPos -> KnightPos -> Bool
-- canReachIn3 end = elem end . in3.run

main args = do
    println $ in3.run $ (6, 2)

    println $ canReachIn3 (6, 1) $ (6, 2)
    println $ canReachIn3 (7, 3) $ (6, 2)
実行結果
> java -jar frege3.21.586-g026e8d7.jar move_knight.fr
・・・
> java -cp .;frege3.21.586-g026e8d7.jar sample.MoveKnight

[(8, 1), (8, 3), ・・・
・・・
 (3, 4), (3, 8)]
true
false

N手版

N手版です。

move_knight_many.fr
package sample.MoveKnightMany where

・・・
-- N手先の移動位置を列挙
inMany :: Int -> Kleisli [] KnightPos KnightPos
inMany x = foldr (.) id (replicate x (Kleisli moveKnight))

-- 指定位置にN手で到達できるか否かを判定
canReachInMany :: Arrow a => Int -> KnightPos -> a KnightPos Bool
canReachInMany x end = arr (elem end) . arr (inMany x).run
-- 以下でも可
-- canReachInMany :: Int -> KnightPos -> KnightPos -> Bool
-- canReachInMany x end = elem end . (inMany x).run

main args = do
    println $ (inMany 3).run $ (6, 2)

    println $ canReachInMany 3 (6, 1) $ (6, 2)
    println $ canReachInMany 3 (7, 3) $ (6, 2)
実行結果
> java -jar frege3.21.586-g026e8d7.jar move_knight_many.fr
・・・
> java -cp .;frege3.21.586-g026e8d7.jar sample.MoveKnightMany

[(8, 1), (8, 3), ・・・
・・・
 (3, 4), (3, 8)]
true
false

Scalaz の場合

最後に Scalaz を使った Scala による実装です。
Haskell と同様に >>> で Arrow を合成できるようになっています。

3手版

3手版です。

MoveKnight.scala
package sample

import scalaz._
import Scalaz._

object MoveKnight extends App {

    type KnightPos = Tuple2[Int, Int]

    val inRange = (p: Int) => 1 to 8 contains p

    val moveKnight = (p: KnightPos) => List(
        (p._1 + 2, p._2 - 1), (p._1 + 2, p._2 + 1),
        (p._1 - 2, p._2 - 1), (p._1 - 2, p._2 + 1),
        (p._1 + 1, p._2 - 2), (p._1 + 1, p._2 + 2),
        (p._1 - 1, p._2 - 2), (p._1 - 1, p._2 + 2)
    ).filter { case (x, y) => inRange(x) && inRange(y) }

    // 3手先の移動位置を列挙
    val in3 = Kleisli(moveKnight) >>> Kleisli(moveKnight) >>> Kleisli(moveKnight)
    // 以下でも可
    // val in3 = Kleisli(moveKnight) >==> moveKnight >==> moveKnight

    // 指定位置に3手で到達できるか否かを判定
    val canReachIn3 = (end: KnightPos) => in3.run >>> { xs => xs.contains(end) }

    in3 (6, 2) |> println

    (6, 2) |> canReachIn3 (6, 1) |> println
    (6, 2) |> canReachIn3 (7, 3) |> println
}
実行結果
> gradle run

MoveKnight
:compileJava UP-TO-DATE
:compileScala UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
List((8,1), (8,3), ・・・
・・・
・・・, (3,4), (3,8))
true
false

N手版

N手版です。

MoveKnightMany.scala
package sample

import scalaz._
import Scalaz._

object MoveKnightMany extends App {
    ・・・
    // N手先の移動位置を列挙
    val inMany = (x: Int) => List.fill(x) { Kleisli(moveKnight) }.reduce { (a, b) => a >>> b }
    // 以下でも可
    // val inMany = (x: Int) => List.fill(x) { Kleisli(moveKnight) }.reduce { (a, b) => a >=> b }

    // 指定位置にN手で到達できるか否かを判定
    val canReachInMany = (x: Int) => (end: KnightPos) => inMany(x).run >>> { xs => xs.contains(end) }

    (6, 2) |> inMany(3) |> println

    (6, 2) |> canReachInMany(3)(6, 1) |> println
    (6, 2) |> canReachInMany(3)(7, 3) |> println
}
実行結果
> gradle run -Pmany

MoveKnightMany
:compileJava UP-TO-DATE
:compileScala UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
List((8,1), (8,3), ・・・
・・・
・・・, (3,4), (3,8))
true
false

なお、ビルドと実行には下記のような Gradle ビルド定義ファイルを使用しました。

build.gradle
apply plugin: 'application'
apply plugin: 'scala'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.scala-lang:scala-library:2.11.2'
    compile 'org.scalaz:scalaz-core_2.11:7.1.0'
}

if (!hasProperty('many')) {
    println 'MoveKnight'
    mainClassName = 'sample.MoveKnight'
}
else {
    println 'MoveKnightMany'
    mainClassName = 'sample.MoveKnightMany'
}

Roy で List モナド

前回に続き、今回も Roy を試してみます。

Roy ではモナドを使った do 記法を使えるようなので、JavaScript で List モナド - Monadic 等で試したチェスのナイト移動の List モナド処理を同じように実装してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140724/

モナドの定義

Roy では下記のように return と bind を実装してモナドを定義します。

let <モナド> {
    return: \x -> ・・・
    bind: \x f -> ・・・
}

これで Monadic と同じように do <モナド> ・・・ のような記法が使えます。

それでは、List モナドを定義して do を簡単に使ってみました。
Underscore.js に Haskell の concat と同等の処理が無さそうだったので concat を自前で定義しています。

sample.roy
let _ = require 'underscore'

let concat xs = _.reduce xs (\a b -> a.concat b) []
// リストモナド
let listMonad = {
    return: \x -> [x]
    bind: \xs f -> concat (_.map xs (\y -> f y))
}
// do 記法
let res = do listMonad
    x <- [1, 3, 5]
    y <- ['a', 'b']
    return (x * 2, y)

console.log res

do でタプル (要素1, 要素2, ・・・) を return していますが、下記のようにタプルは配列として扱われるようです。

実行結果
> roy -r sample.roy

[ [ 2, 'a' ],
  [ 2, 'b' ],
  [ 6, 'a' ],
  [ 6, 'b' ],
  [ 10, 'a' ],
  [ 10, 'b' ] ]

3手版

それでは本題に入ります。

まずは、List モナドを使って下記の処理を実装してみます。

  • 3手先のナイトの移動位置を全て列挙する処理
  • 3手後に指定の終了位置に到達するか否かを判定する処理

Haskell と同様に type で型シノニムを定義できるので、ナイトの位置情報を表す KnightPos という型シノニムを定義しました。

関数の引数では (<引数>: <型>) のように型を指定する事も可能です。

また、複数の引数は Haskell と同様に半角スペースで区切る事になります。

move_knight.roy (3手移動版)
let _ = require 'underscore'

let concat xs = _.reduce xs (\a b -> a.concat b) []

let listMonad = {
    return: \x -> [x]
    bind: \xs f -> concat (_.map xs (\y -> f y))
}
// 型シノニムの定義
type KnightPos = {c: Number, r: Number}

let inRange (n: Number) = _.contains (_.range 1 9) n

// ナイトの次の移動先を列挙
let moveKnight (p: KnightPos) = _.filter [
    {c: p.c + 2, r: p.r - 1}, {c: p.c + 2, r: p.r + 1},
    {c: p.c - 2, r: p.r - 1}, {c: p.c - 2, r: p.r + 1},
    {c: p.c + 1, r: p.r - 2}, {c: p.c + 1, r: p.r + 2},
    {c: p.c - 1, r: p.r - 2}, {c: p.c - 1, r: p.r + 2}
] ( \t -> (inRange t.c) && (inRange t.r) )

// 3手先の移動位置を列挙(重複あり)
let in3 (start: KnightPos) = do listMonad
    fst <- moveKnight start
    snd <- moveKnight fst
    moveKnight snd

console.log (in3 {c: 6, r: 2})

// 指定位置に3手で到達できるか否かを判定
let canReachIn3 (start: KnightPos) (end: KnightPos) = _.any (in3 start) (\p -> p.c == end.c && p.r == end.r)

console.log (canReachIn3 {c: 6, r: 2} {c: 6, r: 1})
console.log (canReachIn3 {c: 6, r: 2} {c: 7, r: 3})
実行結果
> roy -r move_knight.roy

[ { c: 8, r: 1 },
  { c: 8, r: 3 },
  { c: 4, r: 1 },
  { c: 4, r: 3 },
  ・・・
  { c: 3, r: 4 },
  { c: 3, r: 8 } ]
true
false

N手版

先程は 3手で固定していましたが、任意の手数を指定できるようにしてみます。

こちらは do を使わず、直接 listMonad.bind を使って n 回のナイト移動処理を連結するようにしました。

move_knight_many.roy (N手移動版)
・・・
// n手先の移動位置を列挙(重複あり)
let inMany (n: Number) (start: KnightPos) = _.reduce (_.range n) (\a b -> listMonad.bind a moveKnight) [start]

console.log (inMany 3 {c: 6, r: 2})

// 指定位置に n手で到達できるか否かを判定
let canReachInMany (n: Number) (start: KnightPos) (end: KnightPos) = _.any (inMany 3 start) (\p -> p.c == end.c && p.r == end.r)

console.log (canReachInMany 3 {c: 6, r: 2} {c: 6, r: 1})
console.log (canReachInMany 3 {c: 6, r: 2} {c: 7, r: 3})
実行結果
> roy -r move_knight_many.roy

[ { c: 8, r: 1 },
  { c: 8, r: 3 },
  { c: 4, r: 1 },
  { c: 4, r: 3 },
  ・・・
  { c: 3, r: 4 },
  { c: 3, r: 8 } ]
true
false