Syntax highlighter

2019-07-08

ベンチマークしてみる

こんなツイートを見かけた。
個人的にシンボルに変換するのはあり得ないかなと思ってはいるのだが、equal?と再帰はどっちが速いか分からない(特に Sagittarius では C 側で実装されているし)。ということでベンチマークを取ってみた。単純な比較でいいかなぁと思ったので、スクリプトは以下のようなものにした。
#!r6rs
(import (rnrs)
        (bench time))

(define string (let-values (((o e) (open-string-output-port)))
                 (do ((i 0 (+ i 1)))
                     ((= i 1000) (e))
                   (put-char o (integer->char i)))))
(define char-list0 (string->list string))
(define char-list1 (string->list string))

(define (->symbol cl)
  (let-values (((o e) (open-string-output-port)))
    (put-datum o cl)
    (string->symbol (e))))

(benchmark 1000 #t (lambda () (equal? char-list0 char-list1)))
(benchmark 1000 #t (lambda ()
                     (let loop ((cl0 char-list0) (cl1 char-list1))
                       (cond ((and (null? cl0) (null? cl1)))
                             ((or (null? cl0) (null? cl1)) #f)
                             ((char=? (car cl0) (car cl1))
                              (loop (cdr cl0) (cdr cl1)))
                             (else #f)))))
(benchmark 1000 #t
           (lambda () (eq? (->symbol char-list0) (->symbol char-list1))))
(bench time)はこんな感じ(Sagittarius 用)
#!r6rs
(library (bench time)
    (export benchmark)
    (import (rnrs)
            (time))
(define (benchmark count expected thunk)
  (define (do-benchmark count expected thunk)
    (do ((i 0 (+ i 1))) ((= i count))
      (unless (equal? expected (thunk)) (error 'benchmark "invalid result"))))
  (time (do-benchmark count expected thunk)))
)
Chez 用も大体似たようなもの。以下が結果。
$ scheme-env run chez@v9.5 --loadpath=. --program bench.scm
(time (do-benchmark count ...))
    3 collections
    0.024401110s elapsed cpu time, including 0.000328248s collecting
    0.024408000s elapsed real time, including 0.000335000s collecting
    25477792 bytes allocated, including 25192304 bytes reclaimed
(time (do-benchmark count ...))
    no collections
    0.002436587s elapsed cpu time
    0.002442000s elapsed real time
    0 bytes allocated
(time (do-benchmark count ...))
    29 collections
    0.144383753s elapsed cpu time, including 0.000803779s collecting
    0.144402000s elapsed real time, including 0.000838000s collecting
    249044288 bytes allocated, including 244363280 bytes reclaimed

$ sash -L. bench.scm

;;  (do-benchmark count expected thunk)
;;  0.111818 real    0.213437 user    0.025128 sys

;;  (do-benchmark count expected thunk)
;;  0.037333 real    0.037329 user    4.0e-600 sys

;;  (do-benchmark count expected thunk)
;;  0.191468 real    0.268644 user    0.019184 sys
以外にも再帰が一番速いっぽい。Chez でやってもそうならまぁそうだろう的な適当な意見だけど。予想通りシンボルにするのは遅い。->symbolを見ればわかると思うが、普通にオーバーヘッドが大きいというのがある。メモ化するとかすれば何とかなるかもしれないが、equal? でハッシュテーブルを作ったら意味ないだろうし、あまりいい実装が思い浮かばなかったので省略している。

特にまとめはない。

2019-05-24

R6RS ライブラリ周りの解説

ツイッターで R6RS のライブラリ解決の挙動について言及している呟きを見かけたので、ちょっと書いてみることにした。あくまで Sagittarius 内部ではこうしているというだけで、これが唯一解ではないことに注意。

R6RS 的なライブラリファイルの位置付け

R6RS 的にはライブラリはファイルである必要はない。ライブラリは式の定義が書かれていると記載されているだけで特にそれがどこにあるかということには言及していないからだ。これを都合よく解釈すれば、処理系の判断でライブラリを In Memory にしておいても問題ないし、マシン語の塊にしておいても問題ないということである。またライブラリは Top level のプログラムではないので、スクリプトファイルにその定義を置くことは許されていない(っが Sagittarius では利便性のため許していたりする)。

このことから、ライブラリを外部ファイルに置くというのは実は処理系依存の挙動であると言える。言い換えると、R6RS が提供する標準ライブラリ以外を使ったプログラムは処理系依存ということになるし、ライブラリ自体を記述することは処理系依存になるということでもある。

要するに R6RS 的にはライブラリという枠組みは用意するけど、それらをどう扱うかは適当に都合よく解釈してねということだ。ある意味 Scheme っぽい。これを踏まえつつ、Sagittarius ではライブラリの解決がどう行われるかを記述していく。

import 句

Top level のプログラムはプログラムの開始に import 句を一つ必ずもつ必要がある。import 句はこの宇宙のどこかにある指定されたライブラリを探し出す必要がある。Sagittarius では探索範囲を load path または In Memory としかつ、必要であればライブラリ名をファイル名に変換して探索する。処理としては
  1. ライブラリ名で読み込み済みライブラリを検索
  2. 見つかったらそれを返す
  3. 見つからなかったら、ライブラリ名をファイル名に変換し、 load path 上を探し load する
  4. 1を試し、見つからなかったらエラー
というようなことをしている。
ライブラリ名の変換はファイルシステムの制約も入ってくるので、シンボルはエスケープしたりしている。

余談だが、load する代わりに read して eval した方がいいような気がする。load だと、ライブラリ以外の式も評価されてしまうというオマケが付くからなのだが、これに依存したコード書いてたか記憶がない…

library

ライブラリは書式以外は処理系依存であるということは上記ですでに触れたので、ここでは de-facto な挙動と完全処理系依存の挙動を記して、どうすればある程度ポータブルにライブラリを記述できるかを示すことにする。

De-facto な挙動

  1. R6RSのライブラリは拡張子 .sls を使う
  2. 特定の処理系のみに読み込んで欲しい場合は .sagittarius.sls の様な .処理系名.sls 形式を使う
  3. ライブラリファイル名は空白をファイルセパレータに置き換える。例 (rnrs base) なら rnrs/base.sls になる。

処理系依存な挙動

  1. load path の指定。処理系ごとに指定の仕方が違う。既定の場所も処理系ごとに違う。
  2. 複数ライブラリを一ファイルに押し込める
    1. 処理系によっては許している(例: Sagittarius)
    2. 基本ポータブルにしたいなら使わないか、処理系依存の処理を押し込めたファイルのみに適用する
  3. meta キーワード
    R6RS で定められているが、処理系ごとに挙動が違う。以下のどちらかになる
    1. 完全無視
    2. フェーズを意識する
    ので、よりポータブルにしたいなら、フェーズを意識して書いた方が良い。

meta キーワード

上記で meta キーワードについて多少触れたので折角なのでもう少し突っ込んでみることにする。
いくつかの処理系、知っている限りでは plt-r6rs と André van Tonder の展開器を使っている処理系だけだが、ではマクロ展開のフェーズ管理を厳密に行う。端的に言えば、マクロ展開時に import された束縛と実行時に import された束縛は別物として扱われるということである。例えば以下のコード
(import (rnrs)
 (only (srfi :1) make-list))

(define-syntax foo
  (lambda (x)
    (define (ntimes i n)  ;; >- expand (meta 1) phase
      (make-list (syntax->datum n) (syntax->datum i)))
    (syntax-case x ()
     ((_ y n)
      (with-syntax (((y* ...) (ntimes #'y #'n)))
        #'(display '(y* ...)))))))
(foo x 5)
フェーズに厳しい処理系だと上記はエラーになるが、フェーズに緩い(または自動でフェーズを検出する)処理系だと上記は(x x x x x) を表示する。これをポータブルに書くには SRFI-1 を import する部分を以下のように書き換える必要がある。
(for (only (srfi :1) make-list) expand)
Sagittarius はフェーズに緩いので、マクロが展開される環境 ≒ 実行時環境になっている(厳密には違うが、処理系自体を弄らない限り意識することなないはず)。

フェーズは低レベルマクロを使用しかつその中で (rnrs) 以外のライブラリを使うもしくは、(meta 2) 以上のコードを書く際に意識する必要がある。(meta 2) はマクロ内で内部マクロを使いかつその内部マクロで別ライブラリの束縛を参照する必要があるので、普通にコードを書いていたらまずお目にかからないだろう。ちなみに、マクロ内で (meta 0) の束縛を参照するのはエラーなので、マクロ内部での内部定義はコピペが横行しやすい。

割とどうでもいいのだが、(rnrs) またはその下にぶら下がっているライブラリ(例: (rnrs base))は (meta 0)(meta 1) で export されているのだが、これを他のライブラリでやる方法は R6RS の範囲では定められていなかったりする。

論旨がまとまらなくなってきたからこの辺で終わり。

2019-04-12

R6RS MongoDB with transaction

I've made a portable R6RS MongoDB library, I think, last year. Now I needed to support transaction so decided to implement it. Since MongoDB 4.0.0, it supports transactions. However, their document doesn't say which command to use or how. The only thing it mentions is I need to set up replica sets.

If you there's no documentation, then what you can do is reverse engineering. So, I've written a couple of scripts to set up MongoDB with transaction and proxy server which observes the commands.

The followings are the scripts I made to investigate:
The first one sets up the server which receives client command and sends to the MongoDB server. It does also dump both requests and responses. The second one sets up the docker network and instances of MongoDB with replica sets and executes the script files with mongo shell. Then the third one prints wire protocol commands in, sort of, a human-readable format.

With this investigation, I've figured out that I need to add lsid, txnNumber, autocommit and startTransaction. Okay, I've never seen them on the document, so I have no idea how these options works, but just followed the example. Then, here comes the transaction support.

How to use
This is an example of a transaction:
#!r6rs
(import (rnrs)
 (mongodb))

(define conn (make-mongodb-connection "localhost" 27017))
(define collection "txn")

(open-mongodb-connection! conn)

;; create the collection a head
(mongodb-database-run-command db `(("create" ,collection)))

(let* ((session (mongodb-session-start conn)) ;; start session
       (db (mongodb-session-database session "test"))) ;; create a database with session
  (guard (e (else (mongodb-session-abort-transaction session)))
    (mongodb-session-start-transaction! session) ;; start transaction
    ;; okay insert documents using usual database procedure
    ;; NB: has to be command, not other procedures...
    (mongodb-database-insert-command db collection
         '#((("id" 1) ("foo" "bar"))))
    (mongodb-database-insert-command db collection
         '#((("id" 2) ("foo" "bar"))))
    ;; and commit
    (mongodb-session-commit-transaction! session))
  ;; session must be end
  (mongodb-session-end! session))

(let* ((db (make-mongodb-database conn "test"))
       (r (mongodb-database-query db collection '())))
  (mongodb-query-result-documents r))
I haven't implement any utilities of transaction related procedures. So at this moment, you need to bare with low-level APIs.

How it works
Maybe you don't want to know, but it's nice to mention. When a session is created, then it also creates a session id. Then the database retrieved from the session adds the session id to query messages (OP_QUERY, not OP_MSG). Once mongodb-session-start-transaction! procedure is called, then it allocates transaction number and after this, the database also adds transaction information.

If the MongoDB server doesn't support the transaction, then the session automatically detects it and doesn't send any session or transaction related command.

And again, I'm not sure if I implemented correctly or not.

Once the official document of the transaction commands is written, I'll come back and review.

2019-04-01

JSON 周りのライブラリ

宝クジが大当たりしました。

四月馬鹿お終い。

Sagittarius は意外にも JSON 周りのライブラリが充実している。開発者である僕の本業が Web 系だからというのも要因の一つだと思う。一昔前の XML みたいな位置に JSON がいるのが大きい。最近書いてるアプリでこれらのライブラリをふんだんに使って我ながら便利な物を書いたものだと感心したので宣伝を兼ねて自慢することにする(これくらいの勢いじゃないとブログ書くネタがないともいう)。

簡単なクエリ
(text json pointer) は簡単な JSON クエリを提供する RFC6901 を実装したもの。対象となる JSON の構造や配列の要素番号が予め分かっている時に使える。こんな感じ
(import (rnrs) (text json pointer) (text json))

(define id-pointer (json-pointer "/id"))
(id-pointer (call-with-input-file "a.json" json-read))
これで JSON オブジェクトが id フィールドを持っていれば引っ張ってくる。id-pointer はクロージャなので再利用可能。

複雑なクエリ
(text json jmespath) は割と複雑なクエリ言語を提供する。前にも紹介記事を書いてるので簡単な使い方はそっちを参照。JSON Pointer では書けないようなクエリを書く際はこっちを使う。例えば、JSON オブジェクトを要素にする配列から name フィールドと description フィールドのみを返すようなクエリはこんな感じで書ける
(import (rnrs) (text json jmespath) (text json))

(define name&desc (jmespath "[].[name, description]"))
(name&desc (call-with-input-file "b.json" json-read))
;; -> (("name of a object" "description of a object") ...)
これ以外にも便利な使い方や、組み込みのクエリー関数があって便利。

変更
(text json patch) は RFC6902 JSON Patch を提供する。他言語での実装と違うのは入力を変更しない点。関数型とかそう言うのではなく、副作用で実装するのは無理ゲー(と言うか不可能)だったからと言うのが真実。こんな感じで使う
(import (rnrs) (text json patch) (text json))

(define id-patcher (json-patcher '(#(("op" . "add) ("path" . "/id") ("value" . 1234)))))
(id-patcher (call-with-input-file "c.json" json-read))
;; -> #(("id" . 1234) ...)
id-patcher はクロージャなので再利用可能。

与太話
これらのライブラリは Scheme に於けるベクタ操作の貧弱さに辟易したので開発されたとも言える。Sagittarius でデファクトとして使っている JSON の S式表現は元々 Chicken Scheme にあった実装を持ってきている。理由は何故かは知らないが、これが JSON オブジェクトをベクタで表していたのが事の発端とも言える。これらのライブラリは元の表現が普通に  alist でやられていたらきっと産まれなかっただろうので、人間万事塞翁が馬みたいな気持ちになるなる(何度変えてやろうかと呪ったか分からん…)
結果を見ればこの上なく便利なライブラリが出来上がったとも言えるのであの時のどす黒い感情はうまく浄化されたのだろう。ということにしておく。

2019-03-15

R7RS-large タンジェリン

タンジェリンってみかんじゃないのか…

2月にR7RS-largeのタンジェリンエディションがでた。ブログ記事を書こうとずっと思っていたのだが、所謂「life gets in」な状態だったので中々時間も取れずズルズルと一月以上経ってしまった(言い訳)。前回のR7RS-largeはレッドエディションだったのだが、レッドの次はオレンジなのに、いきなりタンジェリンになった。理由はこの辺(要するに準備できなかったらしい)。

さて、前置きが長いとダレるのでまずは結果。温州みかんになれたSRFIは以下:
  • SRFI 115 (combinator-based regular expressions) - (scheme regex)
  • SRFI 141 (comprehensive integer division operators) - (scheme division)
  • SRFI 143 (fixnum operators) - (scheme fixnum)
  • SRFI 144 (flonum operators, R6RS plus ) - (scheme flonum)
  • SRFI 146 (persistent tree and hash mappings) - (scheme mapping) (scheme mapping hash)
  • SRFI 151 (comprehensive bitwise operations on integers) - (scheme bitwise)
  • SRFI 158 (backward-compatible additions to SRFI 127 on generators) - (scheme generator) : 既存の置き換え
  • SRFI 159 (combinator formatting) - (scheme format)
  • SRFI 160 (comprehensive homogeneous vector library, including inexact-complex vectors) - (scheme vector @)
(scheme vector @)@ の部分には u8, s8, u16, s16, u32, s32, u64, s64, f32, f64, c64, c128 が入る。(こいつらを vector と呼ぶと混乱する気がするがいいのかね?) 現在のHEADでSagittariusはSRFI 159とSRFI 160を除く全てをサポートした。除外しているSRFIについては記事の最後に理由(愚痴?)を書く。

この辺りのエディションから比較的新しいSRFIがR7RS-largeに取り入れられるようになるのかな?オレンジが何を入れようとしているのかよく分かっていないのであくまで個人的な感想である。そうは言っても、SRFI 143、144、151はR6RSの拡張みたいなものなので、R7RS-largeでも必要とされたと思えばいいのかもしれない。SRFI 158は多少毛色が違うというか、こうする事で既に決定したライブラリを拡張できるというのを示したとも言えるかもしれない。一度決定したらずっとそのまま言われるよりは柔軟でいい。

SRFI 115は古めのSRFIではあるのだが、どれくらいの処理系がサポートしているのかよく分かっていない。Chibi(参照実装元)とSagittariusはサポートしているが、他にあるのかな?(まぁ、そんな事言い出したら今回入ったSRFI自体どれくらいの処理系がサポートしているのやら…)

SRFI 146は個人的に好きでなかったのだが、この機会にサポートすることにした。ライブラリ名の規則が分かりづらいというのが主な理由だったのだが、いまだに妙な気分ではある。

新たに8つのライブラリが追加されたR7RS-largeだけど、どの処理系が追随するかはよく分かっていない。Chibiは次のリリースでタンジェリンをサポートするらしい。Sagittariusは上記の通り、SRFI 159とSRFI 160を除くR7RS-largeをサポートする。(ひょっとしたらSRFI 160もサポートするかもしれないが、今の所その予定はない。理由は愚痴から推測して)

ここから愚痴。
SRFI 159とSRFI 160は正直微妙だなぁと思っている。SRFI 159はよりSchemeっぽいformatを提供するライブラリなのだが、正直formatの方がいいなぁと思ったり。使い慣れてるし。さらにはR7RS-largeに入ったことでSRFIの議論が再開されたりしていて、なんか泥沼感が出ているし。
SRFI 160に関してはそもそもファイナルにすらなっていない。つまり、議論の途中なのに議長権限でリストに入れたとも言える。鶴の一声、鳴り物入りとか言えばいいのかもしれないが、どうにも唸り声が出てしまう。

2019-01-02

(My) best practice of conditions

I've been writing portable/non-portable Scheme libraries for a rather long time and kind of getting my best practice of how to make conditions for libraries. So let me share the opinion.

Disclaimer

This is not community approved best practice nor it can be applied to the latest standard (R7RS). So I don't have any responsibility for your implementation can't handle or claims that nobody is writing this type of code :p

Basics

Condition system

R6RS has a very nice, (yet it was very controversial), the feature called condition system. It is built on top of, again very controversial, record type system. The basic ideas are:
  1. Conditions are inheritable (the absolute base is &condition)
  2. Conditions are compoundable. (using condition procedure)
These 2 concepts make the condition system very beautiful compare with, say, Java's exception.

How to use

The very basic usage is like this:
(define-condition-type &foo &error
  make-foo-error foo-error?)
This defines a condition type of &foo. Then you can signal the condition with raise or raise-continuable procedure.

My practices

Currently, I'm using conditions with the following rules, which I think the best at this moment.
  1. Should create at least one library specific condition
    If you are creating a library named foo, then the library should have a specific condition such as &foo unless the library provides only utilities. (e.g. (srfi :1) provides only list utilities).
  2. Must not use the error procedure
    If you see the standard error, then it's a bad sign. The standard conditions are good for general purposes but not good for a library specific error signalling.
  3. Should split conditions per phases
    If a library has several phases, then the conditions should be split. For example, (text json jmespath) has compilation and evaluation phases. So the condition should be split into 2, one for compilation time, the other one for evaluation time.
  4. Must not put too many fields
    Conditions are records, thus users can put as many as fields onto it. A condition must contain minimum information or resources. If you need more information, then use &irritants
  5. Should use composite then inheritance
    Conditions are records (I'm saying it twice because it's important), means you can only inherit one base condition. However, sometimes you want to put meta information such as &i/o. In this case use the condition procedure instead of creating a new condition type which inherits &i/o.
    For example, suppose you have &foo condition, which inherits &error. Now your library should also signale &i/o when an I/O operation failed. Theoretically, you have the following 2 options:
    1. Composite &i/o instance
    2. Create a new condition type which inherits &i/o
    As long as your base condition is not a subtype of &i/o, then you should use option number 1. In this manner, library users can handle the error situation easier by just adding a guard clause with foo-error? (suppose your condition predicate of &foo is foo-error?). And users can still check I/O error with i/o-error?

Conclusion

I'm quite happy with the above rules whenever I use the libraries constructed with it (sort of dogfooding).

2018-12-20

(usocket): R6RS portable socket library

Motivation

When I wrote the MongoDB client library in R6RS portable manner, I have included socket procedures inside of the library. Now, I want to write Redis client, then I've noticed that it's very inconvenient that if there's no R6RS portable socket library. We have SRFI 106 for a socket library, however, it's not widely implemented especially on R6RS implementations. So I've decided to make it and it's here.

Portable R6RS socket library

Example

The library supports TCP and UDP client and server sockets. Most of the time, I only need TCP client, but it's nice to have in case it's needed. The very basic HTTP request call using this library would look like this:
(import (rnrs)
        (usocket))

(define client (make-tcp-client-usocket "google.com" "80"))
(put-bytevector 
 (client-usocket-output-port client)
  (string->utf8 "GET / HTTP/1.1\r\n\r\n"))

(utf8->string (get-bytevector-n (client-usocket-input-port client) 1024))
;; -> response from the server

(usocket-shutdown! client *usocket:shutdown-read&write*)
(usocket-close! client)
For portability, we don't provide any socket specific operation other than shutdown and close. Everything else needs to be done via input or output port.

Supporting implementations

The library supports the following implementation:
  • Sagittarius (0.9.4 or later)
  • Chez Scheme (v9.5)
  • Larceny (1.3)
Chez and Larceny require PFFI and psystem as their dependencies and they can only run POSIX environment (not on Windows, PR is always welcome :) ).

Who is using this?

As I mentioned above, I'm using this library to create R6RS portable Redis client. It's at least good enough to implement the client.

2018-12-02

SRFI-123の紹介

この記事は Lisp SETF Advent Calendar 2018 二日目の為に書かれました。

0x25歳になって最初に書く記事です(実際には投稿予約の機能を使っているので数日若いが…)。いい感じの区切りの歳の最初の記事としてはイマイチな題材かもしれないなぁとも思いつつ…

SRFI 123: Generic accessor and modifier operators は Taylan Ulrich Bayırlı/Kammer によって提唱された Gauche 風の総称アクセサを提供する SRFI です。取りあえず簡単な例を見てこれの何が嬉しいのかというのを確認してみましょう。
(import (rnrs) (srfi :123))

(define l (list (list 1) 2 3)) ;; Must not a literal list :)

(ref 1 0) ;; -> (1)
(ref* l 0 0) ;; -> 1
;; ~ is an alias of ref*
(~ l 0 0) ;; -> 1

(set! (ref l 1) 4)
(ref l 1) ;; -> 4

(set! (ref* l 0 0) 5)
(ref* l 0 0) ;; -> 5
こんな感じです。上記の例ではリストを使っていますが、その他のデータ又はレコード型でも似たような感じで動作します。

基本的にはref又はref*が適切なデータへのアクセス手続きに変換すると思えば良いです。上記の例ならば、reflist-refに変換しています。

set!は SRFI-17 を利用して実現しています。例えば以下のコード
(set! (ref l 1) 4)
(set! (ref* l 0 0) 5)
は、それぞれこのように変換されます
((setter ref) l 1 4)
((setter ref*) l 0 0 5)
ちなみに、SRFI-17 については LISP Library 365 で書いたSRFI-17の紹介を参照するといいかもしれません。

余談
この SRFI のポータブルな実装を覗くと、もの凄く頑張って色々なデータ型のサポートをしているのが見て取れます。この頑張りは総称函数があれば不用だろうなぁいう感じがするので、この SRFI の前に総称函数の SRFI があればよかったのではとも思ったりします(もしくは、Tiny CLOS を使って実装するとか?)。

 追記
(ref* 1 0 0)を修正(1:数字の一 -> l:小文字のL)

2018-10-08

JMESPath

JSON用のクエリーがほしいなぁと思いいろいろ探していたのだが、まともな仕様があるものが少ない。JSONPathは超有名なブログ記事のみで今一不安だし、JSONiqはちょっとしたクエリ書くだけにしては巨大すぎる感があるし、JSON Queryでググると山のようにオレオレ実装が出てきて嫌気がしたというのがある。(今思えばJSONiqでもよかった気はする)

そんな中で手ごろなサイズでそれなりに仕様が固まっていて、AWS CLIでも使われているらしいJMESPathなるものを知った。名前は微妙だけど(提唱者名が入ってる)AWS CLIで使われているということはそれなりに実績もあるんだろうし、準拠テストもあるしこれでいいかということで実装してみた。(ここまで前置き)

ということで、こんな感じで使える
(import (rnrs) (text json jmespath))

((jmespath "a") '#(("a" . "foo") ("b" . "bar") ("c" . "baz")))
;; -> "foo"
前に作った(text json pointer)と似たような使用感にしてある(再利用可能という意味で)。後、自分が必要だという理由でいくつか追加の手続きを追加している。名前は以下
  • parent
  • unique
  • remove
  • remove_entry
  • is_odd
  • is_even
名前見れば大体何するか分かりそうなので説明は割愛。詳しくはドキュメント読んで。

実装する上で辛かったこと
JMESPathは一応BNFが定義されているのだが、これが左再帰バリバリで書かれている。しかも、各言語での実装はBNFからパーサを作ってるタイプではないので(Javaの実装はANTLRを使っているのでBNFそのままだったが)、そのままPEGに移植することができない。しょうがないので左再帰を全部除去しつつ、ASTの意味が著しく異ならないようにするのにものすごく苦労した。この辺りの経験値はまだ低いなぁと実感。

また、仕様に書かれている例と準拠テストの挙動が違ったり、仕様がやけに曖昧だったりと結構落とし穴が多かった。(個人的にProjection周りの挙動が仕様からは読み取れなかったので、実装を確認する必要があった)

実装した後で発覚した事実
さて実装し終わったし使い倒すぞ!と意気込んでみたら、実はあまり必要ない感じになっている。結局JSONの構造が分かっていないと使えないものではあるので(GLOBみたいなネスとした曖昧マッチはない)、JSONPointerとベクタライブラリでことが足りるのではという感が出てきている。ベクタJSONの簡易な変形が目的なら使い出があるかもしれない。

2018-09-16

JSON パーサ

JMESPathの実装をしていてJSONのパーサをPEGで書いた方が便利だなぁということに気付いたので実装してみた。仕様は最新のJSONの仕様(RFC8259)を参照することにした(別にjson.orgのでもよかったんだけど、なんとなく。どうせ一緒だろ?)。

どうでもいいのだが、「\」を使った文字列のエスケープがかなり制限されてる気がする。「\a」とか「\c(意味のないエスケープ)」とかは仕様に従うならイリーガルになるっぽい。どうしようかな?

書きあえるにあたって気になるのはもちろんパフォーマンス。以前使っていたPackratの実装だと現在の実装に比べて30倍程度遅い(参照: JSONパーサの性能改善)。ということで書いた後にベンチマークをとってみる。こんな感じのコードで測る。結論だけ先に言えば、現在の実装はかなり速いので、いろんな角度で速度を計測した。I/O有、I/O無等。
(import (rnrs)
 (text json parser)
 (text json)
 (sagittarius generators)
 (util file)
 (srfi :127)
 (time))

(define (parse parser) (call-with-input-file "large.json" parser))
(define-syntax time-parse
  (syntax-rules ()
    ((_ parser)
     (begin
       (newline) (display 'parser) (display " from file")
       (time (parse parser))
       (let ((in (open-string-input-port (file->string "large.json"))))
  (newline) (display 'parser) (display " in memory")
  (time (parser in)))))))

(time-parse json-read)
(time-parse parse-json)

(call-with-input-file "large.json"
  (lambda (in)
    (let ((lseq (lseq-realize (generator->lseq (port->char-generator in)))))
      (time (json:parser lseq)))))
結果は以下:
% sash -Lsitelib bench.scm
json-read from file
;;  (parse json-read)
;;  0.313142 real    0.313000 user    0.000000 sys

json-read in memory
;;  (json-read in)
;;  0.097882 real    0.109000 user    0.000000 sys

parse-json from file
;;  (parse parse-json)
;;  0.484309 real    0.531000 user    0.000000 sys

parse-json in memory
;;  (parse-json in)
;;  0.337894 real    0.344000 user    0.000000 sys

;;  (json:parser lseq)
;;  0.269573 real    0.266000 user    0.000000 sys
最初の二つが、現状の実装I/O有、I/O無。続いてPEG版のI/O有、I/O無、正格評価。何をどうひっくり返しても手作りの温かみのある実装に勝てないという結論に至る。I/O有で60%、I/O無で2.5倍のパフォーマンス劣化になることが問題になるかならないかというのもある。PEG(というかパーサコンビネータ)の性質上ある程度の性能劣化はしょうがないかなぁと思っていたが、ここまであるとなぁ。先にPEGの最適化をするべきかもしれない…

余談だが、PEGを使うとJSONパーサが30分程度で書けることが分かった。そろそろ手放せなくなってきたしドキュメント書かないとなぁ。

2018-08-28

JSON Schema

JSON Schemaというものがある。これはみんな大好きJSONにXSDよろしく型をつけようというものだ。XSDより簡単っぽいのでとりあえず実装してみた。こんな感じで使える。
// product.schema.json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://example.com/product.schema.json",
  "title": "Product",
  "description": "A product in the catalog",
  "type": "object",
  "properties": {
    "productId": {
      "description": "The unique identifier for a product",
      "type": "integer"
    }
  },
  "required": [ "productId" ]
}
// input.json
{
  "productId": 1
}
(import (rnrs)
        (text json)
        (text json validator)
        (text json schema))

(define product-validator 
  (json-schema->json-validator 
    (call-with-input-file "product.schema.json" json-read)))
(validate-json product-validator
  (call-with-input-file "input.json" json-read))
;; -> #t
もう少し凝った例を見てみる。本当ならhttps://json-schema.org/learn/にあるのを直接使いたかったが、どうもあまりメンテされてないらしく不整合があって使えなかった。なので、ちょっと長いがSchemaをだらだら書く。
// address.schema.json
{
  "$id": "http://example.com/address.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "An address similar to http://microformats.org/wiki/h-card",
  "type": "object",
  "properties": {
    "post-office-box": {
      "type": "string"
    },
    "extended-address": {
      "type": "string"
    },
    "street-address": {
      "type": "string"
    },
    "locality": {
      "type": "string"
    },
    "region": {
      "type": "string"
    },
    "postal-code": {
      "type": "string"
    },
    "country-name": {
      "type": "string"
    }
  },
  "required": [ "locality", "region", "country-name" ],
  "dependencies": {
    "post-office-box": [ "street-address" ],
    "extended-address": [ "street-address" ]
  }
}
// geographical-location.schema.json
{
  "$id": "http://example.com/geographical-location.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Longitude and Latitude Values",
  "description": "A geographical coordinate.",
  "required": [ "latitude", "longitude" ],
  "type": "object",
  "properties": {
    "latitude": {
      "type": "number",
      "minimum": -90,
      "maximum": 90
    },
    "longitude": {
      "type": "number",
      "minimum": -180,
      "maximum": 180
    }
  }
}
// card.schema.json
{
  "$id": "http://example.com/card.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "A representation of a person, company, organization, or place"\
,
  "type": "object",
  "required": [ "familyName", "givenName" ],
  "properties": {
    "fn": {
      "description": "Formatted Name",
      "type": "string"
    },
    "familyName": {
      "type": "string"
    },
    "givenName": {
      "type": "string"
    },
    "additionalName": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "honorificPrefix": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "honorificSuffix": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "nickname": {
      "type": "string"
    },
    "url": {
      "type": "string"
    },
    "email": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string"
        },
        "value": {
          "type": "string"
        }
      }
    },
    "tel": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string"
        },
        "value": {
          "type": "string"
        }
      }
    },
    "adr": { "$ref": "http://example.com/address.schema.json" },
    "geo": { "$ref": "http://example.com/geographical-location.schema.json" },
    "tz": {
      "type": "string"
    },
    "photo": {
      "type": "string"
    },
    "logo": {
      "type": "string"
    },
    "sound": {
      "type": "string"
    },
    "bday": {
      "type": "string"
    },
    "title": {
      "type": "string"
    },
    "role": {
      "type": "string"
    },
    "org": {
      "type": "object",
      "properties": {
        "organizationName": {
          "type": "string"
        },
        "organizationUnit": {
          "type": "string"
        }
      }
    }
  }
}
// card.json
{
    "familyName": "Kato",
    "givenName": "Takashi",
    "adr": {
        "locality": "locality",
        "region": "region",
        "country-name": "The Netherlands"
    },
    "geo": {
        "latitude": 10,
        "longitude": 90
    }
}
バリデーションコードは以下
(import (rnrs)
        (text json)
        (text json schema)
        (text json validator)
        (srfi :26 cut))

(let* ((validators (map json-schema->json-validator
                        (map (cut call-with-input-file <> json-read)
                             '("address.schema.json"
                               "geographical-location.schema.json"))))
       (validator (apply json-schema->json-validator
                         (call-with-input-file "card.schema.json" json-read)
                         validators)))
  (validate-json validator (call-with-input-file "card.json" json-read)))
;; -> #t

一応公式Githubにあるテストは全部通る(ドラフト7、オプショナル除く)。

余談だが、最近書いたYAMLパーサと組み合わせることもできる。以下はカードYAML
---
familyName: Kato
givenName: Takashi
adr:
  locality: locality
  region: region
  country-name: The Netherlands
geo:
  latitude: 10
  longitude: 90
っで、コード
(import (rnrs)
        (text json)
        (text json schema)
        (text json validator)
        (text yaml)
        (srfi :26 cut))

(let* ((validators (map json-schema->json-validator
                        (map (cut call-with-input-file <> json-read)
                             '("address.schema.json"
                               "geographical-location.schema.json"))))
       (validator (apply json-schema->json-validator
                         (call-with-input-file "card.schema.json" json-read)
                         validators)))
  (map (cut validate-json validator <>)
       (call-with-input-file "card.yaml" yaml-read)))
;; -> (#t)
YAMLはドキュメントのリストを返すのでmap辺りでリストを回す必要がある。便利に使えそうな雰囲気がある。

2018-08-22

キャッシュバグと手続きの同一性

こんなバグに遭遇した。
ASSERT failure /home/takashi/projects/sagittarius/src/closure.c:50: SG_CODE_BUILDERP(code)
C assertなので、いかんともしがたいやつである。

再現コードはこんな感じ。
;; lib1.scm
(library (lib1)
  (export +closures+)
  (import (rnrs))

(define (foo e)
  (unless (string? e) (assert-violation 'foo "string" e))
  (lambda (v) (string=? v e)))

(define +closures+ `(,foo))
)

;; lib2.scm
(library (lib2)
  (export bar buz)
  (import (rnrs)
          (lib1))

(define (bar s) ((buz) s))
(define (buz) (car +closures+))
)

;; test.scm
(import (rnrs) (lib2))

((bar "s") "s")
何が問題化というと、キャッシュと最適化の問題だったりする。Sagittariusではexportされた変数は変更不可能というのを利用して、キャッシュ可能なオブジェクト(文字列、リスト等)を本来なら大域変数の参照になるところを実際に値にするという最適化がなされる。手続きがSchemeで定義されたもの(closure)であればキャッシュ可能なのと、+closure+に束縛されているのがリストなので、コンパイラはこいつを値に置き換える。

バグの修正はCONSTインストラクションに渡されるオブジェクトをチェックするというものになるのだが(こんな感じ)、まだ完全には直っていない模様(くそったれ!)

このバグで気づいたのだが、Sagittariusでは手続きの同一性が保証されない。以下のコードは1回目と2回目の実行で結果が異なる。
(import (rnrs) (lib1) (lib2))

(eq? (car +closures+) (buz))
個人的にはこれはR6RSなら11.5 Equivalence predicatesにある以下の例の範疇だと思っているのだが、R7RS的には常に#tを返さないといけなかったはず。
(let ((p (lambda (x) x)))
  (eq? p p)) ;; unspecified

(let ((p (lambda (x) x)))
  (eqv? p p)) ;; unspecified
いちおう両方準拠を謳っているから直さないとまずいかねぇ…

2018-08-15

YAMLパーサー

個人的にはYAMLは好きではないのだが、世の中の流れはYAMLに行っているのは明白かなぁと思っている。ということで、SagittariusにはYAMLのサポートを入れることにした。こんな感じで使える。
# test.yaml
%YAML 1.2
---
receipt:     Oz-Ware Purchase Invoice
date:        2012-08-06
customer:
    first_name:   Dorothy
    family_name:  Gale

items:
    - part_no:   A4786
      descrip:   Water Bucket (Filled)
      price:     1.47
      quantity:  4

    - part_no:   E1628
      descrip:   High Heeled "Ruby" Slippers
      size:      8
      price:     133.7
      quantity:  1

bill-to:  &id001
    street: |
            123 Tornado Alley
            Suite 16
    city:   East Centerville
    state:  KS

ship-to:  *id001

specialDelivery:  >
    Follow the Yellow Brick
    Road to the Emerald City.
    Pay no attention to the
    man behind the curtain.
(import (rnrs)
        (text yaml))

(call-with-input-file "test.yaml" yaml-read)

#|
(#(("receipt" . "Oz-Ware Purchase Invoice")
   ("date" . "2012-08-06T00:00:00Z")
   ("customer"
    .
    #(("first_name" . "Dorothy")
      ("family_name" . "Gale")))
   ("items"
    #(("part_no" . "A4786")
      ("descrip" . "Water Bucket (Filled)")
      ("price" . 1.47)
      ("quantity" . 4))
    #(("part_no" . "E1628")
      ("descrip" . "High Heeled \"Ruby\" Slippers")
      ("size" . 8)
      ("price" . 133.7)
      ("quantity" . 1)))
   ("bill-to"
    .
    #(("street" . "123 Tornado Alley
Suite 16
")
      ("city" . "East Centerville")
      ("state" . "KS")))
   ("ship-to"
    .
    #(("street" . "123 Tornado Alley
Suite 16
")
      ("city" . "East Centerville")
      ("state" . "KS")))
   ("specialDelivery"
    .
    "Follow the Yellow Brick Road to the Emerald City. Pay no attention to the man behind the curtain.
")))
|#
YAMLは一ファイルの中に複数ドキュメント含むことを許しているのでリストを返すことにした。デフォルトでは(text json)が返す書式と同じものを返すが、オプショナル引数でその辺を制御することもできる。書き出しは以下のようにする。

;; suppose variable yaml is bound to a YAML document
(yaml-write yaml)

;; if it's read by yaml-read, then it should be like this
(for-each yaml-write yaml)
書き出しはあまりこみったことをしないので(複数ラインリテラルとか、ラベルとか)、完全に元のドキュメントに復元はしない可能性がある。(ラベルくらいは実装してもいいかなぁとはブログ書いてて思った。)

これ書いてて思ったのは、YAMLの文法は思った以上に機械に優しくないということか。ヒューマンリーダブルかどうかは議論する気はないが(個人的には読みづらいと思ってる)、一文字ずつ読む感じのPEGでの実装はやる気をなくすレベルであった(ついでに公式サイトにあるBNFは人にも機械にも辛い気がする)。

あとは適当に使ってみて不具合をつぶしていくかね。

2018-07-19

パイプライン アイデア編(2)

前回何となく書いたパイプラインのアイデアをもう少し進めてみた。

例えばこんな感じで使えるとうれしいだろうか?
(import (rnrs)
        (util concurrent))

(define-pipeline-catalogue pipeline-catalogue-a
  (* => pipe1)
  ((sync pipe1) (=> pipe2)
                (-> epipe1))
  ((async pipe2 5) (symbol? pipe3sym)
                   (string? pipe3str)
                   (=> pipe3gen))
  (pipe3sym => *)
  (pipe3str => *)
  (pipe3gen => pipe3sym)
  ;; = (async epipe1 1)
  (epipe1 (=> pipe1)
          (-> !)))

;; so something
(define-pipe (pipe1 input) 'output)
;; do correction
(define-pipe (epipe1 error) 'output)

(define-pipe (pipe2 input) "string")
(define-pipe (pipe3sym input) input)
(define-pipe (pipe3str input) (string->symbol input))
(define-pipe (pipe3gen input) 'symbol)
(define-pipe (epipe1 e) 'recover)

(define pipeline-a (instantiate-pipeline-catalogue pipeline-catalogue-a))

;; async call
(pipeline-send-message! pipeline-a 'message)
;; waits
(pipeline-receive-message! pipeline-a)
パイプラインカタログはパイプのつながり方を定義し、define-pipeは実際のパイプを定義する。(パイプという名前はいまいちだから、define-pipe-unitにしようかな?) カタログの定義が終わった段階では特に何もせず、インスタンスを作って初めて使用可能にする。まぁ、再利用可能にするため。同期と非同期はちと微妙な感じもする。

問題はパイプラインを作るたびにスレッドを10個とか作りそうなところがあることか?ネットワーク通信みたいな重たい処理だけにほしいので、基本同期の方がいいのかな?もう少し寝かせた方がいいかもしれない…

2018-07-16

【備忘録】Windows 10 上である程度まともな開発環境を作る

今月から新しい職場になったのだが、前職と違い開発環境がWindowsであった。噂にはMacが与えられる予定だったらしいのだが、偉いさんの鶴の一声で却下されたとか…まぁ、大企業あるあるだと思って前向きに考えることにした。っで、今日環境がWindows 7からWindows 10にアップグレードされたので、WSLを使ってそれなりにまともな開発環境をこさえる努力をすることにした。

【Ubuntu on WSLを入れる】
Micorsoft Storeが使えればそれをそのまま使えばいい。っが、今回はStoreがブロックされているので直接Zipファイルをダウンロードする方法をとらざるを得なかった。詳細は以下のStack overflowが詳しい:
Is there a way of installing Windows Subsystem for Linux on Win10 (v1709) without using the Store?

Ubuntuのバージョンが16.04だったので、do-release-updateを使って18.04にした。

【VcXsrvを入れる】
まともなターミナルエミュレータを使わないとまともな開発環境は作れない。ここでいうまともの定義は少なくともtmuxがまともに動く程度(だが、Windowsの標準ターミナルだと画面がちらつくのだよ)。いろいろオプションはあるが、Windows側にX11サーバを立てる方法が一番楽かなぁと思いそれにした。

VcXsrvは64ビットバイナリがSourceforgeにあるので、それを落とす。軌道はマルチウィンドウであれば後は適当でもいいと思う。

【xfce4-terminalを入れる】
Gnomeでもいいのだが、軽い方がいいかなぁと思い。

【起動スクリプトを書く】
デフォルトのubuntu.exeではWindows標準ターミナルが開くので、起動スクリプトを書く。こんな感じ。
Set objShell = WScript.CreateObject("WScript.Shell")
objShell.Run "%LocalAppData%\Microsoft\WindowsApps\ubuntu1804.exe run DISPLAY=localhost:0.0 xfce4-terminal --working-directory=/home/takashi -x /bin/zsh -i", 0
Set objShell = Nothing
Storeを使わなかった場合は適当に展開先のパスに置き換える。VBScriptを使ってるのは余計なコンソールを起動したくないから。

【個人的な設定】
tmuxのデフォルトシェルをzshにする。以下を.tmux.confに追加する。
set -g default-shell /bin/zsh

以下は職場で必要だった設定。

【CA証明書の追加】
職場のネットワーク環境は独自のルートCA証明書をもっていて、そいつをTrustedストアにいれてやる。以下のようにする。
$ mv certificate.crt /usr/local/share/ca-certificates/
$ sudo update-ca-certificates
拡張子が.crtじゃないと認識してくれない。

2018-06-07

パイプライン アイデア編

MongoDBバインディングの使用例を書いている際に、アクターを繋げてパイプライン的に動かすようなのを書いたのだが、これうまくやるとパターンにできないかなぁと思えてきた。

イメージとしては以下のようなブロックをつなげていくような感じ
    +==============pipeline==============+
    |                                    |
    |    +-------+          +-------+    |
==input=>| pipe1 |=success=>| pipe2 |=success=>
    |    +-------+     |    +-------+    |
    |        |         |        |        |
    |      error       |      error      |
    |        |         |        |        |
    |    +-------+     |        |        |
    |    | pipe3 |==>>=+        |        |
    |    +-------+ (success)    |        |
    |        |                  |        |
    |      error----------------+        |
    +========+===========================+
             |
pipe1は入力を受け取って処理をし、成功すればpipe2に処理したデータを渡す。失敗すればerrorに渡す。errorの接続先は別のパイプでもよく、pipe3の処理が成功すれば(例、データ変換)すればpipe2に処理を渡す。失敗すればerrorに渡す。以下同様。 っで、全体のpiplelineとしての処理はpipe同様二本のチャンネルで得る。
最終的にはpipleline同士を繋げてより大きなパイプラインにするみたいな。

パイプ、パイプラインともにアクターの亜種として定義すれば、処理ごとにアトミックになるし、必要なら同一チャンネルを使うユニットを増やすことで処理の多重化もできそう。うまく書ければ使いではありそうな感じがする。ちと考えてみるか。

2018-06-04

MongoDBバインディング

ここ数週間R6RSな処理系で動くMongoDBバインディングを作っていたのだが、なんとなくお披露目してもいいかなぁという感じの出来になってきたのでお披露目してみる。

リポジトリはここ
こんな感じで使える。
(import (rnrs)
        (mongodb))

(define connection 
  (open-mongodb-connection! (make-mongodb-connection "localhost")))
(define database (make-mongodb-database connection "test"))

(define collection "testCollection")

(mongodb-database-insert database collection
  '#(
     (("id" 1) ("name" "R6RS MongoDB library") ("lang" "Scheme")))
     )
(mongodb-database-upsert database collection
  '(("name" "R7RS PostgreSQL library"))
  '(("$set"
     (("id" 2) ("name" "R7RS PostgreSQL library") ("lang" "Scheme")))))

(mongodb-database-update database collection
  '(("id" 1))
  '(("$set" (("lang" "R6RS Scheme")))))
(mongodb-database-update-all database collection
  '(("id" (("$gt" 0))))
  '(("$set" (("comment" "Portable Scheme library")))))

(mongodb-query-for-each (lambda (doc) (write doc) (newline))
  (mongodb-database-query database collection '()))

(mongodb-database-delete-all database collection '())

(display
 (mongodb-query-result-documents
  (mongodb-database-query database collection '()))) (newline)

(close-mongodb-connection! connection)
#|
;; prints
(("_id" (object-id "5b1110aa9614bc720de985b6"))
 ("id" 1)
 ("name" "R6RS MongoDB library")
 ("lang" "R6RS Scheme")
 ("comment" "Portable Scheme library"))
(("_id" (object-id "5b1110aa9614bc720de985b8"))
 ("id" 2)
 ("name" "R7RS PostgreSQL library") 
 ("lang" "Scheme")
 ("comment" "Portable Scheme library"))
#()
|#
BSONは手で書くには不便すぎるのでSBSON(S-expr BSON)という形式で書く。SBSONはalist形式(Gauche形式)のJSONによく似ているが、型をつけれるという点とBSONである必要がる(全てがJSONでいうObject等)で多少異なる。

現状でサポートしている処理系は3つだが、以下のSRFIを両方サポートしている処理系なら動くはず:
  • SRFI-39
  • SRFI-106
 もう少しドキュメントとサンプルを足したらリリースのアナウンスをしようかなぁと思っている。

余談
このライブラリのテストにScheme Envを使っているのだが、まぁ、便利に使えている。Bashの中にサポートしている処理系のリストを作れば後は共通でやってくれるので楽である。Bash内にリストせずにインストールしている処理系に対してというのをやれると便利かなぁと思っているので、処理系のメタデータ的なものを入れるといいかもしれないと思ってきた。サポートしている仕様とか。

2018-05-16

あるソフトウェア開発者の転職経験記

オランダ在住かつマーケット以上の給料をもらっているという前提条件があるので、あまり参考にならないかもしれないが、こんな感じでやったら成功したというのを一応記録しておく。

【条件設定】

転職する際にはなぜ転職したいのかを明確にしておくとゴールが設定しやすい。僕の場合は以下が大きな理由。
  • 今の会社での給料が頭打ちになった
    • 明確に開発者以外の方向にも行くようにと言われた
  • 会社の先行きに不安があった
  • 会社の変革する方向に不満があった
    • 言動不一致というかこれじゃない感が漂っていた(る)
    • 割とゆるい感じだったのが、締め付けられていく感もあった(る)
ここから以下の条件を設定した。
  • 手取りの給料が一緒かそれ以上
    • 今の会社はペンションプランがないので、それがある会社に行くと下がる可能性があった
  • ある程度アジャイルな環境
    • 今更フォーターフォールは嫌
    • せっかく開発者なのだから、なにか目に見えるもの開発したいよね
  • ある程度自由な社風
    • 俺に従え的な雰囲気があると辛い(経験済)
個人的に給料の下がる転職は不可能なので、一番上が一番重要だった。下2つは正直何を言われても入ってみないと分からないというのがあるので、最低でも一つは条件を満たさないとがっかりする転職になりかねないというのもあった。


【応募】

応募するのは基本以下の3つの経路があると思っている。
  • エージェント経由
  • コーポレートリクルーター経由(会社お抱えのリクルータ−)
  • 自前
エージェント経由は便利だが正直あまり良いイメージがない。理由は概ね以下
  • 興味があると言ったものと別のものを出してくる
    • 広告用の募集要項なんだそうだ
  • 必ず電話で話したいと言い出す
    • そんな時間ない(業務時間外でもOKという融通の効くところもある)
  • 希望する給料をいうと第一声が「下げられないか?」である
    • 高いのはわかっているが無理なものは無理だ
  • 転職後一年もするとまた電話してくる
上記が問題にならないのであれば、便利だと思う。履歴書のスクリーニングで落ちる可能性が格段に低くなるのもよい。

コーポレートリクルーターはコンタクトを取ってきたエージェントが希望する会社の人であれば一番よい。LinkedInやIndeed経由でメールが飛んでくるので気になる会社であれば返事をするのがいいだろう。

自前は茨の道であることは否めない。かなりの確率で履歴書のスクリーニングを通過できない。頑張ってカバーレターを書いたのに涙を飲むというのもままある。っが、希望する職にダイレクトに応募できるというメリットもあるので、鉄の心臓を持っている、時間的に余裕がある等頑張れる要因があるのであればこの道を取ってみてもいいかもしれない(ちなみに今回の転職はこれだった)。

【面接】

履歴書のスクリーニングを通過すると面接がある。会社によっては電話だったりスカイプだったりするが、そこまでの大差はない。いきなり課題を出す会社もあるが、プロセスの一環なので気にしない。昔から面接では苦労した記憶がないので、ここまで来るとオファーがもらえる確率が50%くらいという気持ちになる。面接では大抵以下のことが聞かれるので、適当に納得できそうなものを用意しておくといい:
  • 自己紹介(経歴等)
  • なぜ転職するのか
  • 自慢したい直近の成果
相手の会社について事前に調べる必要があったことはない。ほぼ100%面接の場で事業内容等を説明してくれる。質問があれば都度すればよいし、多分した方がよいと思われる。テクニカルな面接でない場合はコミュニケーション能力を計っている場合が主なので、無言の方が印象が悪いと思われる。(知らない単語が出てきたら、「なにそれ?」くらいの質問でいい気がする)

プロセスは前後することがあるが、面接の後は大抵課題が与えられる。課題は経験上以下の2種類がある:
  • 面接官付きの短期決戦型
  • お題が与えられる長期戦型
これらは見られているものが違うので注意しないと行けない。短期決戦型の課題はアルゴリズムに対する知識が問われるのが主なので、以下のトピックを習熟しておけば怖くない:
  • グラフ操作(BFS、DFS等)
  • ソート
  • 探索
  • ビッグ・オー記法
大抵の場合は上記の組み合わせまたは変形でなんとかなる。普段からHackerRank辺りで遊んでいるといいかもしれない。面接者は(おそらく)実際のコーディングの様子、問題解決のプロセス等を見ているので、思考を口に出すと割と喜ばれる。

長期戦型はあるトピックに対してのフルアプリを作成することが多い。短期決戦型と違って何が出てくるのか予測がつかないが、大抵4時間から1日くらい費やせば完了する程度の規模なので、週末を使ってじっくり取り組めば問題ない。短期決戦型と違い、デザイン、データ等実践的な解決を見られるので妥協しないで頑張った方がよい。(テストカバレッジ100%とか含む)

課題を無事通過すると、第2面接か、次の交渉のステージになる。第2面接は大抵課題の解決方法の理由を聞かれたりする。真面目に取り組んでいれば問題なく答えられるはず。

【交渉】

ここまでを無事通過するとサラリーの交渉になる。エージェントを経由する場合はこのステップはエージェント経由になるので、直には経験しないかもしれない。

人によっては現在の年収等を聞かれるのは好きではないかもしれないが、馬鹿正直に答える必要はない、自分が望む額を適当に言えばよいのである(今回ペイスリップを求められたが断った)。そうは言ってもあまり高すぎると向こうの予算オーバーになるので、適当に調節する必要はある。今回は現在の月収+€200くらいで答えたりした(前々回が+€400で前回は+€700だったかな、今回は今の会社を早く去りたかったというのもある)。基本的にここは交渉の場なので、向こうがそれでは高すぎると判断すれば下げられないか聞いてくる。もちろん、下げる必要はないし、ダメならオファーが来ないだけである。交渉の余地がない場合もあるので、その場合は金額と仕事内容で判断すればよい。僕は気長にやれば希望金額より上を出す会社もあるということを知っているので、大抵断ったけど。

交渉がまとまれば晴れて契約。ちなみに、ここまでは現在の会社に内緒で行う必要がる。契約書にサインしたら辞意を伝えればよい。

【その他】

ここからはオランダのソフトウェア開発者市場のみに当てはまるものの可能性があるので、あまり参考にならないかもしれない。

【職種】
今回の転職活動でSI/コンサル系の大手からもオファーをもらったのだが、えらくしょぼかった。もし次があった場合はSI/コンサル系は外した方がいいだろう。元々好きでもないし。そう言えば、あの会社は課題的なのなかったな。あんまり技術を売っていないのかもしれない…

【大手ホテル予約会社】
向こうから連絡してきて、アポ取れと言われたので取ったのだがシカト食らった。なんだったんだろう?(単なる愚痴)

【向こうの予算】
オランダはよくソフトウェア開発者不足と聞くが、給料はあまり高くない気がする。リクルーターに現在の年収を伝えると次から大抵連絡が来なくなるという現象が多発した(コーポレートリクルーターなら予算外と言われたり)。

まとまらなくなってきたので終わり。

2018-05-07

ライブラリの依存性

Scheme でライブラリを書いていると巨大なファイルになることがままある。最近だとこのファイル。まだ未完成なのだが既に1000行近くある(まだ1000行とも言えるが、個人的にファイル内の移動が既に辛い)。辛いなら分ければいいじゃない?という話なのだが、怠け者根性と分け方で迷っているというのの2つがあって今のところ放置している。

ライブラリを複数のサブライブラリに分ける際に大抵以下の2種類のパターンで迷う:
  1. 機能ごとに分ける(例:型、手続き、etc.)
  2. オブジェクト指向的に分ける
個人的にどちらが優れているという気はないし、ケースバイケース(その時の気分ともいう)で違う分け方をしている。今回は2の方向で分けたいと思っているのだが、これだと不都合がでる。ライブラリの循環だ。

今作っているライブラリはXMLのDOMを扱うものなのだが(最終的にどこまでサポートするかは不明)、DOMにはquerySelectorなる手続きがある。これはParentNodeで定義されているが、このインターフェースはDocumentDocumentFragment及びElementで実装されている。CLOSを使っていないので、これらは別々に実装される必要があって、どう実装するかは考えてないんだけど、3つの似たりよったりな手続きが実装されることになると思われる。そうすると、この手続きはどこか別の場所に分けたいのだが、ライブラリをオブジェクト指向的に分けるとこんな感じになって、循環する:
  • Elementライブラリ: Selectorライブラリが必要
  • Documentライブラリ: Selectorライブラリが必要
  • DocumentFragmentライブラリ: Selectorライブラリが必要
  • Selectorライブラリ: Elementライブラリが最低必要
 (Selectorライブラリに分けたいのはこいつが別仕様だからだったりする。)

こういう時は機能毎に分ければうまく行くんだけど、なんだかなぁという気がしていて、う〜ん。ライブラリの遅延ロードみたいなのを導入して循環参照を許してもいいんだけど、それもなぁ的な。Schemeのライブラリは生まれて日が浅い(と言っても既に数年経っているが)ので、こういう場合のベストプラクティス的なものはないし(Scheme的にはこういうケースでベストプラクティスが必要なのって言語仕様的にどうよってなる気もしないでもないがどうなの?)、さてどうしようかね…

2018-04-26

あなたがSchemeを使うべきではないn個の理由

ちょっと前にこんなことを呟いた。
Schemeは好きだがいろいろな理由を考えるとなかなかに現場では使えないというのは賛成である。そこで、あまり気乗りはしないが、使うべきではない理由をあげてみようと思う。これらは現状での改善点でもあるとは思うので、これを読んでなんとかしないとと思えたらぜひとも改善してみてほしい。また、ここに列挙するのは個人的に現場導入に躊躇する理由なので、他にもあるぞ!という方がいればコメントあたりに書いてくれると追記するかもしれない。

【標準だけでは機能が貧弱】

 よく言われることではある。っが、反論としては他の標準がある言語はどうよというのはある(例:CはPOSIX抜くとソケットすらない)。もう一つ反論としては、現在R7RS-largeのプロセスが(遅々として進まなないが)動いているので、ある程度決まってくると結構な機能が揃うと思う。

【開発環境が貧弱】

JavaならIntelliJ、Eclipse、NetBeansなどいろいろあるが、Schemeだと多分Emacs一択だと思う。 更に機能も貧弱。拡張としてGeiserがあるが、(使ったことないのでドキュメント眺めただけの情報だが)これもREPL+αぐらいしかなさそう(例えば、リファクタリング機能みたいなのはなさそうだ)。

【ビルドシステムが貧弱】

JavaならMaven+Nexus、JavaScriptならNPMがあるが、Schemeにはない。パッケージ管理で言えばSNOWがあるが、いまいち。処理系固有ならegg(Chicken Scheme)、PLaneT(Racket)があるが、処理系が固定される。(Racketは正式にはScheme方言になる)

【処理系が多い】

メジャーな処理系だけでも両手で足りないくらいある。選択肢が多いのはいいことだが、CのようにとりあえずGCCでよくね?的に決めるのは難しい。処理系選びに知識が必要になるので、初心者泣かせではあるだろう。(昔処理系の選び方を書いたが、今でもたまにアクセスがあるんだよね。2018年版でも書こうかな)

【コミュニティが小さい】

コミュニティの大きさ=知識ベースだと言える。つまり、困った時に自力で何とかするしかない場面が圧倒的に多くなる。例えばStack Overflowにある質問を見ても数も少なければCSの学生レベルの質問がメインだったりする。また、RacketでWebアプリ作ってるんだけど、みたいな質問にはなかなか回答がつかなかったりする。

【ライブラリが少ない】

ポータブルなライブラリがものすごく少ない。例えばデータベースにアクセスするポータブルなライブラリは知るところでは拙作のr7rs-postgresqlしか寡聞にして聞かない。この辺は標準+SRFIの貧弱差も出てしまうので難しいところではある。ちなみに、R6RS処理系ならば、拙作のr6rs-pffiがあるのでバインディングさえ作ってしまえば、夢は広がるはず(だれかChez対応のPR送ってくれないかなぁ、チラチラ)。

とりあえず思いついたのを列挙してみた。ライブラリや開発環境の充実は個人でも頑張ればできそうな部分ではあると思う。Rubyが今ほどにポピュラーになったのはRailsが出てきたからという意見もあることを思うと、Schemeでポータブルに動くキラーアプリが出れば現状を打開できるかもしれない。