Tonbly開発日記 #1 -開発の背景-

ハロー!

去年の12月くらいから作っているツールが多少形になってきたので開発日記をつけることにしました。

github.com

どんなツールかというと、Qiitaもどきみたいなやつです。
UIは調整しますがこんな感じ

f:id:devilune:20200225231431p:plain f:id:devilune:20200225231520p:plain f:id:devilune:20200225231532p:plain f:id:devilune:20200225231554p:plain

※テストデータはQiitaのAPIを使って放り込んでいます

今のところこんなことができます。

  • 記事の作成、更新、削除
  • 記事の一覧
    • 新着、タグ、ユーザー単位で閲覧可能
  • 記事にコメントを付ける
    • コメントの編集、削除も

こうやって書き出してみると、Railsかなんかのチュートリアルで作れそうな機能しかないですね。
でも違うんです...これから色々実装していくんですぅ...。

で、貴重な人生の時間を費やしてなんでこんなものを作っているかというと、社内ブログなんですよ。社内ブログ。
私はいたって普通の受託開発ベンダーで働いているのですが、今まで社内で情報共有する場所がなくて...というか正確には そういう用途も兼ねているグループウェアを全社で使っているんですが、使いにくいんですよこれが。

コードブロックもシンタックスハイライトもないしタグもないしで、ちょっと技術情報を載せるのは辛いなーと。
書式もMarkdownじゃなくてWYSIWYGエディタで書くタイプなんですけど、これがwうわっぷww辛いんでゲスよwww
いや本当に辛い。挙動が謎すぎる。例えば、一度作成した記事を更新しようとすると箇条書きが機能しなくなることが結構な頻度であるんですよ。
で、しまいには空の箇条書きを文末に残しておくという謎のしぐさが流行始めたりして(結局無駄だったけど)。
このエディタ、タブを切り替えると出力結果のHTMLを見られる&編集できるようになってるんですけど、これは自分でHTML直せということなのか???

怨の念により脱線いたしましたが、とにかく今使えるものがイマイチなので、社内ブログを始めるにあたっては何か違うものを使いたかったのです。
で、まずは既存のツールを(超適当に)探したんですが、見つかったのはこのへんでした。

ここから選定していくわけですが、まず有償のものはNGです。
だって稟議通すの苦手だし、通ったとしても費用が発生している以上色々面倒だもの。
そうすると選択肢は残りの無償ツールになるわけですが、うーん...どれもいまいち!(暴言)
他にもいくつか見ましたが、残念ながら私の希望に沿うものはありませんでした。

私の希望

  • UIがキレイでシンプルでスッキリしててオシャレである
  • Markdownで書ける
  • 無償 or OSSでセルフホストできる
    • セルフホストの場合はDockerイメージがあると尚可
  • 投稿とかメンションのタイミングでWebhookを飛ばせる
  • いいね!できる
  • SPAである
  • SAMLでSSOできる

まあ他にも色々あるんですが、とにかくUIの時点でシックリくるものがありませんでした。大事じゃないですか。見た目。
どうもどれもモッサリ&ゴチャゴチャしてるんですよね...。

ということで自分で作ることにしました!というのが背景です。
ただ、大変正直に申し上げると、最終的に自分で作ることになるんだろうな...というか作りたいな...と始めから思っていたのでロクに調べてません。
良いツールがあったらぜひ教えてくださいね。

あと、なんかの間違いでヒットしてGitHubにたくさん星がついて承認欲求満漢全席コースというスケベ心もあります。もちろん。

本当は技術スタックについても書くつもりだったのですが、怨嗟の波動で疲れたので次回にします...。

Go言語でつくるインタプリタを読んだ

こちらの本を読んだので感想をば www.oreilly.co.jp

Scalaに移植しました github.com

結論

  • 言語自作に関する前提知識無しでインタプリタ実装を学べる良書
  • プログラミング言語を作ってみたいけど何から…という人には是非オススメ
  • Goがわからなくても大丈夫

本の簡単な説明

Go言語を使って簡単な仮想言語のインタプリタを作っていこうぜ!という本です。

本の構成はこのようになっています。

  1. 字句解析器の実装(Lexer)
  2. 構文解析器の実装(Parser)
  3. 評価器の実装(Evaluator)

ソースを読んでからその内容が実行されるまでのステップを一つずつ実装していくようになっていて、この手の内容では一般的な構成なのかなと思います。

低レイヤを知りたい人のためのCコンパイラ作成入門にも書かれていますが、この構成だと最後の最後までプログラムを実行できないので道中つまらないのでは?と思うかもしれません。
しかしこの本では序盤でREPLを実装し、例えば入力した文字の字句解析結果をコンソールで確認したり、都度のステップで動かして遊べるようになっていますので、あまり心配する必要はないと思います。

その他の特徴としては、TDD(テスト駆動開発)で進めていくのでテストをたくさん書きます。
わりとテストコードの量が多くて正直面倒なんですが、のちのち拡張するときにこれが効いてきます(後述)。

最終的にインタプリタが完成すると、こんなプログラムを実行できるようになります!

# 変数束縛
let age = 1;
let name = "Monkey";
let result = 10 * (20 / 2);

# 配列
let myArray = [1, 2, 3, 4, 5];

# ハッシュ
let thorsten = {"name": "Thorsten", "age": 28};

# 関数
let add = fn(a, b) { return a + b; };
add(1, 2);

# 再帰
let fibonacci = fn(x) {
  if (x == 0) {
    0
  } else {
    if (x == 1) {
      1
    } else {
      fibonacci(x - 1) + fibonacci(x - 2);
    }
  }
};

# 高階関数
let twice = fn(f, x) {
  return f(f(x));
};

let addTwo = fn(x) {
  return x + 2;
};

twice(addTwo, 2); // => 6

抜粋:: Thorsten Ball “Go言語でつくるインタプリタ

よかったところ

翻訳が素晴らしい

これは本当によかった。
翻訳ものにありがちなぎこちなさが皆無で、下手な和書より全然読みやすいです。
すべての訳書がこのレベルだと非常にありがたいですね...。

文体がフランクで読みやすい

これは好みが分かれそうですが、例えば「〇〇の実装はこんな感じだ。 簡単そうだろう? 実際そうなんだ。」みたいな感じで全体的にそういうノリで書いてあります。
原著がそんな感じで、それを丁寧に再現しているのかなーと思います。
"読みやすい"というよりは"親しみやすい"、かも。
ターゲットにうまくマッチしている印象。

難しい理論が出てこない

私は情報工学を学んだこともなければ、(忘れてしまったので)中学の数学さえ怪しい人間ですが、順を追って進めていけばまったく問題なく理解できました。
何も知らない人向けに書いてあるようで、初見でハア?となりそうな用語や理論は一切出てきません。
その代わりに、

  • 今からどんな機能を追加するのか
  • そのためには何をしなくてはいけないのか、その理由

が丁寧に書いてあります。

あと、ここはちょっと大変かも、とかここは実はめっちゃ簡単やで!みたいなのがちょこちょこ書いてあるのもやさしい世界。

Goを選択した筆者の選球眼

私はほとんどGoを触ったことがありませんが*1JavaなりJavaScriptなり何かしらの言語をやっていればなんとなく読めると思います。
唯一、型アサーションだけは調べるまで確信が持てませんでしたがその程度です。

そういった意味で、少なくとも技術書で使われる言語としてGoは良い選択肢だと思います。
テーブルドリブンテストも便利!

作ったインタプリタが拡張しやすい

前述の通りテストがきっちり書かれているので、拡張の過程でぶっ壊してもすぐわかります。
テスト自体の拡張も容易で、TableDrivenTestsで書いてある既存のテストにパターンを追加するか、真似て新しいテストを書いていくだけです。
(テストが多いぶんScalaに移植するとき心が折れそうになりましたけどねハハハ)

テストの例(OCamlのようなlet文のパースをテストするケース)
こんな感じでinputとexpectedを定義して、このパターンを使って実装を検証していきます。

tests := []struct {
    input              string
    expectedIdentifier string
    expectedValue      interface{}
}{
    {"let x = 5;", "x", 5},
    {"let y = true;", "y", true},
    {"let foobar = y;", "foobar", "y"},
}

おもちゃではなく、わりとちゃんとした言語ができる

冒頭のコード例を見ていただければわかるかと思いますが、文字列をprintできまーすとか、四則演算ができまーすみたいな単機能の言語ではなく、いちおう最低限の機能が実装されています。
最終的にここまでできるのならばやる気も出るというものです。

これはこの本を執筆した動機の一つのようで、導入の部分でも下記のように書かれています。
(引用して気づきましたが脱字ありますね)

私が欲しかったのは、900ページにも及ぶコンパイラについて書籍と、50行のRubyコードでLispインタプリタを実装する方法に関するブログ記事との間にあるようなものだ。

抜粋:: Thorsten Ball “Go言語でつくるインタプリタ

ちなみに、インタプリタが完成すると組み込み関数を使ってMonkey上でmapやreduceを実装できるようにもなります。

本の構成がイテレーティブ

イテレーティブって言いたかっただけです。
TDDなのでまあそうなのかなと思いますが、どの章も以下のような流れになっていて、一定のリズムで読み進める&手を動かしていくことができます。

  1. 今から実装する機能の概要説明(識別子って何?とか)
  2. テスト書く -> 失敗
  3. 実装の説明&実装
  4. テスト成功

これ、読み進めていくうちに流れが体に刻みこれていくのがわかります。

プログラミング言語って、結局式と文の集合でできてるだけじゃん!

というのがわかって非常におもしろかった。
式と文についてもちゃんと説明してくれます。
一見難しそうな機能でも、実はこれただの式だからさ!前の章で作った機能を使って簡単に処理できるんだ!みたいなアハ体験?の連続で、いやー本当にうまくできてますね...という感じ。

最後がいい

普通、最初は四則演算とかHello Worldの実現から始めるような気がしますが、なんとこの本は一番最後の"グランドフィナーレ"という章でやっとHello World(というか文字列の出力)を実装します。

3章では、Monkeyプログラミング言語に生を与えた。呼吸を始めたんだ。
そして、私たちの最後の変更で、声を与えた。そう、Monkeyはついに、本物のプログラミング言語になった。

抜粋:: Thorsten Ball “Go言語でつくるインタプリタ

>> puts("Hello World!")
Hello World!

Go言語でつくるインタプリタ 〜完〜

良質な物語か何かかな???

わるかったところ

ほとんどなかったのと面倒なので略(省エネ)
唯一、クロージャの実装で出てくるEnvironmentについては図が欲しかったかも。

どうやって進めたか

まず、コードの写経は一切やらずに全てコピペしました。
これは私の特性で、写経しても何も身につかないからです。
(できたものを拡張したりいじくりまわすときに初めて頭に入ってくるタイプの人間なのです...)
もちろん文章とコードを読んで理解しなが進めていきますが、あとでガチャガチャやるときに頭に定着させればいーやーという感じで進めました。

この前提で普通に頭から読み進めていきましたが、この本のキモであるPratt構文解析の部分はちょっと複雑だったので、そこだけ謎DSLをテキストに書きながら理解していきました。

晒すのが恥ずかしいですが、こんな感じです
(他人が見ても意味不明だと思いますが)

parseExpressionStatement<1, +>
  ★parseExpression<1, +>(LOWEST) 1を消化 for(LOWEST<SUM) -> peekFn(+) next(+)
    parseInfixExpression<+, 2>(left(1)) next(2)
      ★parseExpression<2, *>(SUM) 2を消化 for(SUM<PRODUCT) -> peekFn(*) next(*)
        parseInfixExpression<*, 3>(left(2)) next(3)
          ★parseExpression<3, ;>(PRODUCT) 3を消化 !for(!; or PRODUCT<LOWEST) RETURN 3
        <3, ;> expression.Right=3 RETURN expression #expression(left:2, op:*, right: 3)
      <3, ;> RETURN expression #expression(left:2, op:*, right: 3)
    <3, ;> RETURN expression #expression(left:1, right(expression(left:2, op:*, right: 3)))
  <3, ;> !for(!;) RETURN expression #expression(left:1, right(expression(left:2, op:*, right: 3)))
expression(left:1, right(expression(left:2, op:*, right: 3)))

ここが難しいのはもちろん筆者も理解していて、いい感じにログを出せるようになるコードが掲載されています。
ただ、それだと情報が足りなかったので自作した次第です。

エディタはVisual Studio Codeを使いましたが、まったく問題なく快適に進められました。
(まあほとんどコピペするだけなんですが)

最後に

冒頭にも書きましたが、プログラミング言語を作ってみたいけどスキルや前提知識に不安が...という人にはうってつけだと思います。
この後は、機能を追加するなりコンパイラを作るなり無限に広げていけるはず。
続編としてGoでコンパイラを作る本(未翻訳)が出ているので、いずれそちらも読んでみたいですね。

また、言語自作に限らずとも、この本で得た知識は例えば何らかの独自形式設定ファイルを解析するとかそういうシーンでも役に立つと思います。
(私、仕事では基幹系システムの受託開発をやっているのですが、たまにそういう"案件"があるんですよね...。たいてい地獄を見るけどね★

ちなみに現在はScala移植版をベースに自作言語をシコシコ作っています。
まだほとんどMonkeyのままですが、色々いじっていきたい所存です。

github.com

*1:数年前にゴミみたいなCLIツールを作ったことはあるが、もはやほとんど覚えていない

Alpine Linuxの破壊的変更でAsciiDocのGitLab CIがぶっ壊れた話

まとめ

  • ある日突然、Dockerのasciidoctor/docker-asciidoctorを使ってAsciiDoc文書の変換と成果物(HTML)のアップロードを行っているGitLab CIのジョブが落ちるようになった
  • 原因は、アップロードに使っていたftpputコマンドがAlpine Linuxのバージョン3.7からbusybox-extrasというパッケージに切り出されていたため
    (docker-asciidoctorはAlpine Linuxベース)
  • 特にDockerイメージのバージョンは指定していないので、自動でバージョンが上がって壊れた
  • busybox-extrasをインストールすればすぐ直る

無慈悲なエラーの図

/builds/XXXX/XXXX/documents/build.sh: line 43: ftpput: not found

修正方法

以下の手順でbusybox-extrasをインストールすればOK!

# busyboxを更新
$ apk add -u busybox
# busybox-extrasをインストール
apk add busybox-extras

ちなみに、busyboxを更新しないと以下のようなエラーになる。

ERROR: unsatisfiable constraints:
  busybox-extras (missing):
    required by: world[busybox-extras]

ちなみにちなみに、.gitlab-ci.ymlはこんな感じになる。

image: asciidoctor/docker-asciidoctor

build:
  stage: build
  script:
    - apk update
    - apk add -u busybox
    - apk add busybox-extras
    - sh ${CI_PROJECT_DIR}/build.sh

busybox-extrasに切り出されたコマンドの一覧

※正確性は保証しません

  • /usr/bin/telnet
  • /usr/sbin/httpd
  • /usr/bin/ftpget
  • /usr/bin/ftpput
  • /usr/sbin/ftpd
  • /usr/bin/tftp
  • /usr/sbin/fakeidentd
  • /usr/sbin/dnsd
  • /usr/sbin/inetd
  • /usr/sbin/udhcpd

結論

AsciiDocとGitLab CI死ぬほど便利!みんな使おう!!

ScalaでArray(1, 2, 3, 1).groupBy(i =&gt; i)みたいに書いてる場合はidentityを使うと良い

// (1) これは...
Array(1, 2, 3, 1).groupBy(i => i)

// (2) こう書ける(意味は同じだけどスッキリ!)
Array(1, 2, 3, 1).groupBy(identity)

最初は↓のように書いていたのですが、コンパイルが通らず、結局上記(1)のように書いていました。

Array(1, 2, 3, 1).groupBy(_)

よくよく考えると関数が期待されている部分に値をそのまま渡しているので当然ですね。
うーんでもこの書き方は冗長だなあ...と思って調べていたら

Scala 異なるものを検索する実験(Hishidama's Scala different search Memo)

神のブログに解決策が載っていました!
しかもまったく同じ間違い方してるw

Scalaに標準で用意されているidentityという関数は渡された引数をそのまま返す関数なのでこのようなケースで使えるというわけですね。

普段仕事で使ってるC#にもこういうのないのかな?と思ったんですが、自分で作ってstatic importすれば同じように使えそうですね。
ということで作ってみました。

namespace DotnetPlayground
{
    public static class Predef
    {
        public static T Identity<T>(T p)
        {
            return p;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;

// static importしておく
using static DotnetPlayground.Predef;

namespace DotnetPlayground
{
    class Program
    {
        static void Main(string[] args)
        {
            var ints = new List<int> { 1, 2, 3, 1 };
            
            var grouped = ints
                .GroupBy(Identity) // Scalaと同じように書ける!
                .ToList();

            var result = grouped.Select(i => $"{i.Key}: {i.Count()}");
            Console.WriteLine(string.Join("\n", result));
            
            /*
               Result
               1: 2
               2: 1
               3: 1
             */
        }
    }
}