Language Design

LP-9 is written the Smalltalk way: messages sent to values, as in 3 + 4, list size, db exec: sql. It reads as Smalltalk reads, and underneath it is a language with type checking whose values do not change.

The language is the same as Lambda Prelude, from the same company.

Types Are Checked Without Being Written

Types are not written. What type each value has is inferred from how it is used, and where types do not fit, it is an error before the program runs.

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.                      "an error before it runs: Int and Str do not add"

You write as little as in a language without types, and mistakes are found before running, as in a language with them. A method like pair:, which works the same whatever it is given, is used with many types as it is.

A type is written only where inference has no way to tell, such as JSON read from outside.

Values Never Change Behind Your Back

A value never changes once made. To change part of it, make a new value with that part changed.

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: unchanged"

An order line handed to a screen cannot have been rewritten by some other code. A value handed to another actor or another thread cannot be rewritten there either, so it needs no copy and no lock.

A value is made with every field given. There is no nil, so a field left out is an error.

Shared Behaviour Without Inheritance Hierarchies

There is no class inheritance. What several classes share is put in a protocol: a promise to have certain methods, together with methods written in terms of them.

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 writes only area, and gets describe and bigger:. A class that claims Shape and forgets area is an error.

A change to a parent class cannot quietly change some subclass's behaviour. What a class shares is written on the one line that declares it.

No nil to Crash On

There is no nil. A value that may be absent is a Maybe, and it cannot be taken out without saying what to do when it is absent.

(Json parse: '{"name": "Alice"}') ifPresent: [ :o | (o at: 'name') stringValue displayNl ].
((row at: 'note') orElse: '(none)') displayNl.

A NULL column from the database and broken JSON both arrive as an absent Maybe. When data from outside is broken, the program does not stop; it goes to the case for absence.

Failures Caught in One Place

Dividing by zero, a missing key, SQL the database refuses, a passed deadline: every failure is one kind of thing, carrying its reason as a string. There is no hierarchy of exception classes to learn.

r := Error try: [ 10 / 0 ] onError: [ :why | 0 - 1 ].
r displayNl.                                 "-1"

A file held open can be closed on every way out:

[ log append: record ] ensure: [ log close ]

A failure inside an actor's handler stops that actor alone (Actors).

Money Is an Exact Decimal

A number written with an s is a Decimal, an exact decimal number. 0.1 + 0.2 does not drift as it does in binary floating point.

Dividing a Decimal always names the scale of the answer and the rounding:

(10.00s divide: 3.00s scale: 5 rounding: Rounding halfUp) displayNl.   "3.33333"

There is no default rounding because the right one depends on the business: banks round half to even, databases half up, taxes are truncated. Which was used stays in the code for whoever reads it later. There are eight, including one that stops when the division is not exact.

Int and Decimal do not mix in arithmetic; quantity times unit price is price * qty asDecimal. On a screen, grouped writes an amount in thousands.

Dates Are Calendar Days

Date is a day on the calendar, with no time and no zone, and month ends follow the calendar.

d := Date y: 2026 m: 1 d: 31.
(d addMonths: 1) displayNl.                 "2026-02-28"
d dayOfWeek displayNl.                       "6 (Monday is 1)"
(d formattedAs: 'yyyy/MM/dd') displayNl.     "2026/01/31"

What a clock reads is a DateTime, a moment that is the same on every machine in every country is an Instant, and the world's time zones are TimeZone. The time zone data is inside the executable, so every machine answers alike.

Strings

Strings are UTF-8. One character is a one-character string; there is no separate character type. Counting characters (charCount) and counting bytes (size) are separate methods, so the characters shown on a screen and the bytes written to a file are kept apart.