設計

金額は Decimal

ソースに 1234.50 と書くと Decimal になります。二進の浮動小数点ではありません。Float にしたいときは 1234.50f と書きます。

(0.1 + 0.2) printNl.                  "0.3"
(1.00 / 3) printNl.                   "割り切れないのでエラー"
((1.00 / 3) roundedTo: 2) printNl.    "0.33"

割り切れない除算はエラーです。丸めるときは、小数第何位で、どの方向に丸めるかを指定します。roundedTo: truncatedTo: floorTo: ceilingTo: awayFromZeroTo: があります。除算と丸めを同時に指定する dividedBy:roundedTo: もあります。

業務システムでは金額の計算誤差が許されないため、小数の既定を Decimal にしています。

Int は BigInt です。253 を超えても桁は落ちません。

日付は暦の日付

Date は暦の日付だけを持ちます。時刻もタイムゾーンもありません。

JavaScript の Date は日付ではなく時刻です。画面で 8 月 10 日を選ぶと、東京では「8 月 10 日の 0 時」という時刻になります。これを JSON にしてサーバへ送ると UTC に変換されて 2026-08-09T15:00:00.000Z になり、サーバが日付部分だけを読むと 8 月 9 日として保存されます。LPScript の Date は時刻を持たないので、この問題は起きません。

日付の計算は暦に従います。1 月 31 日の一か月後は 2 月 28 日(閏年なら 29 日)です。

日付と時刻に関する型は四つあります。

型内容
Date暦の日付。時刻もタイムゾーンも持たない
Time時刻。日付もタイムゾーンも持たない。締切や営業時間に使う
DateTime日付と時刻。タイムゾーンは持たない。人が入力する値や DATETIME 列の値に使う
Instant絶対時刻

Time どうしの足し算はできません。23:45 の三十分後が翌日なのか入力誤りなのか、時刻だけでは判断できないためです。秒に変換して計算し、Time fromSeconds: で戻します。範囲外の値はエラーになります。

DateTime は絶対時刻ではありません。2026-03-08 02:30 は、タイムゾーンによって二回存在したり存在しなかったりします。DateTime と Instant の変換には、タイムゾーンをオフセット(分)で指定します。タイムゾーンのデータベースは持っていないので、地域名では指定できません。

JSON の数値は桁を落とさない

JSON.parse は 1234567890123456789.12 を 1234567890123456800 にします。LPScript は JSON を独自に読み、数値を届いた文字列のまま保持します。型に変換するのは、値を取り出すときです。

(j at: 'total') asDecimal        "Some(1234567890123456789.12)"

at: は例外を投げません。オブジェクトでない値に at: を送ると null の Json が返るので、(j at: 'a') at: 'b' のように途中を確かめずに辿れます。型に変換するときに Maybe が返り、変換できなければ none です。

機械が書いた文字列と、人が入力した文字列

asInteger asDecimal asDate asTime asDateTime は、文字列を厳密に読みます。'1,000' は none です。連携データの形式誤りを見逃さないためです。

人が入力欄に打った文字列は、厳密な形式では来ません。全角の 10、桁区切りのコンマ、斜線区切りの日付、9:05 のような一桁の時刻が来ます。そのために asEnteredInteger asEnteredDecimal asEnteredDate asEnteredTime asEnteredDateTime があります。これらは表記の揺れを正規化してから厳密に読みます。12x は none のままで、2026/2/30 は日付として認めません。

二組に分けた理由は、同じ文字列に対して、連携データと人の入力では期待する結果が逆になるためです。厳密な読み方だけだったとき、次のコードで、1,000 と入力した利用者の数量が元の値に戻る不具合がありました。

draftQty asInteger orElse: [o qty]

型システム

型推論は Hindley-Milner です。型注釈は書けますが、必須ではありません。サブタイピングはありません。

メッセージ送信の型検査は三段階です。受信側の型が分かっていれば、その場でセレクタを検査します。まだ決まっていなければ保留し、他の場所でその型が決まった時点で検査します。最後まで決まらないものは、そのまま通します。

三段目は意図的に残している穴です。ただし、ここを通るのは実行できないコードだけです。その送信に到達するには型の決まらない値が必要ですが、値を作れば型が決まり、決まった時点で検査されます。

クラスは名前で区別します。継承はありません。共通の振る舞いは、プロトコルで持たせます。

プロトコル

case はありません。「このどれか」を表すには、種類ごとにクラスを作り、プロトコルで実装すべきメソッドを宣言します。

Protocol subclass: #Status requires: (#label #badge #next).

プロトコルを実装すると宣言したクラスがメソッドを書き忘れると、コンパイルエラーになります。

Cancelled claims protocol Status but does not implement "colour"

直和型と case のある言語では、種類を追加したときに分岐の書き漏らしがコンパイルエラーになります。LPScript では、種類を追加したときに実装の書き漏らしがコンパイルエラーになります。

スレッドを跨げる値

ActorRef は、そのアクタがどのスレッドで動いているかを持ちません。同じスレッドでも Web Worker でも、書き方は同じです。そのため、メッセージで渡す値は複製可能でなければなりません。

ブロックは複製できません。作られた場所の変数を参照しているためです。ブロックを持つ状態を Worker に置こうとすると、原因の項目を示してコンパイルエラーになります。

Decimal を JavaScript の関数に渡す宣言も、コンパイルエラーです。JavaScript に小数の型が無いので、桁を保つ(asString)か計算できる形にする(asFloat)かを、呼ぶ側が選びます。

型検査の単位

imports: で繋がったモジュールは、まとめて解析し、まとめて推論します。分割コンパイルはありません。

理由は、三つの検査が全体を見ないと成り立たないためです。プロトコルのメソッドの型は、実装している全クラスから決めます。become: で置き換わった先のクラスが何を受け付けるべきかは、その参照が作られた全箇所から決めます。スレッドを跨げるかどうかは、メッセージが運ぶ値の内側まで辿ります。

型検査の速度は問題になりません。280 行の受注画面の型検査は 0.000 秒(計測の分解能未満)でした。81,000 行のプログラムでも 1 秒で、行数に対して線形です。