コンテンツにスキップ

帳簿を書く

.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 では、売上金の勘定が次のようになっています。

account shop_balance(shop: string) : USD
  at least 0 refused as insufficient_balance

単位

unit 円

額は、いちばん小さい単位(円なら 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 万を超えません。

account ポイント(会員: string) : pt
  at least 0 refused as ポイント不足
  at most 1000000 refused as 上限超え

下限は 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 は、境界で拒否されたキーで、もう一度呼んだときに返るものです。キーは使ったことになったままなので、リトライしても同じものが返ります。もう一度通したいなら、新しいキーで呼びます。続きは何を検査するかに、帳簿を走らせることはシナリオを流すに書いています。