2019年10月31日木曜日

Kaleidox: LXSV

Kaleidoxはレコードを記述するRecordというデータ型を提供しています。

このRecordをテキストとして入出力するためのテキストフォーマットとしてLXSVを開発しました。LXSVを使うことでkaleidoxプログラム内でRecordをリテラルとして記述できます。

LXSVはLTSV(Labeled Tab-separeted Values)的なフォーマットを拡張したもので以下の特徴があります。

  • Kaleidoxプログラム内でリテラルとして記述可能
  • LTSVの概ね上位互換
  • 区切り記号を選択することができる

LXSVフォーマット

LXSVはUnicodeによるテキストフォーマットで区切り記号によってキーと値の対の列によるレコードと、改行コードで区切られたレコードの列を表現できます。

例えば以下のようなテキストがLXSVです。

name:Taro,city:Yokohama
name:Hanako,city:Kawasaki

このテキストではキーと値の対の区切り記号として「:」、キーと値の対の間の区切り記号として「,」を用いています。

キーと値の対の区切り記号は「:」固定ですが、キーと値の対の間の区切り記号は以下のルールによって決められます。

  • TAB、SPACE、「,」、「;」の中で最初に登場した文字を区切り記号とする。

このため以下のLXSVはすべて同じデータ構造を記述しています。

区切り記号: TAB

name:Taro city:Yokohama
name:Hanako city:Kawasaki

区切り記号: SPACE

name:Taro city:Yokohama
name:Hanako city:Kawasaki

区切り記号: ,

name:Taro,city:Yokohama
name:Hanako,city:Kawasaki

区切り記号: ;

name:Taro;city:Yokohama
name:Hanako;city:Kawasaki

区切り記号の入力

値の中にTAB, SPACE, 「,」, 「;」といった区切り記号を入れたい場合、以下の3つの方法があります。

  • 区切り記号の使い分け
  • 「"」で囲んで文字列として記述する。
区切り記号の使い分け

区切り記号を可変にしている理由の1つは区切り記号を値に入れたい場合の文字エスケープを回避することを可能にするためです。

name:Taro;city:Yokohama,Kawasaki
name:Hanako;city:Kawasaki,Yokohama

たとえば以下のように値の中に値の列を記述することも可能になります。ここではLXSVの区切り記号に「;」を使っています。そして、値の中の区切り記号に「,」を使っています。

name:Taro;city:Yokohama,Kawasaki
name:Hanako;city:Kawasaki,Yokohama
文字列

値の中にTAB, SPACE, 「,」, 「;」といった区切り記号を入れたい場合には、前述したように別の区切り記号を使うのが有効ですが、別の方法として「"」を使って文字列を指定することもできます。

name:Taro;city:"Yokohama,Kawasaki"
name:Hanako;city:"Kawasaki,Yokohama"

「"」を使って文字列を記述する場合は、「\n」や「\r」といったエスケープ文字を使って文字列に制御文字を入れることができます。また「”」の文字列内に「"」を入れる場合は「\"」として「\」を使ってエスケープします。

name:Taro;city:"Yokohama\nKawasaki"
name:Hanako;city:"Kawasaki\"Yokohama"

LXSVリテラル

Kaleidoxではレコードを記述するRecordというデータ型を提供しています。このRecordをリテラルとして記述する時にLXSVを用います。

kaleidox> name:Taro,city:Yokohama
name:Taro,city:Yokohama
kaleidox> :show
Record[2] name:Taro,city:Yokohama
文字列リテラルでの入力

Recordを文字列に対する修飾を使って入力することができます。文字列に対する修飾は文字列リテラルの前に修飾を示すプレフィックスを指定します。Recordの場合は「record」です。

以下のように使用します。

kaleidox> record"name:Taro,city:Yokohama"
name:Taro,city:Yokohama
kaleidox> :show
Reord[2] name:Taro,city:Yokohama

KaleidoxではLXSVのデータ型も用意されています。このデータ型を使う場合には修飾を示すプレフィックスとして「lxsv」を指定します。

kaleidox> lxsv"name:Taro,city:Yokohama"
name:Taro,city:Yokohama
kaleidox> :show
Lxsv[2] name:Taro,city:Yokohama

まとめ

Recordを使用するための前提となるLXSVについてご紹介しました。

次回はRecordの具体的な使い方について説明する予定です。

諸元

  • Kaleidox : 0.1.6

2019年9月30日月曜日

Kaleidox: JSON

Kaleidoxでは、実用的に利用できるアクション言語としてXML, HTML, JSON, CSV, Excelといったクラウドアプリケーションで使用する各種データ形式を簡単に扱えるようになっています。

前々回、前回とXMLとHTMLの操作方法について紹介しました。

今回はJSONの操作方法について紹介します。

JSONリテラル

例として以下のJSON文書を考えます。

また、このJSON文書を格納したJSONファイルをfile.jsonとして用意します。

{
  "id": "0001",
  "name": "taro",
  "point": 100
}
リテラル記述

KaleidoxではJSON文書をそのままリテラルとして記述することができます。

REPLでも以下のように直接入力することができます。

kaleidox> {
  "id": "0001",
  "name": "taro",
  "point": 100
}
{\n  "id": "0001",\n  "name": "taro",\n  "point": 100\n}
ファイル入力

REPLでJSON文書が格納されているJSONファイルからJSON文書を取り込む時は以下のようにJSONファイルのURLを記述すればOKです。

kaleidox> file:sample.json
{\n  "id": "0001",\n  "name": "taro",\n  "point": 100\n}\n

この例はプロトコルに「file」を指定してローカルファイルを読み込んでいますが、「http」を指定してリモートファイルを読み込むこともできます。

kaleidox> http://example.com/sample.json
{\n  "id": "0001",\n  "name": "taro",\n  "point": 100\n}\n

XPath

KaleidoxではJSON文書に対してXPathでパス検索をかけることができます。

{
  "id": "0001",
  "name": "taro",
  "point": 100
}

まずJSONファイルsample.jsonに対してXPath「@name」で検索すると以下になります。

JSON文書にある属性nameの値を取得することができました。

kaleidox> @name file:sample.json
"taro"

次にXPath「/name」で検索すると以下になります。こちらもJSON文書にある属性nameの値を取得することができました。

JSONにはXMLにある属性と要素の区別がないので、要素に対する検索ですがJSONの属性に対しても有効になっています。

kaleidox> /name file:sample.json
"taro"

XSLT

KaleidoxはJSON文書をXML文書にみたててXSLT変換を行うことができます。

サンプルとして使用しているJSON文書です。

{
  "id": "0001",
  "name": "taro",
  "point": 100
}

このJSON文書に対して提供するXSL記述は以下になります。

<?xml version="1.0" encoding="Shift_JIS"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html"></xsl:output>

  <xsl:template match="/">
    <HTML>
    <BODY>
    <xsl:apply-templates/>
    </BODY>
    </HTML>
  </xsl:template> 

  <xsl:template match="record">
    <TABLE>
      <THEAD>
        <TR>
          <TH>ID</TH>
          <TH>Name</TH>
   <TH>Point</TH>
        </TR>
      </THEAD>
      <TBODY>
 <TR>
   <TD><xsl:value-of select="id"/></TD>
   <TD><xsl:value-of select="name"/></TD>
   <TD><xsl:value-of select="point"/></TD>
 </TR>
      </TBODY>
    </TABLE>
  </xsl:template> 
</xsl:stylesheet>

XMLやHTMLの場合と同様にXSLファイルに続けてJSONファイルを指定するとXSL変換が行われます。

kaleidox> file:sample.xsl file:sample.json
<HTML><BODY><TABLE><THEAD><TR><TH>ID</TH>
<TH>Name</TH><TH>Point</TH></TR>...

変換後のXML文書を詳細を表示すると以下になります。

無事XSLTでJSONファイルをHTML文書に変換することができました。

kaleidox> :show:print
<HTML><BODY><TABLE><THEAD><TR><TH>ID</TH>
<TH>Name</TH><TH>Point</TH></TR></THEAD>
<TBODY><TR><TD>0001</TD><TD>taro</TD>
<TD>100</TD></TR></TBODY></TABLE>
</BODY></HTML>

Table

XMLやHTMLと同様にJSON文書で記述した表情報をTableデータとして扱うことができます。

サンプルとして使用しているJSON文書です。

{
  "id": "0001",
  "name": "taro",
  "point": 100
}

このJSON文書に対してtable-make関数を適用します。

kaleidox> table-make file:sample.json
Table[3x1]

その結果、幅3、高さ1の表を得ることができました。

この表の内容をshowコマンドで表示すると以下になります。無事HTMLの表の内容を取得できています。

kaleidox> :show
Table[3x1]
┏━━━━┯━━━━┯━━━━━┓
┃id  │name│point┃
┣━━━━┿━━━━┿━━━━━┫
┃0001│taro│100  ┃
┗━━━━┷━━━━┷━━━━━┛

JSON文書がJson Objectの場合は1行のテーブルに変換されます。

レコードシーケンス

JSON文書がJson Objectの配列だった場合は配列の長さ分の行を持ったテーブルに変換されます。

まずJson Objectの配列を持ったJSON文書を用意します。

[{
  "id": "0001",
  "name": "taro",
  "point": 100
},{
  "id": "0002",
  "name": "hanako",
  "point": 200
}]

このJSON文書に対してtable-make関数を適用します。

kaleidox> table-make file:sample-table.json
Table[3x2]

その結果、幅3、高さ2の表を得ることができました。

この表の内容をshowコマンドで表示すると以下になります。無事HTMLの表の内容を取得できています。

kaleidox> :show:print
┏━━━━┯━━━━━━┯━━━━━┓
┃id  │name  │point┃
┣━━━━┿━━━━━━┿━━━━━┫
┃0001│taro  │100  ┃
┠────┼──────┼─────┨
┃0002│hanako│200  ┃
┗━━━━┷━━━━━━┷━━━━━┛

まとめ

今回はJSONの操作について説明しました。

JSONではXPath(パス検索)はXSLT(データ変換)といったXMLで用意されている便利な機能が基本機能として提供されていません。

KaleidoxではJSON文書をXML文書として操作可能にすることで、XMLの便利な機能をJSON文書にも適用できるようにしました。

前々回、前回、今回でXML、HTML、JSONの操作方法についてみてきました。

KaleidoxではXML、HTML、JSONについて詳細を意識することなくシームレスに操作することができることを確認できました。

次回はこのメカニズムの土台となっているRecordについてみていく予定です。

諸元

  • Kaleidox : 0.1.5

2019年8月25日日曜日

Kaleidox: HTML

Kaleidoxでは、実用的に利用できるアクション言語としてXML, HTML, JSON, CSV, Excelといったクラウドアプリケーションで使用する各種データ形式を簡単に扱えるようになっています。

前回はこの中でXMLの操作方法について紹介しました。

今回はHTMLの操作方法について紹介します。

HTMLリテラル

HTMLは元々SGMLベースですが、XMLベースのXHTMLもあります。

XHTMLのみを対象にするとXML処理的には楽ですが、実際のところはSGMLベースのHTMLが主流なのでこちらの方も扱えないといけません。

Kaleidoxでは、SGMLベース、XMLベースのどちらのHTMLもHTMLデータとして扱い、内部的にはDOMで操作しています。

まず以下のHTML文書を考えます。XML的な観点ではすこしブロークンですが、このような形のタグ構造も扱える必要があります。

<b>HTML文書

HTMLリテラルは、文字列リテラルのプレフィックスにHTMLをつけたものになります。

kaleidox> html"""<b>HTML文書"""
<html><head xmlns="http://www.w3.org/1999/xhtml"/><body><b>HTML文書\n</b></b...

showコマンドを使うとHTML全体を表示することができます。

kaleidox> :show
SHtml: <html><head xmlns="http://www.w3.org/1999/xhtml"/><body><b>HTML文書
</b></body></html>

元のHTMLの内容はB要素のみのHTMLの断片ですが、読み込み時にはHTML要素から始まる本格的なHTML文書になっています。これは、HTMLに対するXSLTなどの処理が統一的に行えるように必要な情報を補完しているためです。

ファイル

ファイルに格納されているHTMLは、URIを指定して取り込むことができます。

kaleidox> file:sample.html
<html><head xmlns="http://www.w3.org/1999/xhtml"/><body><b>HTML文書\n</b></b...
kaleidox> :show
SHtml: <html><head xmlns="http://www.w3.org/1999/xhtml"/><body><b>HTML文書
</b></body></html>

XPath

HTMLに対してもXMLと同様にXPathによるアクセスを行うことができます。

まず、HTML文書を読み込みます。

kaleidox> file:sample.html
<html><head xmlns="http://www.w3.org/1999/xhtml"/><body><b>HTML文書\n</b></b...

HTML文書がスタックに積まれたので、これに対してXPath「htmlbody/b」を適用します。

kaleidox> /html/body/b
"HTML文書\n"

XPathを適用の結果、B要素の内容である「HTML文書」を取り出すことができました。

XPathの式の直後にHTMLファイルを指定しても同じ結果を得ることができます。

kaleidox> /html/body/b file:sample.html
"HTML文書\n"

XSLT

HTMLに対してもXMLと同様にXSLTによる変換を行うことができます。

XSLTに使用するXSL文書として以下のものを用意しました。

HTML文書の一部を変更する目的の典型的なXSL文書です。ここではB要素をI要素に変換するXSL文書になっています。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0">

  <xsl:template match="b">
    <i><xsl:value-of select="."/></i>
  </xsl:template>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

それではXSL文書をHTMLに適用してみましょう。

XSLT文書のファイル名の後ろにHTML文書名を指定します。すると内部的にはxslt関数が実行され、XSLT変換が行われます。

kaleidox> file:sample.xsl file:sample.html
<html><head xmlns="http://www.w3.org/1999/xhtml"/><body><i>HTML文書\n</i></b...

実行結果のHTML文書をshowコマンドで表示すると以下のようになりました。無事B要素がI要素に変換されています。

kaleidox> :show
SHtml: <html><head xmlns="http://www.w3.org/1999/xhtml"/><body><i>HTML文書
</i></body></html>

スクレイピング

KaleidoxはHTML文書内の表データを抽出する機能を提供しています。

まず表を記述したHTML文書を用意します。

<html>
    <head>
 <title>表サンプル</title>
    </head>
    <body>
 <p>表のサンプルです</p>
 <table>
     <thead>
  <tr><th>A</th><th>B</th></tr>
     </thead>
     <tbody>
  <tr><td>1</td><td>11</td></tr>
  <tr><td>2</td><td>12</td></tr>
  <tr><td>3</td><td>13</td></tr>
     </tbody>
 </table>
    </body>
</html>

このHTML文書に対してtable-make関数を適用します。

kaleidox> table-make file:table.html
Table[2x3]

その結果、幅2、高さ3の表を得ることができました。

この表の内容をshowコマンドで表示すると以下になります。無事HTMLの表の内容を取得できています。

kaleidox> :show:print
┏━┯━━┓
┃A│B ┃
┣━┿━━┫
┃1│11┃
┠─┼──┨
┃2│12┃
┠─┼──┨
┃3│13┃
┗━┷━━┛

まとめ

今回はXMLの操作について説明しました。

次回はJSONの操作方法についてみていく予定です。

諸元

  • Kaleidox : 0.1.4

2019年7月29日月曜日

Kaleidox: XML

アクション言語を実用的に使用するにはXML, HTML, JSON, CSV, Excelといったクラウドアプリケーションで使用する各種データ形式を容易に扱えるようになっている必要があります。これらのデータをプログラム内に取り込んだり、データ形式間の相互運用が簡単にできることが重要です。

Kaleidoxでは各種データ形式の個々の操作性と、これらのデータ形式間の相互運用の両面について最適な記述が可能となるような言語機能を提供しています。

ここではまずXMLの操作についてみていきます。

XMLリテラル

KaleidoxはXMLを第一級の言語要素として考えていることもあり、リテラルで記述することができます。

以下のXMLを考えます。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

まずKaleidoxプログラムですが、以下のように上記XMLを直接XMLリテラルとして記述することができます。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

上記プログラムをsample.kに格納して実行すると以下のようになります。

$ kaleidox sample.k
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XMLリテラルで記述されたXML文書がそのまま評価され、評価結果がXML文書として出力されました。

REPL

REPLで同様の処理を行うと以下のようになります。

kaleidox> <account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...

プロンプトからXML文書を入力しています。

通常REPLのプロンプトからは改行を区切りとした一行の式を入力しますが、XMLやJSONなどの構造を持つリテラルはリテラル内に改行が入っていても入力することができます。

kaleidox> :show
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

ファイルから読み込む場合はURLを指定します。

kaleidox> file:sample.xml
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...

読み込み結果をshowコマンドで表示すると以下になります。

kaleidox> :show
Xml(String)
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XPath

XML文書を処理する場合にはXPathによるXML文書内のデータアクセスが極めて有効です。

KaleidoxではXPathによるXML文書アクセスを行うことができます。

kaleidox> <account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...
kaleidox> /account/name
"taro"

XSLT

XML文書の操作ではXSLTによる変換も重要です。

変換対象のXML文書として前出のものを使用します。ファイルsample.xmlに格納しています。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XML文書を閲覧用のHTML文書に変換するXSLです。ファイルsample.xslに格納します。

<?xml version="1.0" encoding="Shift_JIS"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html"></xsl:output>

  <xsl:template match="/">
    <HTML>
    <BODY>
    <xsl:apply-templates/>
    </BODY>
    </HTML>
  </xsl:template> 

  <xsl:template match="account">
    <TABLE>
      <THEAD>
        <TR>
          <TH>Name</TH>
   <TH>Value</TH>
        </TR>
      </THEAD>
      <TBODY>
 <TR>
   <TD><xsl:value-of select="id"/></TD>
   <TD><xsl:value-of select="name"/></TD>
   <TD><xsl:value-of select="point"/></TD>
 </TR>
      </TBODY>
    </TABLE>
  </xsl:template> 
</xsl:stylesheet>

XSLT変換はxslt関数で行うことができます。

引数にはXSLリテラルまたはXSL文書URL、XMLリテラルまたはXML文書URLを指定します。

以下ではXSL文書URLとXML文書URLを指定しています。xslt関数実行の結果、XML文書を変換したHTML文書に得ることができました。

kaleidox> xslt file:sample.xsl file:sample.xml
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TB...
kaleidox> :show:print
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TBODY><TR><TD>0001</TD><TD>taro</TD><TD>100</TD></TR></TBODY></TABLE></BODY></HTML>

関数を省略してもXSLファイルが先頭の場合はxslt関数が実行されます。

kaleidox> file:sample.xsl file:sample.xml
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TB...

Record

Kaleidoxはレコード/テーブル指向の言語なので、各種データをレコードに変換すると、より柔軟にデータ操作を行うことができます。

record-make関数でXMLからレコードを生成する事ができます。record-make関数はスキーマを与えなくてもXML文書の内容からレコードのスキーマを推測します。

kaleidox> record-make file:sample.xml
id:1,name:taro,point:100

Table

XMLをテーブルに変換することで、Kaleidoxが提供するテーブル操作機能を使ってデータ操作を行うことができます。

table-make関数でXMLからテーブルを生成することができます。table-make関数はスキーマを与えなくてもXML文書の内容からレコードのスキーマを推測します。

kaleidox> table-make file:sample.xml
Table[3x1]

生成されたテーブルデータを表示するためにshowコマンドを実行すると以下の情報が表示されます。

kaleidox> :show
Table[3x1]
┏━━━━┯━━━━┯━━━━━┓
┃id  │name│point┃
┣━━━━┿━━━━┿━━━━━┫
┃0001│taro│100  ┃
┗━━━━┷━━━━┷━━━━━┛
リスト

データ列を記述するXML文書として以下のものをsample-list.xmlとして用意します。

<accounts>
  <account>
    <id>0001</id>
    <name>taro</name>
    <point>100</point>
  </account>
  <account>
    <id>0002</id>
    <name>hanako</name>
    <point>200</point>
  </account>
</accounts>

ここからtable-make関数を使ってテーブルを生成します。

kaleidox> table-make file:sample-list.xml
Table[3x2]

生成した結果のテーブルは以下になります。

kaleidox> :show:print
┏━━━━┯━━━━━━┯━━━━━┓
┃id  │name  │point┃
┣━━━━┿━━━━━━┿━━━━━┫
┃0001│taro  │100  ┃
┠────┼──────┼─────┨
┃0002│hanako│200  ┃
┗━━━━┷━━━━━━┷━━━━━┛

まとめ

今回はXMLの操作について説明しました。

次回以降、HTML, JSON, CSV, Excelの順にデータ操作方法についてみていく予定です。

諸元

  • Kaleidox : 0.1.3

2019年6月2日日曜日

Kaleidox

前回SimpleModeling構想の中核のプロダクトとして開発中のアクション言語(Action Language)であるkaledidoxを紹介しました。

今回はこのkaleidoxのざっくりとした概要について説明します。

特徴

Kaleidoxは以下のプログラミング言語からヒントを得ています。

  • Lisp + Shell Script + Forth + COBOL

ベースとなるのはLispで、基本的にはLispインタープリターがプログラムを解釈実行する構造になっています。

Shell Scriptは、アクション言語として簡単なパイプライン構造でデータ操作を中心としたプログラミングが可能となるように、Shell Script的な文法をLispに対するシンタクスシュガーとして実現しています。

Forthは古のスタック指向言語ですがShell Script的文法と組み合わせてパイプライン・プログラミングを実現するために、Forth的なスタック操作の機能を提供しています。

COBOLは(1)レコード指向、(2)Divisionによる区画を参考にしています。

ソースコード

KaleidoxはGitHubで開発しています。

インストール

Kaleidoxは開発の初期であることもあり簡単にインストール可能なリリース版は用意していません。

興味のある方はソースコードからビルドしてみてください。

以下の手順でビルドすることができます。

$ sbt universal:packageBin

targetuniversalkaleidox-0.1.2.zip というファイルが作成されるのでこれを展開してインストールします。

sbtの機能を使って、debian, rpm, docker, graalvmのインストールパッケージを作成することも可能です。

Hello World

まずkaleidoxの簡単な使い方としてREPLによる実行をしてみます。

コンソールから引数なしでkaleidoxを起動するとREPLのプロンプトが表示されます。

$ kaleidox
kaleidox> 

kaleidoxはLispなので式を評価することでプログラムを実行します。

最も簡単な式はリテラルです。最も重要なリテラルである文字列を入力してみます。

kaleidox> "Hello World"
Hello World

評価の結果、文字列がそのまま返ってきます。

簡単な計算

次は関数の評価です。+関数で足し算を行ってみます。

kaleidox> (+ 1 2)
3

上記は普通のLispの関数評価ですが、kaleidoxの文法では以下のように括弧を外す表記ができるようになっています。

kaleidox> + 1 2
3

この例だけでは分かりづらいかもしれませんが、この文法によりShell Script的にプログラムを書くことができるようになっています。

まとめ

今回はかkaleidoxのざっくりした概要とインストール、Hello Worldまで紹介しました。

次回はJson, XML周りの扱いについて紹介する予定です。

諸元

  • Kaleidox : 0.1.2

2019年5月18日土曜日

SimpleModeling

Modegramming Styleブログではモデリングとプログラミングの一体化を目指してModegrammingというコンセプトを提唱しています。

このModegrammingを実現するための技術体系としてまとめようとしているのがSimpleModelingです。

SimpleModelingでは仕様定義と実装が一体化したモデル駆動開発の実現を目指して、方法論の整備とツールの開発を行っています。

SimpleModelingの基本的な方法論については以下の本にまとめています。

SimpleModelingの枠組みの中で、ここまでで以下のツールを開発してきました。

SimpleModelingによる方法論をモデル駆動開発につなげているツールとしてsimplemodelerを開発していましたが、この開発を通じて見えてきたのが自動生成したアプリケーションの動作基盤となるクラウドプラットフォームです。モデル駆動開発のための重要な要素技術としてクラウドアプリケーションプラットフォーム(以下CAP)が必要という認識にいたりました。

このCAPとしてを業務向けに開発を進めてきたものが Prefer Cloud です。Prefer CloudはPrefer Cloud Platform(以下PCP)というCAP上で動作しています。2012年から足掛け8年ほど開発を進め、一応の軌道に乗ってきました。

そこで、PCPの開発は進めつつ、次の活動としてモデル駆動開発を実現するためのSimpleModelingを再始動することにしました。

モデリング

オブジェクト指向開発方法論(OOAD)の一旦の完成時期を2004年だとすると、すでに15年が経過しており、その間にさまざまな技術革新がありました。

モデリングに関しては関数型言語の進化を取り込んだObject-Functional Analysis and Designの整備が必要だと考えています。

この問題に関しては以前から検討を続けており以下のような記事で検討を続けてきました。

また、クラウド環境を取り巻く以下のようなの新技術をカバーできるようにメタモデルの拡張やプロファイルの整備も行う必要があるでしょう。

  • AI
  • IoT
  • RPA
  • FinTech

プロダクト

SimpleModelingを支えるプロダクトとして以下のものを開発中です。いずれもOSSとして展開しています。

  • smartdox : 文書処理系 (前出)
  • simplemodeler : モデルコンパイラ (前出)
  • kaleidox :: アクション言語
  • arcadia :: ウェブ・フレームワーク
kaleidox

kaleidoxはオブジェクトモデリングとプログラミングをシームレスに連携させることを目的とするアクション言語です。

アクション言語はモデル駆動開発を成立させるミッシングリンクを埋める要の技術ではないか、というのが最近の技術的な興味で、これを具象化するためのアクション言語としてkaleidoxを開発してみました。

スクリプト言語としても面白いものに仕上がっていると思います。機能が多岐に渡るので次回以降少しずつ紹介していく予定です。

arcadia

モデル駆動開発を進める際にネックとなるのがWebフロントです。

サーバーサイドのサービスはRESTサーバーとして実現する場合、モデル駆動開発との相性がよい形で実現しやすいですが、Webフロントに関してはスクラッチの開発になるケースが多いと思います。

画面デザインの調整などどうしてもスクラッチ開発せざるを得ない部分もありますが、ビジネスルールの適用(パラメタの値域など)やアプリケーションロジックとのREST連携などモデル駆動開発の恩恵を得られると考えられるパーツも多いと思います。

モデル駆動開発では、後者の部分は自動生成しつつスクラッチ開発の画面部分との連携をとるためのフレームワークが必要となってくると考えています。

このようなモデル駆動開発向けWebフレームワークとして開発したものがarcadiaです。

kaleidoxの機能を一通りご紹介した後、arcadiaについても紹介していこうと思います。

まとめ

SimpleModelingの全体構想とプロダクトについてご紹介しました。

次回以降は当面SimpleModelingの中核プロダクトであるkaleidoxについて取り上げていく予定です。

2016年12月9日金曜日

ReaderWriterStateモナドと畳込み

ReaderWriterStateモナドは「Patterns in Types - A look at Reader, Writer and State in Scala」を見てからずっと気になっていたのですが、実務のプログラミングでも汎用的な基盤として使えるのではないかとふたたび自分の中でブームになってきたので、少し試してみました。

例題

レコードを正常なものと異常なものに選別する処理を考えます。異常なレコードは異常と判断した理由つきで記録します。

データ連携処理ではよく出てくる処理だと思います。

この処理を以下の関数として実装することにします。

def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord])

この関数を以下のバリエーションで実装していきます。

Foldable
通常の畳込み
Monoid
モノイドによる畳込み
State
Stateモナドによる畳込み
Traverse&State
TraverseとStateモナドによる畳込み
Reducer
Reducerを使った畳込み
ReaderWriterState
ReaderWriterStateモナドを使った畳込み

直接の目的はボクが常用しているFoldableによる畳込みとReaderWriterStateモナドによる畳込みを比較することです。

同時に色々なアプローチの比較も行い、それぞれのアプローチの使い所を探っていきます。

準備

まずプログラムが扱うドメインのオブジェクトを定義します。

object Domain {
  type Record = Map[String, Any]
  type Errors = Vector[ErrorRecord]
  type Reason = String
  case class ErrorRecord(record: Record, reason: Reason)
  case class RecordsState(totalCount: Int = 0, errorCount: Int = 0) {
    def success = copy(totalCount = totalCount + 1)
    def error = RecordsState(totalCount + 1, errorCount + 1)
  }
  trait ExecutionContext {
    def verify(r: Record): Option[Reason]
  }
  class DefaultExecutionContext() extends ExecutionContext {
    def verify(r: Record): Option[Reason] =
      if (r.get("id").isEmpty)
        Some("No id")
      else
        None
  }
}

以下の型、case class、クラスを定義しました。

Record
レコード
ErrorRecord
エラーとなったレコードと理由
Reason
エラー理由
Errors
ErrorRecordの集まり
RecordState
処理状況を記録
ExecutionContext
実行コンテキストのインタフェース
ExecutionContextImpl
実行コンテキストの実装

単にエラーコードを選り分けるだけでなく、以下の機能を実現できるようにしています。

  • 実行状況をRecordStateに記録して取得可能にしている。
  • エラーの判定のロジックをExecutionContextとして指定可能にしている。

Foldable

FP(Functional Programming)で一般的な畳込み処理です。

object FoldLeft {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) =
    xs.foldLeft(Z(context))(_+_).result
}

VectorのfoldLeft関数を使って畳込み処理を行います。

case classを使う手法はボクが個人的に使っているもので一般的ではないと思いますが、特に難しくはないと思います。(「foldの小技」)

FoldLeftのfold関数はExecutionContextを暗黙パラメタとして受取りcase class Zのパラメタとして渡しています。case class ZはこのExecutionContextを使用してロジックを実行します。ロジックの可変部分をExecutionContextに分離することでfold関数の処理をチューニング可能な構造になっています。

Monoid

次はMonoidを使った畳込みを考えてみます。

object FoldableMonoid {
  import Domain._
  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    val context: ExecutionContext = new ExecutionContextImpl()
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def +(rhs: Z): Z = copy(
      records = records ++ rhs.records,
      errors = errors ++ rhs.errors
    )
  }

  object Z {
    def empty = Z()
    def point(rec: Record) = empty + rec
  }

  implicit object ZMonoid extends Monoid[Z] {
    def zero = Z.empty
    def append(lhs: Z, rhs: => Z) = lhs + rhs
  }

  def fold(xs: Vector[Record]): (Vector[Record], Vector[ErrorRecord]) = {
    xs.foldMap(Z.point).result
  }
}

モノイドの場合、実行コンテキストの意図のExecutionContextを外部から与えることは筋悪そうなので固定のものを使うことにしています。

評価

コーディング的には、Monoid計算に適合するように各種関数を用意したり、型クラスMoonoidの型クラスインスタンを定義したりという手間がかかります。

何回も使用するロジックの場合はよいですが、その場限りのロジックの場合はコーディングのオーバーヘッドの方が大きくなるのでFoldable方式の方がよさそうです。

またExecutionContextを外付けで与えることができないのはかなり大きな問題です。

Monoidについては、すでにMonoidがある場合はそれを利用するのがよいですが、畳込みのために、わざわざMonoidのメカニズムを積極的に使うというほどではないようです。

State

次はStateモナドを使った畳み込みです。

object StateWithTraverse {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    val ts = xs.traverseS(x => State[Z, Unit] {
      case s => (s + x, ())
    })
    ts.exec(Z(context)).result
  }
}

scalazの型クラスTraverseにはStateモナドを使って走査する機能があります。Stateモナドで畳み込み動作をするようにしておけば、Traverseでの走査の過程で畳み込みを行うことができます。

ここではFoldableで使用したcase class Zと同じものをStateモナドでの状態として使用する実装を行っています。

実行コンテキストであるExecutionContextは状態の一部として受渡しています。

評価

Stateモナドの使い方に慣れていれば、Foldableとほぼ同じような手間でプログラミングすることができます。

ただ、Stateモナド実行のオーバヘッドなどを考えるとFoldableで間に合っているものをわざわざStateモナド化する必然性はなさそうです。

再利用可能なStateモナド部品を作った時に、このロジックで畳み込みに利用することも可能という選択肢として考えておくとよいと思います。

Stateモナドは畳込みの汎用ロジック向けではなく、以下の記事にまとめたように状態遷移/状態機械を作る時のキーパーツとして考えていくのがよさそうに思いました。

Traverse&State

Stateモナドは、1つの処理ごとに型パラメータAで示す処理結果を出力する機能があり、for式などで組み合わせる時にパラメタとして受け渡しすることで、全体として複雑な処理を記述できる機能を持っているのですが、traverseSによる畳込みの場合はここの部分で、走査結果を蓄積する形になります。

このためTraverseとStateを組み合わせる場合、Traverseの機能を活用してStateの実行結果をTraverse側に蓄積させることができるので、その点を活かした実装に改良してみました。

object TraverseState {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def apply(rhs: Record): (Z, Option[Record]) = context.verify(rhs) match {
      case Some(reason) => (copy(errors = errors :+ ErrorRecord(rhs, reason)), None)
      case None => (copy(records = records :+ rhs), Some(rhs))
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    val ts = xs.traverseS(x => State[Z, Option[Record]] {
      case s => s(x)
    })
    val (s, records) = ts.run(Z(context))
    (records.flatten, s.errors)
  }
}
評価

前節「State」は正常レコードもcase class Z経由で取得することを前提にTraverseの主ルートには「()」を渡していて、事実上封印していました。

ここでは、正常レコードをTraverseの主ルートで受け渡すことができるようにcase class Zにapplyメソッドを追加しました。

case class Zが再利用可能な汎用ロジックを実装できるのであれば、ひと手間かけてapplyメソッドを追加しておくことで、利用範囲が広がります。

ただ、foldLeftよりはやや手間がかかるのは「State」と同じなので、一度限りのロジックに対して普段使いで適用する感じではなさそうです。

Reducer

ちょっと脱線してReducerを使った畳込みを考えてみました。

Reducerは畳み込み処理の中のデータを足し込む処理を汎用化したものです。畳込み対象がMonoidでなく、畳込み結果がMonoidである場合に使用できます。畳み込み処理の走査処理を汎用化(左畳み込み、右畳み込みの最適選択)したGeneratorと組み合わせて使用するのが基本的な使い方のようです。

object Reducer {
  import Domain._
  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    val context: ExecutionContext = new ExecutionContextImpl()
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def +(rhs: Z): Z = copy(
      records = records ++ rhs.records,
      errors = errors ++ rhs.errors
    )
  }

  object Z {
    def empty = Z()
    def point(rec: Record) = empty + rec
  }

  implicit object ZMonoid extends Monoid[Z] {
    def zero = Z()
    def append(lhs: Z, rhs: => Z) = lhs + rhs
  }

  def fold(xs: Vector[Record]): (Vector[Record], Vector[ErrorRecord]) = {
    val reducer = UnitReducer((x: Record) => Z.point(x))
    val G = Generator.FoldlGenerator[Vector]
    G.reduce(reducer, xs).result
  }
}

ReducerはMonoid以外の畳込み対象を一度Monoidに変換してから畳み込むというロジックなので、畳込みがMonoidの機能範囲に限定されます。

今回のケースではExecutionContextを外付けにするのが難しいので、Monoidであるcase class Zが内部で固定で持っています。

評価

ReducerはMonoid以外の要素の列をMonoidに畳み込む時のアダプタ的な機能と考えると分かりやすいと思います。ただ、このための機能としてはFoldableのfoldMapコンビネータという非常に汎用的な機能があるので、Reducerをわざわざ使うシーンはあまりなさそうです。

また、色々と糊コードを書かないといけないのとMonoidの制約が入ってくるので、汎用の畳込み機能として使うのはお得ではなさそうということも確認できました。

ReducerはscalazのreduceUnordered関数で並列実行したTaskの結果の順不同畳込みに使用されています。こういった、特別な用途向けの機能と考えておくとよさそうです。

ReaderWriterState

それでは本命のReaderWriterStateモナドを使ってみます。

object ReaderWriterStateFold {
  import scala.language.higherKinds
  import Domain._

  def run[C[_]: Foldable, X, R, W: Monoid, S, A: Monoid](xs: C[X], rws: X => ReaderWriterState[R, W, S, A], r: R, s: S): (W, A, S) = {
    case class RWSZ(
      writer: W = Monoid[W].zero,
      outcome: A = Monoid[A].zero,
      state: S = s
    ) {
      def result = (writer, outcome, state)
      def apply(r: R, x: X) = {
        val (rw, ra, rs) = rws(x).run(r, state)
        RWSZ(rw, ra, rs)
      }
    }
    xs.foldLeft(RWSZ())(_.apply(r, _)).result
  }

  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def apply(context: ExecutionContext, rhs: Record) = {
      val z = context.verify(rhs) match {
        case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
        case None => copy(records = records :+ rhs)
      }
      (z.errors, z.records, z)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    def rws(a: Record) = ReaderWriterState[ExecutionContext, Vector[ErrorRecord], Z, Vector[Record]] {
      case (r, s) => s.apply(r, a)
    }
    val (errors, records, z) = run(xs, rws, context, Z())
    (records, errors)
  }
}

run関数は汎用関数なので、今回の用途向けに作成した部分はcase class Zとfold関数だけなのでそれほど大きくはありません。run関数を再利用することを前提にすると、ほとんどStateやTraverse&Stateと同じ手間で畳み込み処理を書くことができます。

ReaderWriterStateモナドは、Stateモナドの持つStateモナド自身と処理結果の出力に加えて実行コンテキストなどの参照専用データの受け渡し(Reader)とログ的な蓄積データの出力(Writer)の機能を持っています。

run関数では、引数に処理対象のVectorとReaderWriterStateモナド、実行コンテキストのExecutionContextと状態を持つcase class Zの初期値を渡しています。実行コンテキストと状態の初期値を外部から与えることができるので、ReaderWriterStateモナドの振る舞いを実行時にカスタマイズできる構造になっています。

run関数の返却値はStateモナドの実行結果の正常レコードとWriterに蓄積されたエラーコード、そしてState(case class Z)の最終結果です。

評価

run関数を事前に用意しておけば、Traverse&Stateモナドとほとんど変わらない使い勝手で使うことができることが確認できました。

Stateモナドの場合は、実行コンテキスト(ExecutionContext)の指定と蓄積データ(エラーレコード)の取得を状態(case class Z)の中に自分で実装する必要がありました。

一方、ReaderWriterStateモナドでは、実行コンテキストの指定は蓄積データの取得はReaderWriterStateモナドの機能としてもっているので、状態と計算結果に実装上の注意を集中することができます。また、実行コンテキストと蓄積データのインタフェースが決まっているので、部品として組み合わせることも可能になります。

本例でもそうであったように、多くの用途ではReaderWriterStateモナドが提供する機能で要件が満たせる事が多いのではないかと思います。そうであるならば、ReaderWriterStateモナドが提供する汎用機能を使って再利用可能な部品を作ることで、部品の再利用を促進できる可能性が高いと考えられます。

考察

畳み込み処理に関しては、一度限りのロジックであるならばFoldableのfoldLeftを使って普通に書くのが一番開発効率がよさそうです。

一方、StateモナドやReaderWriterStateモナドを使って畳込みを実装するのも、それほどの手間でないことも確認できました。StateモナドやReaderWriterStateモナドにピッタリ合うケースでは、一度限りののロジックでもこれらのモナドを使うのもありそうです。

StateモナドやReaderWriterStateモナドは共通部品向けの汎用インタフェースという意味合いが大きいですが、使い方が難しいと積極的には使いづらいところです。どちらのモナドもわりと簡単に使えることが分かったのが収穫でした。StateモナドやReaderWriterStateモナドをつかって再利用可能な部品を整備していく方向性が実現可能という感触を得ることができました。

また、Stateモナド、ReaderWriterStateモナド、Monoid、Reducerの機能差も改めて確認することができました。

当面は以下のような方針で適用していきたいと考えています。

  • 一度限りのロジックはfoldLeft(普通の畳込み)
  • 再利用可能な処理で実行コンテキストがなくMonoid化できるものはMonoid
  • 共通部品はReaderWriterStateモナド化を考える(実行コンテキスト&蓄積データ&計算結果&状態)
  • 必要に応じてStateモナドやReducerを使う

諸元

  • Scala 2.11.7
  • Scalaz 7.2.0