設計
リテラルの区切りは ,
配列リテラル、レコードリテラル、フィールド宣言の区切りは , です。
Object subclass: #Order fields: (code : Str, qty : Int).
Order class >> code: c qty: q = Order fields: { code: c, qty: q }.
Form class >> keys = #[ 'code', 'qty' ].
, は連結演算子でもあります。どちらとして読まれるかは、開いている一番内側の括弧で決まります。#[ と { の中では区切り、( と [ の中では連結演算子です。そのため、レコードリテラルの中で連結するときは括弧で囲みます。
self copyWith: { label: (name , ' 様') }
. は文の終わりを表します。リテラルの中でも、ブロックの中の文は . で区切ります。
#[ [ x. y ], [ z ] ]金額は Decimal
小数リテラルには s を付けます。Smalltalk の ScaledDecimal と同じ形です。1234.50s は Decimal で、s を付けない 1234.50 は Float です。
1234.50s "Decimal。桁数は書いた桁から決まる"
1.5s2 "Decimal。桁数を指定する。1.50"
3s2 "Decimal。小数点は要らない。3.00"
1.5 "Float"
指定した桁数より多い桁を書くとエラーです。リテラルの時点で丸めることになるためです。
1.555s2 has more digits after the point than its scale(0.1s + 0.2s) displayNl. "0.3"
(1.00s dividedBy: 3.00s roundedTo: 2) displayNl. "0.33"
Decimal に / はありません。割るときは、結果を何桁にするかと、どの方向に丸めるかを指定します。dividedBy:roundedTo: dividedBy:truncatedTo: dividedBy:floorTo: dividedBy:ceilingTo: dividedBy:awayFromZeroTo: があります。すでにある値を丸めるときは、dividedBy: を外した同じ五つの名前です。
業務システムでは金額の計算誤差が許されないこと、そして丸めが必要な箇所では、どこでどちらに丸めるかをプログラムに書かせることが理由です。
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 日に addMonths: 1 を送った結果は 2 月 28 日(閏年なら 29 日)で、3 月 3 日にはなりません。業務のコードで最も多い日付の不具合がこれです。
日付と時刻に関する型は五つあります。Date DateTime Instant TimeZone は名前ごと Lambda Prelude のもので、Time だけがこの言語のものです。
| 型 | 内容 |
|---|---|
Date | 暦の日付。時刻もタイムゾーンも持たない。受注日、納期、締め日 |
Time | 時刻。日付もタイムゾーンも持たない。締切、営業時間、予約枠 |
DateTime | 壁の時計の日付と時刻。タイムゾーンは持たない。人が入力する値や DATETIME 列の値 |
Instant | 絶対時刻。作成時刻やログ |
TimeZone | IANA の名前で指すタイムゾーン。ある瞬間が、その土地の時計で何時になるか |
Time どうしの足し算はできません。23:45 の三十分後が翌日なのか入力誤りなのか、時刻だけでは判断できないためです。秒に変換して計算し、Time fromSeconds: で戻します。範囲外の値は、巻き戻さずにエラーにします。深夜を越えた予約枠は、そこで分かります。
DateTime は絶対時刻ではありません。2026-03-08 02:30 は、タイムゾーンによって二回存在したり存在しなかったりします。だから DateTime と Instant を行き来するときはタイムゾーンを指定します。型がそれを書かせます。指定の形は、名前で指すタイムゾーン、UTC から東への秒数、実行環境のタイムゾーンの三つです。
dt asInstantIn: (TimeZone named: 'America/New_York').
dt asInstantOffset: 32400. "+09:00"
dt asLocalInstant.
名前で指したタイムゾーンは、その土地の時差の移り変わりを知っています。だから同じ瞬間が、土地によって違う日になります。
i := Instant parse: '2026-08-10T03:00:00Z'.
(i dateIn: (TimeZone named: 'Asia/Tokyo')) displayNl. "2026-08-10"
(i dateIn: (TimeZone named: 'America/Los_Angeles')) displayNl. "2026-08-09"
このためのデータは同梱しません。実行環境の Intl を使います。ブラウザも Node も持っているものです。どの版かは TimeZone databaseVersion が答えます。ただし答えるのは実行環境が言う場合だけで、Node は言い、ブラウザは言いません。データベースに無い名前はエラーです。大文字小文字だけが違う名前もエラーです。
TimeZone named: no time zone is named asia/tokyo
夏時間の切り替えで飛ばされた時刻はその分だけ後ろへずれ、二度ある時刻は早い方の瞬間になります。Java の ZonedDateTime.of と同じ読み方です。
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 asTime は、文字列を厳密に読んで Maybe を返します。'1,000' も '26:30' も none です。連携データの形式誤りを見逃さないためです。
日付とタイムスタンプを読むのは Date parse: DateTime parse: Instant parse: で、こちらは none ではなくエラーにします。暦に無い日を送ってくる連携は、そのまま進める値ではなく、壊れた連携です。Date parse: が読むのは yyyy-MM-dd だけ、DateTime parse: はタイムゾーンを書いた文字列を受け付けず、Instant parse: は逆にタイムゾーンの無い文字列を受け付けません。サーバから来るタイムスタンプはオフセットを持っていて、DateTime は壁の時計の読み取りだからです。
人が入力欄に打った文字列は、厳密な形式では来ません。全角の 10、桁区切りのコンマ、斜線区切りの日付、9:05 のような一桁の時刻が来ます。そのために asEnteredInteger asEnteredDecimal asEnteredDate asEnteredTime asEnteredDateTime があります。これらは表記の揺れを正規化してから厳密に読み、Maybe を返します。12x は none のままで、2026/2/30 は日付として認めません。
二組に分けた理由は、同じ文字列に対して、連携データと人の入力では期待する結果が逆になるためです。厳密な読み方だけだったとき、次のコードで、1,000 と入力した利用者の数量が元の値に戻る不具合がありました。
draftQty asInteger orElse: [o qty]型システム
型推論は Hindley-Milner です。型注釈は書けますが、必須ではありません。サブタイピングはありません。
メッセージ送信の型検査は三段階です。受信側の型が分かっていれば、その場でセレクタを検査します。まだ決まっていなければ保留し、他の場所でその型が決まった時点で検査します。最後まで決まらないものは、そのまま通します。
三段目は意図的に残している穴です。ただし、ここを通るのは実行できないコードだけです。その送信に到達するには型の決まらない値が必要ですが、値を作れば型が決まり、決まった時点で検査されます。
クラスは名前で区別します。継承はありません。共通の振る舞いは、プロトコルで持たせます。
フィールドの型はクラス名だけで書きます。クラスはフィールドの数を型引数として外に出さないので、held : Vm と書けば Vm が何を持っていても通ります。
型を書いていないフィールドを持つ値クラスは、そのフィールドについて多相です。Object subclass: #Box fields: (item). は、ある場所では Int を、別の場所では Str を入れられます。アクタは多相ではありません。ActorRef[C] には推論で増えた型引数を置く場所がないので、型引数が要るアクタは宣言に書きます。
局所束縛は多相ではありません。f := [:x | x] は束縛の時点で一つの型に決まります。多相なのはメソッド、クラス、フィールドで、:= は含みません。
プロトコル
case はありません。「このどれか」を表すには、種類ごとにクラスを作り、プロトコルで必要なものを宣言します。
Protocol subclass: #Status requires: (#label #colour).
値クラス向けのプロトコルはセレクタを並べます。アクタ向けのプロトコルはメッセージクラスを並べます。アクタのハンドラはメッセージクラスの名前を持つからです。
Protocol subclass: #Door requires: (Open Close State).
プロトコルを実装すると宣言したクラスがメソッドを書き忘れると、コンパイルエラーになります。
Cancelled claims protocol Status but does not implement "colour"
直和型と case のある言語では、種類を追加したときに分岐の書き漏らしがコンパイルエラーになります。LPScript では、種類を追加したときに実装の書き漏らしがコンパイルエラーになります。
プロトコルは型ではない
プロトコルは、クラスの分からない値について分かっていることです。Lambda Prelude と同じ型クラスです。型変数は、使われ方から付いたプロトコルの制約を持ち('a{Comparable})、その型変数が型に決まった時点で、そのクラスがそれぞれを名乗っているかを検査します。
type Kg does not implement protocol Comparable
そのため、型を書く場所にプロトコルは書けません。フィールドの型にも、extern の署名にも書けません。どの区分でも入るフィールドは型を書かずにおき、クラスはそのフィールドについて総称になります。
Object subclass: #Order fields: (code : Str, status).
Order >> line = code , ' ' , status label , '(' , status colour , ')'.
status に Status が付くのは、status label を送っているからです。Status を名乗っていないクラスの値を status に入れて Order を作ると、その行がコンパイルエラーになります。
type Plain does not implement protocol Status
同じセレクタを持つプロトコルが二つ見えているときは、(e : P) でどちらかを指定します。クラスの分からない値にセレクタを送ったときの読み方は、すべての値が答えるもの、その値にすでに付いているプロトコル、見えているプロトコルのうちそれを持つ唯一のもの、プログラム全体でそれに答える唯一のクラス、最後に引数の型、という順です。どれでも決まらなければ、その送信は保留します。
プロトコルがセレクタを持つというのは、requires: に挙げているか、その名前のメソッドを書いているか、どちらかです。だから Comparable の付いた値には、要求されているのが < だけでも、> も同じように届きます。
必須セレクタの型は書きません。プロトコルの付いた値へのその送信を、プログラム全体から集めて決めます。だから、何かを要求するプロトコルがあるプログラムは二回推論します。同じプロトコルを名乗るクラスどうしの型が一致している必要はありません。Score >> < は Score を取り、Kg >> < は Kg を取り、どちらも Comparable を名乗れます。受け手のクラスが分かっているときはそのクラスのメソッドを直接呼び、< のような演算子は JavaScript の演算子として出ます。
組み込みの型は、必須セレクタを自分で全部持っていれば、そのプロトコルを名乗っているものとして扱います。Comparable を求める場所に Int をそのまま渡せます。プロトコルの既定メソッドが付くわけではありません。言語が持つプロトコルは二つで、Arithmetic(+ - *)は Int Float Decimal が、Numeric(/ \\)は Int と Float が名乗ります。Decimal が Numeric に入っていないのは、Decimal の除算が桁数と丸め方を取るためです。
Enumerable と Comparable
この二つは言語の組み込みではなく、コンパイラに付いてくる標準ライブラリです。imports: に名前を書くと、そこから読みます。
imports: (Enumerable, Comparable).
Enumerable は inject:into: を要求し、collect: select: reject: sum max includes: joinWith: flatCollect: detect:ifNone: count: などを与えます。Comparable は < を要求し、> <= >= min: max: を与えます。クラスの側に書くのは一つで済みます。
Object subclass: #Bag fields: (items) protocols: (Enumerable).
Bag class >> of: xs = Bag fields: { items: xs }.
Bag >> inject: acc into: blk = items inject: acc into: blk.
Array と Str も Enumerable を名乗ります。sum joinWith: flatCollect: は Array の組み込みではないので、配列に対して使うときもこの import が必要です。
Object
Object は、型の検査が効かない値です。asObject がこれを返します。どの型とも一致し、何も束縛しません。Object への送信は実行時に解決され、結果はまた Object です。違うクラスの値を一か所に入れるときに使います。
(#[ draft asObject, sent asObject, cancelled asObject ] collect: [:o | o line ]) displayNl.
Object に (e : T) と書けば、中身が何であるかを指定でき、そこから先はまた検査されます。
System allClasses はプログラムのクラスを全部返し、クラスは name と protocols を答えます。実装を名前から引くのはこれで書きます。規則が JSON ファイルや設定表にあり、そこに書かれた名前から実装クラスに辿り着きたい場合です。
greeters := System allClasses select: [:c | c protocols includes: 'Greets'].
(greeters collect: [:c | (c fields: {}) greet]) displayNl.スレッドを跨げる値
ActorRef は、そのアクタがどのスレッドで動いているかを持ちません。同じスレッドでも Web Worker でも、書き方は同じです。そのため、メッセージで渡す値は複製可能でなければなりません。
ブロックは複製できません。作られた場所の変数を参照しているためです。ブロックを持つ状態を Worker に置こうとすると、原因のフィールドを示してコンパイルエラーになります。
組んだ Regex も複製できません。エラーは理由まで言います。
the message Go cannot cross an actor boundary: it is Go, whose pat is Regex,
and a compiled pattern belongs to the engine that compiled it
Decimal を JavaScript の関数に渡す宣言も、コンパイルエラーです。JavaScript に小数の型が無いので、桁を保つ(asString)か計算できる形にする(asFloat)かを、呼ぶ側が選びます。
型検査の単位
imports: で繋がったモジュールは、まとめて解析し、まとめて推論します。分割コンパイルはありません。
理由は、三つの検査が全体を見ないと成り立たないためです。プロトコルの必須セレクタの型は、プログラム全体のその送信から決めます。クラスの分からない値への送信は、プログラム全体でそのセレクタに答えるクラスが一つならそのクラスと読みます。スレッドを跨げるかどうかは、メッセージが運ぶ値の内側まで辿ります。
型検査の速度は問題になりません。280 行の受注画面の型検査は 0.000 秒(計測の分解能未満)でした。81,000 行のプログラムでも 1 秒で、行数に対して線形です。