帳簿を書く
.book のファイル一つが、帳簿一つです。帳簿には、ものを数える単位、それを持っておく勘定と勘定ごとの境界、勘定から勘定へ動かす振替の種類を書きます。このページでは、マーケットプレイスの例を、日本語の版
examples/marketplace/marketplace.ja.book
で順に読みます。マーケットプレイスが店ごとの売上金を持つための帳簿で、売上では、買い手が払った額を店の取り分と手数料に分けます。返金では両方を戻し、店への振込は、銀行が受け付けるまで押さえておきます。上限と、期限の切れる仮押さえは、ポイントの例
examples/points/points.ja.book
から二か所を引いて見ます。言語の全体は、一ページにまとめた言語のリファレンス(英語)にあります。
最初の行
# 売上は、買い手が払った額を店の取り分とマーケットプレイスの手数料に分ける。額は呼ぶ側が計算して渡す
book マーケットプレイス v1
description "店ごとに売上金を持つ。売上は、買い手が払った額を店の売上金と手数料に分ける(額は呼ぶ側が計算して渡す。手数料は規則が決める)。振込は銀行が受け付けるまで押さえ、返金は両方を戻す"
book マーケットプレイス v1は、帳簿の名前とバージョンです。バージョンは勘定や振替の中身には入らないので、v2にしても、データベースにある残高と仮押さえはそのまま続きます。新しいバージョンで変えてはいけないものは、chobo check --diff-baseが見つけます(何を検査するか)。#から行の終わりまでがコメントです。ブロックは字下げで作ります。descriptionには、帳簿が何のためのものかを書きます。人が読むページ(chobo doc)に載り、chobo apiの出力にも入ります。勘定と振替にも書けます。
キーワードは英語で、名前(帳簿、単位、勘定、振替、引数、理由)はどの言語でも書けます。例はどれも英語の版が元で、名前を日本語にした版を <名前>.ja.book として横に置いています。この例の英語の版 marketplace.book では、売上金の勘定が次のようになっています。
単位
額は、いちばん小さい単位(円なら 1 円、ドルなら 1 セント)で数えた整数です。unit 円 なら 1 円ずつ数えます。小数のある通貨は、scale で小数の桁を書きます。英語の版の unit USD scale 2 は 1 セントずつ数える単位で、帳簿に書く数(境界や額)を小数 2 桁で書き、chobo doc もそう見せます。12.50 は 1250 セントです。お金でない単位、たとえば unit 個 は、個数を数えます。一つの移動は、同じ単位の勘定から勘定へ、その単位の額を動かします(E010)。chobo は単位の換算をしません。くわしくはお金と単位にあり、お金が税込か税抜かの書き方もそこで説明しています。
勘定と境界
account 売上金(店: string) : 円
description "マーケットプレイスが店に払う分"
at least 0 refused as 売上金不足
account 買い手 : 円 outside
account 手数料収入 : 円 outside
account 銀行 : 円 outside
- 店ごとに一つの勘定。 勘定の種類は引数で分かれ、引数は文字列です。
売上金(店-2)が、一つの店の売上金です。勘定は最初に使われたときにでき、残高は 0 から始まります。 - 残高は、入った量から出た量を引いたものです。
outsideを付けた勘定は、帳簿の外を表します。ここでは、買い手、マーケットプレイスが受け取る手数料、銀行の三つです。外の勘定に境界は無く、そこから出ていくにつれて残高はマイナスになります。 - 境界と、拒否の理由。
at least 0 refused as 売上金不足は、残高を 0 より下にする振替を通さず、売上金不足で拒否するという意味です。この名前は呼ぶ側に返る理由で、ワークフローのタスクが宣言するエラーの名前にもなります。帳簿の中の勘定には境界が要り(E020)、外の勘定には書けず(E021)、どの境界にも理由の名前が要ります(E023)。
ポイントの例には上限もあります。会員のポイントは 0 を下回らず、100 万を超えません。
下限は 0 より上(棚に 3 個は残す)にも、0 より下(マイナス 50000 円まで後払いできる)にもできます。上限は 0 以上です(E022)。境界は勘定の種類ごとの定数です。勘定ごとに違う枠、たとえば会員ごとの与信枠は、その枠を一つの勘定にして、振替で満たします。
帳簿に書ける条件は境界だけで、これはわざとそうしています。境界が見るのは一つの勘定なので、その勘定を書き換える書き込みの中で確かめられ、あいだにほかの読み取りが入りません。二つの勘定にまたがる条件は、勘定を一つ足して、その勘定の境界として書きます。返金の例は「返金は売上を超えない」をそう書いています(設計)。
振替
transfer 売上(注文: string, 店: string, 店の取り分: 円, 手数料: 円)
description "店の取り分と手数料を足すと、買い手が払った額になる。手数料は呼ぶ側が計算する"
key 注文
move 店の取り分 from 買い手 to 売上金(店)
move 手数料 from 買い手 to 手数料収入
- 引数は、文字列か、単位の付いた額です。
key 注文:売上は、注文ごとに一度だけ動きます。同じ引数でもう一度呼ぶと、何もせずにdone_beforeを返すので、リトライしても二度は動きません。注文が同じで、ほかの引数が違う呼び出しは、key_conflictで拒否されます。額の引数はキーに入れられません(E031)。入れると、額だけが違う二つの呼び出しが、両方とも通ってしまうからです。move <額> from <勘定> to <勘定>:一行が一つの移動です。額は引数かリテラルで、勘定には、振替の引数(か文字列のリテラル)を位置の順に渡します。移動は書いた順に行われ、全部通るか、一つも通らないかのどちらかです。
振替は何も計算しません。店の取り分と手数料は、呼ぶ側が自分で計算するか rulec の規則に計算させてから、振替に渡します。返金は両方を戻します。
transfer 返金(返金番号: string, 店: string, 店の取り分: 円, 手数料: 円)
description "手数料と店の取り分を、両方とも戻すか、どちらも戻さない"
key 返金番号
move 手数料 from 手数料収入 to 買い手
move 店の取り分 from 売上金(店) to 買い手
移動は一つずつ、前の移動が残した残高で確かめます。店の売上金が足りなければ、2 つ目の移動が 売上金不足 で拒否され、1 つ目の移動も元に戻って、何も動きません。移動をそのときの残高で確かめるので、移動の順序で結果が変わることがあります。ある勘定から取る移動を、同じ勘定へ入れる移動より先に書くと、二つを合わせれば足りる場合でも拒否されることがあります。検査はそれを警告します(W103。何を検査するか)。
仮押さえ
transfer 振込(振込番号: string, 店: string, 額: 円)
description "銀行振込を始めたときに押さえる。銀行が受け付けたら確定し、失敗したら取り消す"
key 振込番号
pending never expires
move 額 from 売上金(店) to 銀行
pending を書くと、振替はまず押さえます。操作は三つあります。hold は、移動が動かすはずの量を押さえます。post は仮押さえを確定します。全部でも、渡した額だけでもよく、残りは元に戻ります。void は仮押さえを取り消します。振込の仮押さえは期限が切れず、銀行からの返事で、確定か取消のどちらかになります。仮押さえを、期限が来たら切れるようにもできます。ポイントの例では、会計で使うポイントの仮押さえが 15 分で切れます。
transfer 利用(会計: string, 会員: string, 数: pt)
description "支払いが通ったら確定し、通らなかったら取り消す"
key 会計
pending expires after 15 minutes
move 数 from ポイント(会員) to 利用済み
仮押さえは、作るときに、最悪の場合を考えて確かめます。押さえて出ていく分は下限に対して数え、押さえて入ってくる分は上限に対して数えます。逆向きにはどちらも数えません。だから確定が境界を破ることはなく、確定が境界で拒否されることもありません。仮押さえが終わったあとに同じキーで押さえ直すと、done_before を返して何も押さえません。新しく押さえるには、新しいキーを使います。
| 仮押さえの状態 | post |
void |
期限が来る |
|---|---|---|---|
| 押さえ中 | 確定する(期限からあとは expired で拒否) |
取り消す(期限からあとは同じく拒否) | 期限切れになる |
| 確定 | 同じ額なら done_before、違う額なら key_conflict |
already_posted |
— |
| 取消 | already_voided |
done_before |
— |
| 期限切れ | expired |
expired |
— |
| そのキーの仮押さえが無い | no_such_hold |
no_such_hold |
— |
操作が返すもの
すぐに動かす振替の操作は、do 一つです。先に押さえる振替には、hold、post、void があります。どの操作も、done、done_before、理由つきの refused のどれかを返し、拒否されたときは何も変わりません。帳簿全体の拒否されうる理由は、chobo check が並べます。
$ chobo check examples/marketplace/marketplace.ja.book --lang ja
examples/marketplace/marketplace.ja.book: ok
売上.do 拒否されうる理由: key_conflict
返金.do 拒否されうる理由: 売上金不足(売上金(店) at least 0)、key_conflict、already_refused
振込.hold 拒否されうる理由: 売上金不足(売上金(店) at least 0)、key_conflict、already_refused
振込.post 拒否されうる理由: key_conflict、already_voided、over_hold、no_such_hold
振込.void 拒否されうる理由: already_posted、no_such_hold
キー: 売上 は、注文 ごとに一度だけ動きます。キーが同じで 店、店の取り分、手数料 のどれかが違う二度目の呼び出しは、key_conflict で拒否されます。
キー: 返金 は、返金番号 ごとに一度だけ動きます。キーが同じで 店、店の取り分、手数料 のどれかが違う二度目の呼び出しは、key_conflict で拒否されます。
キー: 振込 は、振込番号 ごとに一度だけ動きます。キーが同じで 店、額 のどれかが違う二度目の呼び出しは、key_conflict で拒否されます。仮押さえが終わったあとに同じキーでもう一度押さえると、done_before が返り、何も押さえません。
期限の切れない振込は expired で拒否されることがなく、入れるだけの売上は境界で拒否されることがありません。理由が並ぶのは、そこに至る操作を検査が見つけたときだけだからです。already_refused は、境界で拒否されたキーで、もう一度呼んだときに返るものです。キーは使ったことになったままなので、リトライしても同じものが返ります。もう一度通したいなら、新しいキーで呼びます。続きは何を検査するかに、帳簿を走らせることはシナリオを流すに書いています。