1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Pony 試乗記(7) 文芸的プログラミング的 Pony プログラミング

Posted at

はじめに

以前 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 プログラミング」同様に、今回の記事も作成したプログラムを用いて生成しました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?