2020/04/12


僕はよく C++ 用の SQLite3 ライブラリを探し歩いていて、見つけるたびに「1行掲示板」を実装してみている。

ウェブサーバは crow というライブラリを使い、SQLite3 の部分だけ差し替えて試すという感じ。初代は MySQL 用に作った。

GitHub - mattn/crow-bbs
https://github.com/mattn/crow-bbs

これを SQLite3 向けに直したのが crow-bbs-sqlite3

GitHub - mattn/crow-bbs-sqlite3
https://github.com/mattn/crow-bbs-sqlite3

さらにそれを sqlpp11 向けに直したのが crow-bbs-sqlite3-sqlpp11

GitHub - mattn/crow-bbs-sqlite3-sqlpp11
https://github.com/mattn/crow-bbs-sqlite3-sqlpp11

sqlpp11 は SQLite3 専用という訳ではなく、connector を使って異なる RDBMS も扱える。

先日、C++ 用 SQLite3 ORM の sqlite_orm を見つけたのでこれも試してみた。

GitHub - fnc12/sqlite_orm: ❤️ SQLite ORM light header only library for modern C++

// SELECT AVG(id) FROM users auto averageId = storage.avg(&User::id); cout << " averageId = " << ave...

https://github.com/fnc12/sqlite_orm

作った物はこちら。

GitHub - mattn/crow-bbs-sqlite3-sqlite_orm
https://github.com/mattn/crow-bbs-sqlite3-sqlite_orm

sqlite_orm は SQLite3 専用ではあるものの、sqlite3 ネイティブライブラリの様に SQLite3 色が濃くなく、sqlpp11 の様に SQL も登場しない。初回のスキーマ構築で struct との bind を行う。

struct Post {
  int id;
  std::string text;
  std::string created;
};
auto storage = sqlite_orm::make_storage("bbs.db",
    sqlite_orm::make_table("bbs",
      sqlite_orm::make_column("id", &Post::id, sqlite_orm::autoincrement(), sqlite_orm::primary_key()),
      sqlite_orm::make_column("text", &Post::text),
      sqlite_orm::make_column("created", &Post::created)
));
storage.sync_schema();

全ての Post を得るのであれば1行で出来る。

auto posts = storage.get_all<Post>();

追加も簡単

Post post = {.text = q, .created = storage.select(sqlite_orm::datetime("now""localtime")).front()};
storage.insert(post);

1行掲示板くらいのアプリであればとても小さく書ける。

#include <memory>
#include <stdexcept>
#include "crow_all.h"
#include <sqlite_orm/sqlite_orm.h>

struct Post {
  int id;
  std::string text;
  std::string created;
};

int
main() {
  auto storage = sqlite_orm::make_storage("bbs.db",
      sqlite_orm::make_table("bbs",
        sqlite_orm::make_column("id", &Post::id, sqlite_orm::autoincrement(), sqlite_orm::primary_key()),
        sqlite_orm::make_column("text", &Post::text),
        sqlite_orm::make_column("created", &Post::created)
  ));
  storage.sync_schema();
  crow::SimpleApp app;
  crow::mustache::set_base(".");

  CROW_ROUTE(app, "/")
  ([&] {
    crow::mustache::context ctx;
    auto posts = storage.get_all<Post>();
    int n = 0;
    for(auto &post : posts) {
      ctx["posts"][n]["id"] = post.id;
      ctx["posts"][n]["text"] = post.text;
      ctx["posts"][n]["created"] = post.created;
      n++;
    }
    return crow::mustache::load("bbs.html").render(ctx);
  });

  CROW_ROUTE(app, "/post").methods("POST"_method)
  ([&](const crow::request& req, crow::response& res) {
    crow::query_string params(std::string("?") + req.body);
    char* q = params.get("text");
    if (q == nullptr) {
      res = crow::response(400);
      res.write("bad request");
      res.end();
      return;
    }

    Post post = {.text = q, .created = storage.select(sqlite_orm::datetime("now""localtime")).front()};
    storage.insert(post);
    res = crow::response(302);
    res.set_header("Location""/");
    res.end();
  });

  app.port(40081)
    //.multithreaded()
    .run();
}

ちょっと触ってみた感じだと SQLite3 を扱うのであれば一番便利かもしれない。

Posted at by



2020/04/05


1週間ほど前の深夜、ふと Go で連結リスト構造を書いていたら次第に car/cdr 形式になってしまい、気付いたら手が滑って Lisp 処理系を作り始めてしまいました。

初日は深夜だったのでパーサを書いた所で終了。次の日の夕方には四則演算と FizzBuzz が動きました。実は Lisp 処理系を書くのは人生でたぶん4回目くらいで、前回はC言語で書きました。

GitHub - mattn/cisp: Minimal Lisp Interpreter
https://github.com/mattn/cisp

今回のルールとして「過去の自分の実装や他の実装は見ない」というオレオレルールを作ってしまったので幾分時間が掛かってしまった様に思います。テストコードはさすがにいいだろという事で、cisp のテストコードは借りました。マクロを除いて cisp と互換性があります。

今回 Lisp 処理系を書きながらなんとなくやってみたいなと思っていたのが Lisp での非同期処理。Go言語の goroutine と channel を使って通信出来たらどんな物ができるだろう、という研究目的でした。

本日ようやく goroutine/channel が動いたのでブログで公開しておきます。

GitHub - mattn/golisp: Lisp Interpreter
https://github.com/mattn/golisp

(go:make-chan string) で chan を作る事ができ、(go:chan-send ch "foo") で送信、(go:chan-recv ch) で受信する事ができます。また (go (print 1) (print 2)) で goroutine が起動するので、以下の様な channel を使った通信ができます。

(setq time (go:import time))
(let ((ch (go:make-chan string 1)))
    (go
        (.Sleep time 1e9)
        (go:chan-send ch "1")
        (.Sleep time 1e9)
        (go:chan-send ch "2")
        (.Sleep time 1e9)
        (go:chan-send ch "3")
        (.Sleep time 1e9)
        (go:chan-send ch "ダーッ!")
    )
    (print (car (go:chan-recv ch)))
    (print (car (go:chan-recv ch)))
    (print (car (go:chan-recv ch)))
    (print (car (go:chan-recv ch)))
)

研究の成果としては、面白い動きが確認できたのでまずまずの成果と思います。上記の様に Go のパッケージを import して関数やメソッドを呼び出せる様になっています。例えば乱数を表示するには以下の様に実行します。

(setq time (go:import 'time))
(setq rand (go:import 'math/rand))
(.Seed rand (.UnixNano (.Now time)))
(print (.Int rand))

今の所、実用的ではありませんが、細々とメンテナンスしていってツールを書く程度に使えるまでは持っていきたいと思います。

Posted at by



2020/04/04


Go でスライスに挿入する例として Go の Wiki に以下の物が記載されている。

しかしこのコードは、挿入されるスライスから部分スライスを取り出し、そこに挿入するスライスを append に割り当てる為に展開し、さらに残りの部分スライスも append に割り当てる展開を行っている。なので実装コードとしては短いが、実際に実行されるオペレーションコードが冗長になる。また、スライスの伸長はおよそ2倍ずつ増える。なので例えば 5000 個のスライスに 1000 個のアイテムを挿入すると、キャパシティはおよそ 12000 近くになる。Go ではこういったスライスの操作も、基本的には自身で make によりメモリを確保し、ループで値を割り当てるのが良い(面倒かもしれないがその方がメモリも節約され速い)とされている。

以下、ベンチマークを取ってみる。

package main_test

import (
    "reflect"
    "testing"
)

type T int

func BenchmarkAppend(b *testing.B) {
    b.ResetTimer()

    input := make([]int5000)
    for i := 0; i < 5000; i++ {
        input[i] = i
    }
    insert := make([]int1000)
    for i := 0; i < 1000; i++ {
        insert[i] = i
    }
    expected := make([]int6000)
    for i := 0; i < 6000; i++ {
        if i < 1000 {
            expected[i] = i
        } else if i < 2000 {
            expected[i] = i - 1000
        } else {
            expected[i] = i - 1000
        }
    }
    n := 1000
    var output []int

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        output = append(input[:n], append(insert, input[n:]...)...)
    }
    b.StopTimer()
    if !reflect.DeepEqual(output, expected) {
        b.Fatal("bad insertion")
    }
}

func BenchmarkNormal(b *testing.B) {
    input := make([]int5000)
    for i := 0; i < 5000; i++ {
        input[i] = i
    }
    insert := make([]int1000)
    for i := 0; i < 1000; i++ {
        insert[i] = i
    }
    expected := make([]int6000)
    for i := 0; i < 6000; i++ {
        if i < 1000 {
            expected[i] = i
        } else if i < 2000 {
            expected[i] = i - 1000
        } else {
            expected[i] = i - 1000
        }
    }
    n := 1000
    var output []int

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        output = make([]intlen(input)+len(insert))
        for l := 0; l < n; l++ {
            output[l] = input[l]
        }
        for l := 0; l < len(insert); l++ {
            output[l+n] = insert[l]
        }
        for l := n; l < len(input); l++ {
            output[l+len(insert)] = input[l]
        }
    }
    b.StopTimer()
    if !reflect.DeepEqual(output, expected) {
        b.Fatal("bad insertion")
    }
}

func BenchmarkCopy(b *testing.B) {
    input := make([]int5000)
    for i := 0; i < 5000; i++ {
        input[i] = i
    }
    insert := make([]int1000)
    for i := 0; i < 1000; i++ {
        insert[i] = i
    }
    expected := make([]int6000)
    for i := 0; i < 6000; i++ {
        if i < 1000 {
            expected[i] = i
        } else if i < 2000 {
            expected[i] = i - 1000
        } else {
            expected[i] = i - 1000
        }
    }
    n := 1000
    var output []int

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        output = make([]intlen(input)+len(insert))
        copy(output, input[:n])
        copy(output[n:], insert)
        copy(output[n+len(insert):], input[n:])
    }
    b.StopTimer()
    if !reflect.DeepEqual(output, expected) {
        b.Fatal("bad insertion")
    }
}
BenchmarkAppend-8          42700             26185 ns/op
BenchmarkNormal-8          70170             18005 ns/op
BenchmarkCopy-8            90902             15787 ns/op

Wiki に載っているコードは、手動でスライスを挿入するコードの1.5~2倍遅い。copy を使うともう少し速い。

Posted at by