はじめに
以前 Prolog 向けに書いた、文芸的プログラミング的 Prolog プログラミング を Pony 向けにしてみようと思います。
「文芸的プログラミング的プログラミング」というのは、単純に
コメント部分をマークダウンで書いて、コード部分をマークダウンテキストの一部であるかのように書こう、
というだけのことです。
文芸的プログラミング的プログラミング では元のソースコードに含まれるコード・コメントをそのままの順序でマークダウンテキスト化するため、文章の文脈に沿ってソースを並べる必要があります。このため、プログラム記述順序について煩い制限があるプログラミング言語にはあまり適していません。
この点、Pony は 文芸的プログラミング的プログラミング 向きです。
あくまでもプログラミングなので、記述したソースコードはそのままコンパイラが解釈できる必要があります。
このため、コメント部分に微妙に手を入れることでマークダウンテキスト領域とそれ以外とを指示します。
マークダウンテキストへの変換ツール自体は Python で書いても構わないのですが、今回は経験値を上げるために Pony で書くことにしました。
メインアクター
引数に与えられたファイルを読み込んでマークダウン形式に加工します。
記事の長さはある程度限定されているので、単純にソースファイルをまるごとバイト列に取り込んでから加工することにします。
use "files"
actor Main
new create(env: Env) =>
try
let target_file_path = FilePath(env.root, env.args(1)?)
match OpenFile(target_file_path)
| let file: File =>
// ---- ファイルを読み込む
let buf: Array[U8] = Array[U8]
while file.errno() is FileOK do
buf.append(file.read(1024))
end
// ---- 読み込んだ内容をマークダウン形式に変換し、出力する
env.out.write(Translator(buf).output()?)
end
else
try
env.out.print("usage: " + env.args(0)? + " input")
end
end
マークダウン形式への変換
読み込んだファイルの内容を元にマークダウン形式にします。
単純に言ってしまえば、テキスト部分とソースコード部分とを分けて、ソースコード部分の前に "```pony" を、ソースコード部分の後ろに "```" を付けるだけです。
単純にコメント部分すべてをテキスト部分として取り扱ってしまうと逆にソースコード部分にコメントを含めることができません。
このため、コメント開始・終了に数文字付け加えたものをモード切替指定として使用します。
具体的には、ソースファイルに
```*/
(ソースコード)
/*```
のような部分があれば
```
(ソースコード)
```pony
に変換します。
マークダウン化したテキストに含めたくない部分は
!*/
(ソースコード)
/*!
のようにします。
空白の除去などの手間を省くために、これらの特殊コメントは行頭にあるものとします。
変換処理本体は Translator クラスが実行します。
class Translator
let _buf: Array[U8]
new create(buf: Array[U8]) =>
_buf = buf
fun output(): Array[U8] val ? =>
var r: Array[U8] iso = recover iso Array[U8] end
var state: State = StateInit // 初期状態
// --- 一文字づつ処理する
for c in _buf.values() do
(state, r) = state(consume r, c)? // 状態遷移
end
// --- 終端処理
state.finish(consume r)
ここで、output メソッドの戻り値が val なのは、env.out.write が引数として val を要求するためです。
ref を val に直接変換することはできないので、output メソッドにおける r は ref でなく iso(または trn)でなければなりません。
r を iso にするためには、state 呼び出しにおいても iso を一旦明け渡して戻り値として iso を取り戻す、ということが必要です。
状態遷移
Translator クラスの output メソッドは、以下の変換を行います:
- コメント部分 → マークダウン本文
- コード部分 → マークダウンに埋め込むコード部分
- 不要コード部分 → マークダウンに出力しない
今回、この変換は一文字づつ判定して状態遷移することによって実装しました。
状態
状態を表すインターフェースをまず定義します。
apply で入力文字 c に応じて状態遷移を行ったり、あるいは、出力 r に文字を出力します。
finish は、入力文字がなくなったときに出力に付与しなければならない状況で用います
(例えば、生成される文の最後が"```pony" で始まるコード部分だった場合、必ず "```" を付ける必要があります)。
interface State
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ?
fun finish(r: Array[U8] iso): Array[U8] iso^ =>
consume r
初期状態
マジックコメント部分などを避けるために、初期状態では入力文字を出力対象としません。
最初の /* を検知すると、コメント部分処理状態に移行します。
class StateInit is State
let _pattern: String = "/*"
let _index: USize
new create(index: USize = 0) =>
_index = index
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ? =>
if c == _pattern.at_offset(_index.isize())? then
if (_index + 1) < _pattern.size() then
(StateInit(_index + 1), consume r)
else
(StateText, consume r)
end
else
(StateInit, consume r)
end
コメント部分処理状態
コメント部分処理状態は、与えられた入力がマークダウンテキストであるとみなしてそのまま出力します。
改行に続く "```*/" を検知すると、"```pony" を出力した後にコード部分処理状態に移行します。
"!*/" を検知すると空白部分処理状態に移行します。
class StateText is State
let _prev_c_is_nl: Bool // 前回文字が '\n' だったなら true
new create(prev_c_is_nl: Bool = false) =>
_prev_c_is_nl = prev_c_is_nl
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) =>
match (c, _prev_c_is_nl)
| ('`', true) => (StateTextToCode("```*/\n", 1), consume r)
| ('!', true) => (StateTextToIgnore("!*/\n", 1), consume r)
else
r.push(c)
(StateText(if c == '\n' then true else false end), consume r)
end
コード部分処理状態への移行可否判断中の状態に対応するクラスが以下です。
入力文字の並びが、コンストラクタに与えられた文字列に一致したなら、"```pony" を出力したうえでコード部分処理状態に移行します。
class StateTextToCode is State
let _pattern: String
let _index: USize
new create(pattern: String val, index: USize) =>
_pattern = pattern
_index = index
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ? =>
if c == _pattern.at_offset(_index.isize())? then
if (_index + 1) < _pattern.size() then
(StateTextToCode(_pattern, _index + 1), consume r)
else
r.append("\n```pony\n".array())
(StateCode, consume r)
end
else
r.append(_pattern.substring(0, _index.isize()))
r.push(c)
if c == '\n' then
(StateText(where prev_c_is_nl = true), consume r)
else
(StateText(where prev_c_is_nl = false), consume r)
end
end
空白部分処理状態への移行可否判断中の状態も同様に書けます。
class StateTextToIgnore is State
let _pattern: String
let _index: USize
new create(pattern: String val, index: USize) =>
_pattern = pattern
_index = index
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ? =>
if c == _pattern.at_offset(_index.isize())? then
if (_index + 1) < _pattern.size() then
(StateTextToIgnore(_pattern, _index + 1), consume r)
else
(StateIgnore, consume r)
end
else
r.append(_pattern.substring(0, _index.isize()))
r.push(c)
if c == '\n' then
(StateText(where prev_c_is_nl = true), consume r)
else
(StateText(where prev_c_is_nl = false), consume r)
end
end
コード部分処理状態
呼び出し側で "```pony" を出力した前提で、コード部分処理は単純に入力文字をそのまま出力します。
改行に続く "/*```" を検知すると "```" を出力してテキスト部分処理状態に戻ります。
"/*" だけの場合にはコード部分の一部として出力を継続します。
class StateCode is State
let _pattern: String = "\n/*```\n"
let _index: USize
new create(index: USize = 0) =>
_index = index
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ? =>
if c == _pattern.at_offset(_index.isize())? then
if (_index + 1) < _pattern.size() then
(StateCode(_index + 1), consume r)
else
r.append("\n```\n".array())
(StateText, consume r)
end
else
r.append(_pattern.substring(0, _index.isize()))
if c == _pattern.at_offset(0)? then
(StateCode(1), consume r)
else
r.push(c)
(StateCode(0), consume r)
end
end
fun finish(r: Array[U8] iso): Array[U8] iso^ =>
"""
コード部分処理中に入力文字がなくなったら最後に "```" を付ける
"""
r.append("\n```\n".array())
consume r
空白部分処理状態
コメント部分処理状態で、
"!*/"
を検知した場合、それに続くコード部分は無視します。
生成される文章に含めたくないコードを除外するために用います。
"/*!"
でコメント部分処理状態に戻ります。
class StateIgnore is State
let _pattern: String = "/*!"
let _index: USize
new create(index: USize = 0) =>
_index = index
fun apply(r: Array[U8] iso, c: U8): (State, Array[U8] iso^) ? =>
if c == _pattern.at_offset(_index.isize())? then
if (_index + 1) < _pattern.size() then
(StateIgnore(_index + 1), consume r)
else
(StateText, consume r)
end
else
(StateIgnore, consume r)
end
まとめ
入力ファイルを読み込んで出力するプログラムを Pony で書いてみました。
バイト列や文字列をメソッド間でやりとりするプログラムを書いてようやく、ソースコード上に iso の consume や recover がちらつくようになり「Pony でプログラムを書いている」感が出てきた気がします。
今回はバイト列から一文字づつ切り出して状態遷移する形で書きましたが、libpcre2 を使った Pony 用正規表現ライブラリもあるようで、これを使ったほうが良かったかもしれません。
ちなみに、以前書いた「文芸的プログラミング的 Prolog プログラミング」同様に、今回の記事も作成したプログラムを用いて生成しました。