Go言語で構造体と構造体をマッピングする、いわゆるObject mapperを生成するCLIを書きました。

経緯

READMEに書いてる通りですが、いわゆるClean architectureなど多層構造のアプリケーションではどうしても似たようなオブジェクトを定義せざるを得ないことがあります。

特にキツい(?)のがgRPCを採用した場合ですね。 protocが吐く構造体は完全にprotobufに依存したものになっておりある程度層をまたいで同じオブジェクトを持っていくのは許容しよう、と割り切ってもこれをいわゆるドメイン層にそのままもっていくのは結構ハードルが高いのではないかと思います。

#あと、gogo/protobuf 亡き今、protoc-gen-goが非標準命名規則でソースコードを出力するのもキモチワルイ。。。

WEB上にいっぱいあるGo + gRPCのクリーンアーキテクチャサンプル的なものでもここはかならず似たオブジェクトに詰め替えています。GoにはJavaにおけるDTOのような歴史はあまりないですし、アーキテクチャ上必要なことなんだから手で書くべき、という人もいますがとはいっても項目数が多くなってくるとつらいですよね。

これをある程度自動で詰め替えるライブラリはリフレクションを使ったものはそれなりにあるのですが、経験上

  • リフレクションを使ったものはデバッグがつらい
    • 項目が多くなってきたときに、1つだけなんかマッピングされてない!というときになんでマッピングされてないの? というのがわかりづらい
  • 性能が微妙
    • いくらGoのリフレクションが早いといってもリフレクション使わないコードよりは当たり前だが遅い

ということで、コード生成してくれるタイプのものが欲しかったんですがいいのが無かったので書いた、という次第です。

できること

構造体と構造体をマッピングするインタフェースと実装を生成できます。マッピング定義はYAMLに記載、結構柔軟に定義できます。 ネストしたものも再帰的にマッピングできます。

  • 例: stringtime.Timeをマッピングするものを登録しておけば自動的にこれらのフィールドを持つ構造体を変換可能

比較的にきれいな(自分的には)インタフェースを吐くので最悪、このツール使うのやめようとなってもインタフェースに対する実装を自分で書けばよいだけです。生成するソースはこんな感じ

 1package mapper
 2
 3import (
 4    pkg00002 "time"
 5
 6    pkg00001 "example.com/testmod/domain"
 7    pkg00000 "example.com/testmod/model"
 8)
 9
10type TodoMapperHelper interface {
11    ModelToEntity(*pkg00000.TodoModel, *pkg00001.Todo) error
12    EntityToModel(*pkg00001.Todo, *pkg00000.TodoModel) error
13}
14
15type TodoMapper interface {
16    ModelToEntity(*pkg00000.TodoModel) (*pkg00001.Todo, error)
17    EntityToModel(*pkg00001.Todo) (*pkg00000.TodoModel, error)
18}
19
20// ... (TodoMapper default implementation)

Goでの構造体のマッピング、結構どうにかしたいと思いつつとりあえず全部手でやってました、という人も多いと思うのでもしよければ使ってみてください。


GoでCommonMarkのパーサを実装しました。

分かりやすいASTに変換+拡張が容易、そこそこ速い実装になっています。

めちゃくちゃしんどかったです。

経緯

Go言語のMarkdownパーサといえばblackfridayですが、 拡張するための機構がないのでさくっと自前でMarkdownパーサを書くか、と思い立ちました。

そこで「そういえばCommonMarkなんてもんがあったな」と思い出しせっかくならCommonMark準拠にするかとおもってCommonMarkの仕様を読み始めました。

え、なにこれは…

Markdownで出来ることなんてrestructuredTextなどほかのマークアップ言語に比べればわずかなものです。しかし、たかがそれだけを実装するために凄まじく複雑な仕様が定義されているのでした。

以下、CommonMarkに寄せられた声です。

しまいには、CommonMarkの中心人物自体が「えらいもん作ってしまった・・・」となっています。

There are 17 principles governing emphasis , for example, and these rules still leave cases undecided. The rules for list items and HTML blocks are also very complex. All of these rules lead to unexpected results sometimes, and they make writing a parser for CommonMark a complex affair. I despair, at times, of getting to a spec that is worth calling 1.0.

私も仕様を読んだ段階で「これはヤバい」と感じたのですがここで引くのも悔しかったので作り切りました。

CommonMarkについて思うところ

正直なところ、これが世に広まるってどうなの、と思います。

そもそもなんでMarkdownが軽量マークアップ言語の中でこれだけ広く使われるようになったのかというと

  • 書き手としての書きやすさ
  • 実装者としての実装しやすさ

つまり、「書き手の適当さ」と「実装側の適当さ」がいい具合に噛み合ったからだと思っています。 書き手はそれなりに適当に書けるし、実装するにしても仕様が適当なので、適当に実装してもMarkdown 対応であると言うことができました。

それによってあらゆる言語、さまざまなタイプの実装が生まれそれが至るところで使われることになり Markdownは広まっていったのです。

それに対して、CommonMarkはあまりに実装するのがしんどすぎます。さらにはそれだけしんどい思いをしても出来ること自体は少ないのです。テーブルさえ使えないのです。

その実装の難しさからCommonMark対応のパーサはいわゆるMarkdownパーサと比べるとわずかしかありません。 CommonMarkにかかわっている人の中には「それが何の問題ですか?」という人さえいます。 「CとJSで参照実装提供してるじゃん。ブラウザはC動かないからJSも用意してある。それ以外の言語は C実装のバインディング作ればいいだけでしょ。実装なんて1個あればいいんだよ」なんて感じです。

CommonMarkはオリジナルのMarkdown作者から「Markdownの名前は使うな」と言われてCommonMarkという名前になったという経緯があります。

Markdownを冠していないのであれば、perlの神正規表現で実装されたオリジナルのMarkdown.plの動きにできるだけ寄せようとするのではなく、もっと仕様をシンプルにする方向に動いて欲しかったです。

CommonMarkパーサを実装したい人に向けて

とにかく、CommonMarkに準拠するのは難しい。ということでいるかいないかわからない、CommonMarkパーサをこれから書こうと言う人に気づきを共有しておきます。

  • いわゆるプログラミング言語やXML, JSONなどのパースとは全く別物です。そういうものはもともと 「パースしやすいように」という視点で文法が作られてますが、CommonMarkはそんなことはありません。 pegベースのパーサもあるので無理じゃないですがいわゆるLL,LRやLALRのような方法では厳しいです。
  • Markdownはいうなれば「行志向」なので業単位を基本としましょう。
  • 「lazy continuation」はかなり曲者です。
  • 仕様策定者自身が Beyond Markdown で述べている部分は間違いなくしんどいです。特に強調は自分で考えるのは諦めましょう。素直に参照実装と同じアルゴリズムで実装するより他にspec testを通すのは難しいです。
  • タブが本来のタブの意味でつかわれる点に注意。つまりタブ文字の位置によりタブは 1文字分、2文字分、3文字分、4文字分、いずれの文字幅にもなりえます。
  • とにかく折れない心が大事です。CommonMarkとMarkdownは別物です。覚悟してかかりましょう。

相変わらずメインのマシンはWindowsなのですが、batファイルのもろもろがいつまでも覚えられず、bashスクリプトで書きたいなあ、ということでbatファイルにbashスクリプトを埋め込むことにしました。 Cでつくった自作用ツールがあるため、Windowsにはかならずmsys2をいれているのでbashは絶対あるんですよね。

その他の言語を埋め込む方法は

が詳しいです。

シェルスクリプトをバッチファイルに埋め込む

以下のようにすると、バッチファイルにシェルスクリプト(bashスクリプト)を埋め込むことにできます。拡張子 .bat で保存すると cmd.exe からもmsys2の bash.exe からも起動できます。

1:rem () { <<'#__CO__'
2@bash  "%~f0" %* & exit /b
3#__CO__
4}
5
6echo 以下普通にシェルスクリプト

仕組みはバッチファイルのコメントやら、bashのヒアドキュメントやら、: コマンドやらをうまく利用して埋め込んでいます。