言語の設計
LP-9 は Smalltalk の書き方をした言語です。3 + 4、list size、db exec: sql のように、値にメッセージを送る形で書きます。読みやすさは Smalltalk のままで、中身は、型の検査があり、値が変わらない言語です。
言語の形は、同じ会社の Lambda Prelude と同じです。
型は書かずに、検査される
型は書きません。どの値が何の型かは、使われ方から推論されます。そのうえで、型が合わない所は動かす前にエラーになります。
Object subclass: #Tools fields: ().
Tools class >> pair: x = #[ x, x ].
Tools class >> longer: a than: b = a size > b size.
(Tools pair: 3) displayNl. "#(3 3)"
(Tools pair: 'ab') displayNl. "#('ab' 'ab')"
(Tools longer: 'abc' than: 'de') displayNl. "true"
(3 + 'four') displayNl. "動く前にエラー:Int と Str は足せない"
書く量は型の無い言語と変わらず、誤りは型のある言語と同じように動く前に見つかります。pair: のように、何を渡されても同じように働くメソッドは、そのまま色々な型で使えます。
型を書くのは、外から読んだ JSON のように、推論では決めようがない所だけです。
値は、勝手に変わらない
一度作った値は変わりません。一部を変えたいときは、変えた新しい値を作ります。
Object subclass: #Item fields: (name, price, qty).
Item class >> name: n price: p qty: q = Item fields: { name: n, price: p, qty: q }.
Item >> total = price * qty asDecimal.
Item >> more: n = self copyWith: { qty: qty + n }.
Item >> printString = name , ' x' , qty asString , ' = ' , self total asString.
i := Item name: 'bolt' price: 12.50s qty: 4.
i displayNl. "bolt x4 = 50.00"
(i more: 2) displayNl. "bolt x6 = 75.00"
i displayNl. "bolt x4 = 50.00 のまま"
画面に渡した明細が、どこか別の処理で書き換えられていた、ということが起きません。値を別のアクタや別のスレッドに渡しても、相手が書き換える心配がないので、写しを作る必要もロックも要りません。
値を作るときは、全部のフィールドを与えます。nil が無いので、入れ忘れたフィールドがあるとエラーになります。
共通の部分は、継承の階層なしで共有する
クラスの継承はありません。いくつかのクラスに共通するものは、プロトコルにまとめます。プロトコルは「このメソッドを持っていること」という約束と、それを使って書いたメソッドの組です。
Protocol subclass: #Shape requires: (#area).
Shape >> describe = 'area ' , self area asString.
Shape >> bigger: other = self area > other area.
Object subclass: #Square fields: (side) protocols: (Shape).
Square >> area = side * side.
Object subclass: #Rect fields: (w, h) protocols: (Shape).
Rect >> area = w * h.
s := Square fields: { side: 3 }.
r := Rect fields: { w: 2, h: 5 }.
s describe displayNl. "area 9"
(s bigger: r) displayNl. "false"
Square は area を書くだけで、describe と bigger: が使えるようになります。Shape を名乗ったのに area を書き忘れたクラスは、エラーになります。
親クラスの変更が、思わぬ子クラスの振る舞いを変える、ということがありません。どのクラスが何を共有しているかは、クラスの宣言の一行に書いてあります。
nil で落ちない
nil がありません。無いかもしれない値は Maybe で表し、無い場合をどうするかを書かないと値を取り出せません。
(Json parse: '{"name": "Alice"}') ifPresent: [ :o | (o at: 'name') stringValue displayNl ].
((row at: 'note') orElse: '(なし)') displayNl.
データベースの NULL の列も、壊れた JSON も、Maybe の「無い」として届きます。外から来たデータが壊れていても、プログラムは止まらずに、無い場合の処理に進みます。
失敗は、一か所で受け止められる
0 で割る、無いキーを読む、データベースが SQL を断る、通信の締め切りが過ぎる。失敗は全部、理由の文字列を持った一種類のものです。例外クラスの階層を覚える必要はありません。
r := Error try: [ 10 / 0 ] onError: [ :why | 0 - 1 ].
r displayNl. "-1"
開いたファイルは、どの道から抜けても閉じるように書けます。
[ log append: record ] ensure: [ log close ]
アクタの処理の中で失敗したときは、止まるのはそのアクタだけです(アクタ)。
金額は、正確な十進数
s を付けて書いた数は Decimal で、十進の正確な数です。二進の浮動小数点のように 0.1 + 0.2 がずれることはありません。
Decimal の割り算には、結果の桁数と丸め方を必ず書きます。
(10.00s divide: 3.00s scale: 5 rounding: Rounding halfUp) displayNl. "3.33333"
丸め方に既定がないのは、業務によって正しい丸め方が違うからです。銀行は偶数への丸め、データベースは四捨五入、税は切り捨て。どれを使ったかがコードに残るので、後から読む人にも分かります。丸め方は八つあり、割り切れなければ止まる、という指定もできます。
Int と Decimal は混ぜて計算できません。数量に単価を掛けるときは price * qty asDecimal と書きます。画面に出すときは grouped で三桁ごとに区切れます。
日付は、暦の日付
Date は暦の日付で、時刻もタイムゾーンも持ちません。月末の扱いも暦どおりです。
d := Date y: 2026 m: 1 d: 31.
(d addMonths: 1) displayNl. "2026-02-28"
d dayOfWeek displayNl. "6(月曜が 1)"
(d formattedAs: 'yyyy/MM/dd') displayNl. "2026/01/31"
時計の読みは DateTime、どの国の機械でも同じ一瞬を指すのは Instant、世界の時間帯は TimeZone です。時間帯のデータは実行ファイルに入っているので、どの機械でも同じ答えになります。
文字列
文字列は UTF-8 です。一文字は一文字の文字列で、別の文字の型はありません。文字数で数えるメソッド(charCount)とバイト数で数えるメソッド(size)が分かれているので、画面に出す文字数とファイルに書くバイト数を使い分けられます。