Let's start Scheme

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でポータブルに動くキラーアプリが出れば現状を打開できるかもしれない。

2018-03-30

Ephemeron を使ってみる

Ephemeron は SRFI-124で定義されているがどういったものなのか、どういう挙動が期待されるのかというのはなかなかに想像しづらい。ということで、ちょっと触りつつ解説してみる。

まず、 ephemeron とは何か?ものすごく簡単に言えば循環構造を許す弱対といえる。SRFI-124で定義されている ephemeron は一種の鍵対で、鍵が値から参照されてたとしても、その参照のみが生きているのであれば GC 対象になるというもの。日本語は難しいのでコードで説明してみる。
(let* ((key (cons 1 2))
       (value (cons key 1)))
  (make-ephemeron key value))
こんなのが鍵が値から参照されている状態である。この場合だと、鍵は値のみから参照されているので、ephemeron を作った直後に GC が発生すればどちらも回収されるというものである。

さて、実際の挙動を観察してみよう。SRFI-124 によれば ephemeron は MIT Scheme または Racket でサポートされているという(ちなみに、Sagittarius にもなんちゃってはあるが Chibi Scheme と同様のバグを抱えている。っていうか、これ Boehm GC 上で実装できるの?) MIT Scheme は手元にないので Racket を使ってみる。コードは以下の通り:
#lang racket

(define (make-weak v)
  (let* ((a (cons 1 2))
         (b (cons a v)))
    (make-ephemeron a b)))

(define e (make-weak 'a))
(write (ephemeron-value e)) (newline)
(collect-garbage 'major)
(write (ephemeron-value e)) (newline)
出力結果は以下:
Welcome to DrRacket, version 6.3 [3m].
Language: racket; memory limit: 128 MB.
((1 . 2) . a)
#f
GC 後には ephemeron の値が回収されているのがわかる。

ちょっとした追記:
この挙動と何が違うのか考えて見たのだが、鍵となる a が大域に設定されているのがまずいのかなぁというありきたりな推測。 set! でリセットしているが、内部のどこかでは参照が生きているとかそんなのではないだろうか?流石に Racket の中まではわからないので推測に過ぎないが…

2018-03-23

Generatorの勧め

Schemeでリスト処理を書いていると中間リスト(処理の最中に作成されて捨てられるリスト)の存在が気にならないだろうか?ClojureにはTransducersがあるのにSchemeにはないのだろうか?そこでGeneratorである。

Generatorは元もとGaucheに実装されていたもので割と最近SRFIになったもの。これ自体は引数を取らない手続きかつ処理が終了するとEOFを返すというものである。(GaucheのGeneratorも元をたどるとPython辺りからインスパイアされたらしい(要出典)) このGeneratorのSRFIは紆余曲折あって2種類ある。SRFI-121とSRF-158だ。SRFI-158はSRFI-121の上位互換なので、サポートされているのであればそちらを使った方がよい。(ちなみに、Sagittariusは両方サポートしている)

では、これを使って中間リストの削減をしてみる。 まずは以下のような処理を書く。処理自体に特に意味はない。
(filter (lambda (x) (zero? (mod x 3)))                     ;; 最終リスト
        (map cdr                                           ;; 中間リスト
             (filter (lambda (x) (odd? (car x)))           ;; 中間リスト
                     (map (lambda (x) (cons x (square x))) ;; 中間リスト
                          (iota 1000))))))                 ;; 初期リスト
この処理では計3つの中間リストが存在する。iotaで作成されるものも含めると4つになる。Generatorを使うと以下のように減らすことができる。
(generator->list ;; リストの生成はここだけ
 (gfilter (lambda (x) (zero? (mod x 3)))
          (gmap cdr
                (gfilter (lambda (x) (odd? (car x)))
                         (gmap (lambda (x) (cons x (square x)))
                               (make-iota-generator 1000))))))
ここではmake-iota-generatorを使っているが、(list->generator (iota 1000))と書くと、既存の初期リストをGeneratorに変換するような処理になる。

見た目はほぼ同じ(よく目を細めるように)、ならば気になるのは性能だろう。ということでベンチマークを取ってみる。使うスクリプトは以下。
(import (scheme base)
        (scheme write)
        (srfi 1)
        (srfi 158)
        (time))

(define size 1000)
(define (target1)
  (filter (lambda (x) (zero? (mod x 3)))
          (map cdr
               (filter (lambda (x) (odd? (car x)))
                       (map (lambda (x) (cons x (square x)))
                            (iota size))))))


(define (target2)
  (generator->list
   (gfilter (lambda (x) (zero? (mod x 3)))
            (gmap cdr
                  (gfilter (lambda (x) (odd? (car x)))
                           (gmap (lambda (x) (cons x (square x)))
                                 (make-iota-generator size)))))))


(define count 1000)
(define-syntax dotimes
  (syntax-rules ()
    ((_ n expr ...) (do ((c n) (i 0 (+ i 1))) ((= c i)) expr ...))))

;; GCの結果も見るため、交互に計測する
(time (dotimes count (target1))) 
(time (dotimes count (target2)))
結果は以下の通り。
# 中間リスト作成版
$ sash -s test.scm

;;  (dotimes count (target1))
;;  0.385681 real    0.688000 user    0.000000 sys

;; Statistics (*: main thread only):
;;  GC: 21389312bytes heap, 293849719bytes allocated, 49 gc occurred
# Generator版
$ sash -s test.scm

;;  (dotimes count (target2))
;;  0.386407 real    0.480000 user    0.000000 sys

;; Statistics (*: main thread only):
;;  GC: 28368896bytes heap, 198322311bytes allocated, 28 gc occurred
性能に大きな差はないが、GC回数と割り付けられたメモリに大きな差がある。

メモリの圧迫が気になるときに思い出したように使うといいかもしれない。

2018-03-09

右回り

スタックオーバーフローにこんなのがあった。
Rotating a list to the right in scheme

真面目に実装するのはだるいなぁと思ったのでこんな風に書いてみた。
(import (rnrs)
        (srfi :1))

(define (rotate-right l)
  (if (null? l)
      l
      (cons (car (last-pair l)) (drop-right l 1))))

(rotate-right '(1 2 3 4 5))
;; -> (5 1 2 3 4)
一応O(N)で動くと言えば動く。SRFI-1にsplit-at-rightみたいなのがあればリストを舐めるのが一回で済みそうではある。

今週はネタも時間もなかったのでこんなので…(そのうちsplit-at-rightを実装してもう少しマシなのを書くかもしれない。)

2018-03-02

ファイルタイムスタンプとChicken Scheme

Sagittariusではファイルのタイムスタンプを変更する方法を用意していなかった。それが問題になったことがなかったというのが一番の理由である。が、先日これが問題になったので追加したという話。

問題になったもの
Scheme EnvでChickenのインストーラを作成していたら(archive)で展開したファイル郡をビルドできなかった。もちろんtarで展開してやれば動く。エラーとしては、Chickenが事前ビルドプロセスで生成しているファイルをビルドしようとしているというものだった。(例:library.scmからlibrary.cを生成する) これらは依存関係としてMakefileに記載されている。

原因
(当たり前のことで書くのも憚られるが)Makefileの依存関係の解決はファイルの更新日時に強く影響される。上記の場合だと、library.scmはlibrary.cよりも後に変更が加えられているのでmakeが変更有りとして解決しようとしたというもの。(.scmはアルファベット順で.cより後に来るから、展開順的にそうなった)

解決
change-file-timestamps!という手続きを追加し、(archive)で展開されたファイルのタイムスタンプを変更するようにした。特に何にひねりもない王道とも言える。

どうでもいい捕捉
新たに追加された手続きchange-file-timestamps!はこんな感じで使う。
(change-file-timestamps! file 1 (make-time time-utc 0 0))
fileは文字列で存在するファイル名を指す必要がある。残りの引数はそれぞれ、atimemtimeに相当し、最終アクセス日時と最終更新日を変更する。上記の場合だと、最終アクセス日時が現在時間+1秒になり、最終更新日がUnix時間の開始日(1970年1月1日 0時0分0秒 GMT)になる。更新をしたくない場合は#fを指定し、現在時にしたい場合は#t(時間オブジェクトまたは実数以外の真値)を与える。

これが入ったことで、今まで無理やり実装するしかなかったtouchが簡単にできるようになる。例えばこんな感じ。
(import (rnrs) (sagittarius))

(define (touch file)
  (if (file-exists? file)
      (change-file-timestamps! file #t #t)
      (call-with-output-file file (lambda (out)))))

(touch "foo")
上記はあると便利かもしれないが、今のところ用途がないので、どこのライブラリにも入れてない。要望があれば入れる感じ。

2018-02-26

多言語を扱う際のメンタルモデル

NetflixでDeath Noteを見ていた際に「L is dead」というメッセージが見えた。Lが実際に死んでからしばらく時間が立っているので、実は他にも「L is alive」等のメッセージが存在し現在のLの状況を伝えたという感じだったのだろうか。

「Death」「die」「dead」は全て英語で死を表すがそれぞれ、名詞、動詞、形容詞(または副詞)になる。北斗の拳の有名な台詞の一つである、「お前はもう死んでいる」は英語にすると「You are already dead」になる(と思う)が、「You are dying」 にはならない。もし上記のLの死亡を伝えるメッセージが、Lの現在の状態を表すものではないのであれば「L has died」の方がいい気がする。

形容詞としての「dead」対応する日本語の単語は存在するのだろうか?形容詞として「死んでいる状態」を表す単語というのを実は知らない。複数言語(といっても三つだけだが)を扱えるようになると、言語Aにはあるが言語Bに対応する単語がないということがままある。例えば「おにぎり」をずばり表す英訳や蘭訳はない気がする。逆に、蘭語の「beleg」のズバリは日本語にない気がする。(そういえば「忖度」という言葉の訳はドイツ語にズバリで「vorauseilender Gehorsam」とか「unausgesprochene Anweisung」とかいうそうだ。)

多言語を扱う際にはこういった「ある言語にはある概念」を扱う必要がでてくる。二言語だと言語Aで考えて言語Bに翻訳するみたいになる気がする。これが面倒になると最初から言語Bで考えるという風になる気がする(余談だが、この状態になると、酔っ払っていても言語Bで会話できる)。僕はこの状態で言語Cが入ってきた。言語Cの習熟度は他の二言語に比べると遥かに低いのだが、不思議なことに言語Cで話しているときは言語Cで考えているのである(もちろん、足りていない部分は先の二言語で補っているが)。これは一体どういうことなのだろうか?

思考という部分は置き換え可能なパーツでできているとすると割と理解できる動作なのかぁという気がする。脳内モデルが「入出力-思考-抽象概念」みたいな三層でできているとするのである。二言語を扱っていた最初期では「思考」の部分が母語になっていたが、これが徐々に第二言語に置き換えることが可能になった。そして、新たに覚えた言語は基本的な部分ができるようになると、置き換えの動作がスムーズにいき、習熟度が低くても置き換えられるようになったと仮定できる。

なんかこういったのを何度もブログに書いてる気がするなぁ、なんでだろ?自分のメンタルモデルを言語化するのが好きなのかね?

2018-02-24

MSYS2サポート 2

前回の続きと僕なりの回答。

前回MSYS2上でのシンボリックリンクの話とSagittarius上でどうするかというのを書いた。書いた後に助言やMSYSがどのようにシンボリックリンクを扱っているかというののヒント(調べた結果Qiitaの記事が古いのか現状の挙動と違った)をもらったのでそれを踏まえて実装してみた、という話。

【MSYS2上でのシンボリックリンク】

 MSYS2上ではシンボリックリンクはデフォルトでは作られず、ファイルのコピーとなる。この挙動を制御するためには環境変数MSYSを適切に設定する必要がある。2018年現在では以下の表のようになる。
挙動
  • winsymlinks
  • winsymlinks:lnk
ショートカットを作成する。
MSYS2上からはシンボリックリンクとして見える、かつWindows上ではショートカットになる。
  • winsymlinks:native
  • winsymlinks:nativestrict
シンボリックリンクを作成する。
Windows上でもシンボリックリンクになるが、管理者権限またはWindows 10かつDeveloper modeである必要がある。
失敗した場合は単にコピーになるか、作成されない。
  • 上記以外
  • 未設定(デフォルト)
ファイルをコピーする。
ソースも確認したのでこれであっているはず。ちなみに上記はsymlink(2)の挙動なので、ln -sは多少違うかもしれない(たぶんあってると思うけど)。

【Sagittarius上ではどうしたか】 

シンボリックリンクの作成を失敗したくないというのが大前提としてあったので、最低でもショートカットにフォールバックするようにしたかった。ということで、Sagittarius上では以下のフローでシンボリックリンクの作成をする。
  1.  環境変数のチェック
    1. コピー以外のいずれかならばsymlink(2)を使用する
    2. コピーならばWindows上のシンボリックリンクを試す
  2. ファイルが作成されているかチェック
    1. 作成されているなら終了
    2. 作成されていないなら、環境変数MSYSwinsymlinks:lnkを設定し#1へ 
(Windows上のシンボリックリンクを試す必要ないかもしれないなぁとこれ書いてて思ったので、ファイルチェック→環境変数セット→リトライのフローだけにするか。)

とりあえずこれで、create-symbolic-link手続きがMSYS上では何かしらシンボリックリンクっぽいものを作成することになる。いつも思うがサポートする環境を増やすときはこうい環境特有の workaround が必要になるの辛い。

2018-02-16

MSYS2サポート

Sagittarius 0.9.0からはMSYS2が実験的にサポートされる。実験的と書いているのは単にまだ安定しないのと、他のPOSIX環境と多少挙動が異なる点があるというところからである。挙動に関しては積極的に変えていく可能性があるので、WindowsかつMSYS2を使っている方がにフィードバックを送ってくれると反映される可能性が高い。

MSYS2という環境はCygwinに比べるとWindowsとの互換性を重要視したPOSIXエミュレーターに見える(あまり使っていないのでぱっと見の感想)。現状で一番困っているのはシンボリックリンクの扱いで、例えばPOSIXのsymlink(2)は必ず失敗する。失敗してエラーを投げられると困るので、現状はシンボリックリンク関係の部分はWin32APIのCreateSymbolicLinkを使って凌いでいるが、このAPIは管理者権限を要求するので普通にSagittariusを起動して使用すると失敗する(何も作ってくれない)。

個人的にWindows上で使うPOSIXエミュレータを以下の理由からMSYS2に乗り換えようかなと思っている:
  • プロセスの作成が失敗しない
  • メモリの制限がない
この二つは結構重要で、最近書いているこれはプロセスを多用するからである。そうすると上記のシンボリックリンクが問題になってくる。

代替案としては
一つ目は割と茨の道で、理由としては.lnk拡張子がネックになる。二つ目はボリューム跨ぎができない、ジャンクションポイントは絶対パスが必要になるとう結構制限がある。

個人的には管理者権限を要求してもいいかとは思うが、もしそれができない状態かつMSYS2上でscheme-envを使いたいという状況が出てきた場合に困りそうである。いろいろな意見がほしいところである。

2018-02-09

TLS実装2(完)

Sagittarius上にビルトインなTLSを実装し終えた。POSIXな環境(Linux、OSX)ではOpenSSLをWindowsな環境ではSchennal、CygwinとMSYS2上ではあればOpenSSL、なければSchannelを使うという感じになっている。

今までPure SchemeだったのがCでの実装になったのだから性能にもなにかしら影響があるだろうと思ってベンチマークを取ってみた。

意外にも有意な差がない。Schemeの性能が高いんだと喜びたいところだが、以下のような理由だろう:
  • 暗号、復号及びMACは元々Cでやられている
  • TLSパケットの送受信は上記がメイン
  • (おそらく)OpenSSLはスタックの破棄みたいなセキュリティ上のオーバーヘッドがある
元々ビルトインにしようと思ったのは別の理由からなので、あまりおまけを期待しすぎてはいけない。よりセキュアになったということにしておく。

一応2月のリリースには間に合ったということで。

2018-02-01

TLS実装

Scheme環境ツールでTLS実装をOpenSSLかSSPIに切り替えると書いて、実際にそうしているのだが、SSPI(正確にはSchannel)の実装が辛い。何が辛いか?
  • ドキュメントが飛び飛び
    • MSDNにAPIの説明はあるんだけど、フローの説明がない
    • 例えばX.509証明書と秘密鍵をメモリ上から読み取り、紐付けるということが(今のところ)ドキュメントから読み取れない
  • サンプルが少ない
    • Schannel SSL辺りでぐぐっても片手で数えられるくらいしか見つからない
    • それらのサンプルもあんまり使い込まれてないらしく、普通の用途しかサポートしてない
      • X.509証明書はファイルから読むとか
      • だめならSchannelでセルフ署名を作るとか
OpenSSLの実装は例も多く(逆にドキュメントがしょぼい気がする)割と簡単にいったので「これはSchannelもスムーズに行くのでは?」という淡い期待があったのだが、シャボン玉より簡単に弾けた…

これこの後後方互換を保つとかの作業もあるのだが、2月のリリースに間に合うのかね?不安になってきた。

2018-01-26

Mingwサポート

Ubuntu上でもWindows用のコードをコンパイルしたいと思いMingwのサポートをしてみた。実際にMingwでコンパイルされたバイナリを動かしていないのでコンパイルできる以上の意味を今のところ持っていない(そのうちCIでは回すようにしたいが、優先度的には多少低め)。

とりあえずMSVCとの非互換な部分は大きく以下:
  • struct timespecが存在する
  • .ccファイルをC++として認識する(と思われる)
    • Boehm GCのコンパイルで困らされた
  • __try__finallyがない
    • __try1__except1はうまいこと動かない
    • リンカがエラーを投げる
    • なので今のところ代替手段なし…
  • 細かいライブラリの名前が違う、かつ#pragma commentは動かない
    • ws2_32とか
これら以外は大きなコードの変更もなくコンパイルできた(動いたとは言っていない)。

Twitterで「移植性を考えたらC言語を使うしかない」というのをみたが、C言語を使ってかつ多大な努力を払わないと移植性なんて得られんなぁ…同じWindows上のバイナリを作るのにもこれだよ。

上記にもあるが、コンパイルできるだけで動くとは言っていないので、Mingwの環境がある方だれか試してくれないかなぁ(チラチラ Issue上げたりPR送ってくれたりするとなお嬉しいなぁ(チラチラ

2018-01-25

Scheme環境ツール

各種Scheme処理系をある程度便利に使いたいと思い、こんなんを書いている。なんでそんなことを思ったかというと:
  1. タイムゾーンなSRFIを書こうかな
  2. ポータブルな実装がいるよなぁ
  3. 処理系を簡単にスイッチできないかなぁ ← これ
完全にYak shavingである。今時かどうかは知らないが、目標としてはインストール不要の完全オンデマンドみたいなのを目指している。なので、READMEにも書いたがこんな感じでインストールかつ使用できる。
$ curl https://raw.githubusercontent.com/ktakashi/scheme-env/master/bin/install.sh | sh
$ export PATH=$HOME/.scheme-env/bin:$PATH
$ scheme-env install chibi-scheme
$ scheme-env switch chibi-scheme@master
$ scheme-env run
これでChibi Schemeをソースからビルドしてかつ使用可能にしてくれる。ソースからビルドするので、各処理系が要求する依存関係は予めインストールされている必要がある(流石にそこまで面倒見れない)。ちなみに最初のShellスクリプト以外は全部Scheme(Sagittarius)で書かれている。

今のところインストール可能な処理系はChibiとSagittariusだけだが、すぐにGaucheや他の処理系もサポートされる予定。

動作環境はUbuntuを想定しているが、ホスト処理系(Sagittarius)の要求するパッケージのインストールをしないのであれば、 他のPOSIX環境でも動くはず(自信ない)。Cygwinはプロセスを多用していることもあって多分無理(誰かこの問題直して…)。MingwサポートしてMsys上でとかかなぁ。

ここからはどうでもいい話なのだが、実はこれを作ったおかげで0.9.0のリリースが大分遅れることになった。理由は以下:
  •  Ubuntu上でHomeが暗号化されているとC Assertで落ちる
    • キャッシュファイルのPATHが長すぎるのが原因だけど、そのままだとあまりに不便
  • (rfc tls)でbitbucket.orgに繋げられない
    • TLS 1.2実装の問題
    • 流石に温かみのある手作り実装に疲れてきたのでOpenSSLやSSPIに切り替える
1つ目は直したんだけど、2つ目が流石に時間がかかる。OpenSSLは楽なんだけど、SSPIがねぇ。まぁ、来月くらいにはリリースできるんじゃないかなぁくらいの気持ちではいるが、さて。

2018-01-14

リストの作成

こんなツイートをしたが、どう書くかというのは示してなかったので(iPhone上からだったので)、せっかくだしブログのネタにしようという話。



リストの生成方法にはいくつか方法があるが、今回は有名どころを二つ書いておく。
cons + reverse
cons + reverseはまぁよく使われるパターンで、この名前で呼べば大体どんなものかの想像がつくくらいだ(と思う)。ツイートで言及しているQiita記事にある手続きfをこのパターンで書き直してみた。
(define (f/cons+reverse init n)
  (let loop ((i 0) (r init))
    (if (< i n)
        (loop (+ i 1) (cons (tent_func (car r)) r))
        (reverse r))))
nthが何をしているものなのかよくわからなかったが、おそらく(nth -1 L)はリストの最終要素を取り出すものと予想。(そのように実装して元の手続きを動かしたらそれっぽい値返したし。)
これだとappendがなくなるので、O(n^2)がO(n)になる。一時リストの生成が嫌ならreverse!を使えば生成されるリストもたかだか一個になる。
doで以下のように書くこともできる。
(define (f/cons+reverse init n)
  (do ((i 0 (+ i 1)) (r init (cons (tent_func (car r)) r)))
      ((= i n) (reverse r))))
どちらがいいかは好み次第。
ちなみに、reverseがないので保留と言われた(記憶、Twitter上で見つからん)のだが、reverseはこんな風に書ける。
(define (reverse l)
  (do ((l l (cdr l)) (r '() (cons (car l) r)))
      ((null? l) r)))
RnRS準拠の処理系なら絶対持ってるはずだが、自作の処理系だったという可能性も踏まえてみる。 (積極的に無駄な処理をするコードを直していくスタイル)

unfold
Schemeの標準にはないが、SRFI-1 (R7RS-large)にはunfold という手続きがある。これを使うと上記は以下のように書ける。
(define (f/unfold init n)
  (unfold (lambda (x) (< (car x) 0))
          cdr
          (lambda (x) (cons (- (car x) 1) (tent_func (cdr x))))
          (cons n init)))
こっちは多少メモリ効率が悪い(シードの生成に必ずペアを作ってる)し、unfoldの使用方法が多少変則的な感はある。

ループ内で(append result (list ...))みたいなコードを見つけたら積極的に書き換えていきたいところである。

2018-01-08

日付と時間 その2

前回の続き

Shiroさんの案では calendar 自体が (y, m, d, H, M, S) の組を持つというもので、SRFI-19 の date は (y, m, d, H, M, S) + timezone というもの。っで、 calendar の種類は複数あって、例えばグレゴリアン歴の2018年1月8日は D = (2018, 1, 8, 0, 0, 0)だけど、これと同等のユリウス暦は2017年12月26日 D = (2017, 12, 26, 0, 0, 0)になる。更に、それぞれの calendar が時間の演算を持つというもの。例えば calendar A では一日が30時間だとすると、その calendar における日付B+1日というのは Gregorian calendar では1日と6時間を足したことになるが、この差異を calendar がよしなにしてくれるというもの。(意図を正しく理解していれば)

個人的には非常に可搬性が高く、火星に行っても火星用の calendar を作成すれば使えるいいアイデアだと思うんだけど、地球上でプログラムをするのであれば、RFC3339で定義されている時間で概ね事足りると思ったりもする。(まぁ、宇宙開拓史みたいなゲームを作って火星のカレンダーが必要だみたいなのはあるかもしれんが、レアケースと割りきってもいいよね?)

そうするとこんな感じで簡略化するとよくね?という気にもなる。
日付 D = (y, m, d, H, M, S)
カレンダー C
タイムゾーン TZ
として、日付Dは常にRFC3339で表現できるものとする。日付の演算はデフォルトでは以下のようにできる:
D+1d = (y, m, d+1, H, M, S) = (y, m, d, H + 24, M, S) = ...
カレンダーCは例えばユリウス通日等の計算や、そのカレンダー上での一日を日付D上に加算するのに使ったりするこんな感じ。
(calendar:days->date calendar:julian 0) ;; -> D(-4714 11 24 12 0 0)
タイムゾーンTZはまぁ、普通にタイムゾーンで、SRFI-19の date は D+TZ になる。

目を凝らすと日付Dは (y, m, d, H, M, S) + RFC3339 と言えなくもなく、デフォルトの演算は calendar Crfc3339 によって提供されるものとも言えるので、多少の利便性だけとも言えなくもない。じゃあ、最初からそうすればよくね?という気もしてきた。う〜ん。

2018-01-04

日付と時間

日付や時間はプログラムを書く上で鬼門になりがちなものである。例えば、2018年1月4日11時10分と言った時に、この時間は常に同じ時間を指すだろうか?答えはNoである。例えばオランダと日本では別々の時間になる。同一時刻を指すようにするにはタイムゾーンの指定が必要になる。

Schemeには日付と時間を扱うSRFIがある。SRFI-19である。多くの場合はこのSRFIで事足りるのだが、局所時間を扱う際にこまることがある。例えば上記の日付をタイムゾーンを気にせず表せない。
;; これはエラー
(make-date 0 0 10 11 4 1 2018 #f)
;; これは常にUTC
(make-date 0 0 10 11 4 1 2018 0)
;; これだと常にUTC+1:00、夏時間どうする?
(make-date 0 0 10 11 4 1 2018 3600)
とりあえず全ての日付はUTC+0にして扱うという手もあるのだが、バッチ処理を日付が変わるときに行うというようなことが面倒になる。また、このSRFIには日付のみ、時間のみを扱う術がない。例えば11時15分という時間と(make-date 0 0 15 11 0 0 0 0)は等価か?あるいは2018年1月4日は(make-date 0 0 0 0 4 1 2018 0)と等価であるかということである。答えはケースバイケースであるとは思うが、多くの場合はNoではないだろうか?

Javaは1.8からjava.timeというパッケージが導入された。これは上記のようなジレンマを解決してくれそうな雰囲気がある(使ったことないので未確認)。Sagittariusにもこれと似たようなものを入れるべきかなぁと考えている。こんな感じでの階層だろうか?
+-------+   +-------+
| date  |   | time  |
+---+---+   +---+---+
    |           |
    +-----+-----+ 
          |
    +-----+-----+   +-------------+
    | date-time |   | zone-offset |
    +-----+-----+   +------+------+
          |                |
          +-------+--------+
                  |
       +----------+----------+
       | offsetted-date-time | <-- SRFI-19 dateと等価
       +---------------------+
時間が常にローカル時間かというのはわからないので、offsetted-timeみたいなのもあっていいかもしれない。timeだと名前が被るから別名にする必要も有りそうだが。

ちょっと考える必要があるが、割と早めに導入したい気持ちもある。

追記
Shiroさんのアイデア


これを使ってSRFI-19を実装するとすると、Calendarは3つ必要になりそう。(たぶん)Gregorian、JulianとModified Julian。内Julian calendarとModified Julian calendarは要らんかもしれんが、あると綺麗っぽい。これを踏まえると以下のようにするといいだろうか?
                  +------------+
                  |  timezone  |
                  +------------+
                  | - Offset   |
                  +------+-----+
+------------+           |
|    date    |           |
+------------+           |
| - timezone |<>---------+                  +------------+
| - calendar |<>------------+               | date-tuple |
+------------+              |               +------------+
                 +----------+----------+    | -(Y,M,D)   |
                 |      calendar       |    +-----+------+
                 +---------------------+          |
                 | - YMD: date-tuple   |<>--------+
                 | - HmS: time-tuple   |<>--------+
                 +---------------------+          |
                                            +-----+------+
                                            | time-tuple |
                                            +------------+
                                            | -(H,m,S)   |
                                            +------------+
calendarは演算手続きの集合にして、こうした方がいいだろうか?
+--------------+                                  +------------+ +------------+
|   calendar   |                                  | date-tuple | | time-tuple |
+--------------+                                  +------------+ +------------+
| - operations |                                  | - (Y,M,D)  | | - (H,m,S)  |
+------+-------+                                  +------+-----+ +------+-----+
       |                                                 |              |
       |             +-------------------+               |              |
       |             |     local-date    |               |              |
       |             +-------------------+               |              |
       |             | - YMD: date-tuple |<>-------------+              |
       +-----------<>| - calender        |                              |
       |             +-------------------+                              |
       |                                                                |
       |             +-------------------+                              |
       |             |     local-time    |                              |
       |             +-------------------+                              |
       |             | - HmS: time-tuple |<>----------------------------+
       +-----------<>| - calendar        |
                     +-------------------+
                              :
                            so on
                              :
どっちもしっくりこない感じがするなぁ。もう少し考える必要がありそうだ。

2018-01-02

謹賀新年

年末年始を病院で過ごしていたのでいわゆる「今年を振り返って」みたいなのが書けなかった。(僕自身が病気ではないです、後は察してください)

昨年はいろいろあって、一年が長かったのか短かったのかよく分からない状態である。少なくとも去年の1月に何をしていたのかは全く思い出せない。引っ越しがあったのでそれに記憶の大部分を取られている気もする。

今年の抱負と目標:
  • ジムに週二で通う
    • 割と毎年言ってるなぁ
  • Scheme関連のブログ記事を週一で書く
    • 習慣にすればなんとか行けるかなぁ
    • 最悪「Sagittariusの中身」とか書きだせば…
  • 今作ってるWebアプリを公開する
    • モチベーションが落ち気味なので宣言しておけば嫌でもやるだろう
あんまり関係ないけど、転職も考えていたりする。完全リモートワークOKならオランダ国外でも問題ないです。