Python and Viv · Records, classes and tuples
A class with state
What to notice
- A Python object changes in place:
acct.deposit(25.5)updatesself.balance. A Viv def does not change a record it is given (a record is a value, and a write into a parameter's field is refused), so each operation returns a new record and the caller rebinds the name:acct = acct.deposit(25.5). __init__becomes an ordinary function,open_account, that builds the record with every field named.Account { balance: ..., history: ..., ..a }is the recordawith two fields replaced.self.history.append(x)becomeshistory = a.historyand thenhistory = append(history, x). The append must rebind the name it grows (it is refused inside a record literal), and becausea.historystill refers to the old array, the append copies it: the old record keeps its old history.withdrawreturnsaunchanged when it refuses; Python's version returnsNoneand leaves the object as it was.
Python state.py · 32 lines
Viv state.viv · 44 lines
py2viv refuses state.py
1 a type that holds state
4class Account:
5 def __init__(self, owner, balance):
6 self.owner = owner
7 self.balance = balance
8 self.history = []
2record Account {
3 owner: Str
4 balance: Num
5 history: Arr
6}
7
8def open_account(owner, balance) -> Account {
9 return Account { owner: owner, balance: balance, history: [] }
10}
py2viv refuses this program:
state.py:13:9: not numeric Python: list.append (lifted by R17)
self.history.append(amount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
py2viv changes a list (append, pop, insert, extend, remove, clear, reverse, sort) only when nothing else can hold it: bound only to new lists (`[]`, a list literal, a comprehension, list(...), sorted(...)) and read nowhere an alias could be taken (a second name, an argument, a container, a nested def); otherwise allocate the final size with np.zeros(n) and write by index.
state.py:20:9: not numeric Python: list.append (lifted by R17)
self.history.append(-amount)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
py2viv changes a list (append, pop, insert, extend, remove, clear, reverse, sort) only when nothing else can hold it: bound only to new lists (`[]`, a list literal, a comprehension, list(...), sorted(...)) and read nowhere an alias could be taken (a second name, an argument, a container, a nested def); otherwise allocate the final size with np.zeros(n) and write by index.
2 constructs outside the numeric subset (tools/py2viv/SUBSET.md) 2 operations that change it
11 def deposit(self, amount):
12 self.balance += amount
13 self.history.append(amount)
14
15 def withdraw(self, amount):
16 if amount > self.balance:
17 print(f"refused: {self.owner} has {self.balance:.2f}, asked for {amount:.2f}")
18 return
19 self.balance -= amount
20 self.history.append(-amount)
13def deposit(a: Account, amount) -> Account {
14 history = a.history
15 history = append(history, amount)
16 return Account { balance: a.balance + amount, history: history, ..a }
17}
18
19def withdraw(a: Account, amount) -> Account {
20 if amount > a.balance {
21 print(f"refused: {a.owner} has {a.balance:.2f}, asked for {amount:.2f}")
22 return a
23 }
24 history = a.history
25 history = append(history, -amount)
26 return Account { balance: a.balance - amount, history: history, ..a }
27}
(refused)
3 use it
23acct = Account("Ada", 100.0)
24acct.deposit(25.5)
25acct.withdraw(40.0)
26acct.withdraw(500.0)
27print(f"{acct.owner}: {acct.balance:.2f}")
28print("history:", acct.history)
30acct = open_account("Ada", 100.0)
31acct = acct.deposit(25.5)
32acct = acct.withdraw(40.0)
33acct = acct.withdraw(500.0)
34print(f"{acct.owner}: {acct.balance:.2f}")
35print("history:", fmt(acct.history))
(refused)
4 checks
31assert acct.balance == 85.5
32assert len(acct.history) == 2
38test "account" {
39 assert_true(acct.balance == 85.5)
40 assert_true(len(acct.history) == 2)
41}
(refused)
What they print
identical — the two programs print the same text.
Dimmed lines are left out of the comparison. Each Viv test block prints one PASS line; the Python checks the same facts with assert, which prints nothing.
$ python3 state.py
refused: Ada has 85.50, asked for 500.00Ada: 85.50history: [25.5, -40.0]
$ viv run state.viv
refused: Ada has 85.50, asked for 500.00Ada: 85.50history: [25.5, -40.0]PASS account