Zulip Chat Archive

Stream: lean4

Topic: Is this idomatic?


Pim Otte (Aug 14 2023 at 07:07):

I have some questions about the code below. This is code I'm writing for use in a handson session to introduce software engineers to Lean. I'm aiming for a piece that shows the power of lean in a clear way, not necessarily the most golfed version.

  • In checkValid, is there a way to do without the "have prop" lines? Messing with the structure like this feels weird.
  • In checkValid, is there a way to do without the "unreachable .none"? Ideally I would just match on tx.kind and have two branches.
  • Is there some way I can derive Repr for Valid?
  • Any other pointers? (I guess sumWithdraws/sumDeposits could be improved, but I'm tempted to leave it as is, because then "write a sumDeposits" function is a good exercise to include)
inductive TransactionKind where
  | deposit : TransactionKind
  | withdraw : TransactionKind
deriving Repr, DecidableEq

structure Transaction where
  amount : Nat
  kind : TransactionKind
deriving Repr

abbrev TransactionHistory := List Transaction

@[simp]
def balance (hist : TransactionHistory) : Nat :=
  match hist with
  | .nil => 0
  | .cons tx txs =>
    match tx.kind with
    | .deposit => balance txs + tx.amount
    | .withdraw => balance txs - tx.amount

inductive Valid : TransactionHistory  Type where
  | empty : Valid []
  | afterDeposit   (validity : Valid txs)
                   (amount : Nat) :
                   Valid (⟨ amount, .deposit :: txs)
  | afterWithdraw  (validity : Valid txs)
                   (amount : Nat)
                   (sufficientBalance : amount <= balance txs) :
                   Valid (⟨amount, .withdraw :: txs)


@[simp]
def sumWithdraws : TransactionHistory  Nat
  | [] => 0
  | tx :: txs =>
    match tx.kind with
    | .deposit => sumWithdraws txs
    | .withdraw => tx.amount + sumWithdraws txs

@[simp]
def sumDeposits : TransactionHistory  Nat
  | [] => 0
  | tx :: txs =>
    match tx.kind with
    | .deposit => tx.amount + sumDeposits txs
    | .withdraw => sumDeposits txs

def balanceCorrect (validHistory : Valid txs) : balance txs + sumWithdraws txs = sumDeposits txs := by
  match validHistory with
  | .empty => simp
  | .afterDeposit validity amount => {
    simp_arith
    apply balanceCorrect validity
  }
  | .afterWithdraw validity amount sufficientBalance => {
    simp_arith
    erw [Nat.sub_add_cancel sufficientBalance]
    apply balanceCorrect validity
  }

def checkValid : (txs : TransactionHistory)  Option (Valid txs)
  | [] => .some .empty
  | tx :: txs =>
    if h : tx.kind = .deposit then
      do
        let prevValid  checkValid txs
        have prop : tx = { amount := tx.amount, kind := .deposit} := by
          rw [ h]
        return (prop  .afterDeposit prevValid tx.amount)
    else
      if h1 : tx.kind = .withdraw then
        if sufficientBalance : tx.amount <= balance txs then
          do
            let prevValid  checkValid txs
            have prop : tx = { amount := tx.amount, kind := .withdraw} := by
              rw [ h1]
            return (prop  .afterWithdraw prevValid tx.amount sufficientBalance)
        else
          .none
      else
        .none

Marcus Rossel (Aug 14 2023 at 07:50):

You can simplify things by matching on the Transactions and their kind directly.

inductive TransactionKind where
  | deposit
  | withdraw
  deriving Repr, DecidableEq

structure Transaction where
  amount : Nat
  kind : TransactionKind
  deriving Repr

abbrev TransactionHistory := List Transaction

@[simp]
def balance : TransactionHistory  Nat
  | []                         => 0
  | amount, .deposit :: txs  => balance txs + amount
  | amount, .withdraw :: txs => balance txs - amount

inductive Valid : TransactionHistory  Type where
  | empty : Valid []
  | afterDeposit   (validity : Valid txs)
                   (amount : Nat) :
                   Valid (⟨ amount, .deposit :: txs)
  | afterWithdraw  (validity : Valid txs)
                   (amount : Nat)
                   (sufficientBalance : amount <= balance txs) :
                   Valid (⟨amount, .withdraw :: txs)

@[simp]
def sumWithdraws : TransactionHistory  Nat
  | []                         => 0
  | _, .deposit :: txs       => sumWithdraws txs
  | amount, .withdraw :: txs => amount + sumWithdraws txs

@[simp]
def sumDeposits : TransactionHistory  Nat
  | []                        => 0
  | amount, .deposit :: txs => amount + sumDeposits txs
  | _, .withdraw :: txs     => sumDeposits txs

theorem balanceCorrect : Valid txs  balance txs + sumWithdraws txs = sumDeposits txs
  | .empty => rfl
  | .afterDeposit validity _ => by
    simp_arith
    apply balanceCorrect validity
  | .afterWithdraw validity _ sufficientBalance => by
    simp_arith
    erw [Nat.sub_add_cancel sufficientBalance]
    apply balanceCorrect validity

def checkValid : (txs : TransactionHistory)  Option (Valid txs)
  | [] => some .empty
  | amount, .deposit :: txs => do
      let prevValid  checkValid txs
      return (.afterDeposit prevValid amount)
  | amount, .withdraw :: txs => do
      if sufficientBalance : amount <= balance txs then
        let prevValid  checkValid txs
        return (.afterWithdraw prevValid amount sufficientBalance)
      else
        none

Marcus Rossel (Aug 14 2023 at 07:54):

Also, I wasn't aware that one could use @[simp] on non-theorem definitions, so I'm not sure if that's idiomatic.

Pim Otte (Aug 14 2023 at 07:58):

Ah, yes, that makes sense, thanks!

Horațiu Cheval (Aug 14 2023 at 09:02):

Marcus Rossel said:

Also, I wasn't aware that one could use @[simp] on non-theorem definitions, so I'm not sure if that's idiomatic.

I wouldn't say @[simp] def is unidiomatic, e.g. https://github.com/leanprover/std4/blob/main/Std/Data/Option/Basic.lean has several @[simp] defs. What it does AFAIK is it generates equational lemmas corresponding to the function definition which can then be used by simp.

Yaël Dillies (Aug 14 2023 at 09:04):

The equational lemmas are always generated. This is what lets you do rw [my_def]. What changes is whether the equational lemmas are tagged @[simp].

Horațiu Cheval (Aug 14 2023 at 09:09):

How can I see them without @[simp]? With @[simp] I can do

@[simp] def f : Nat  Nat
  | 0 => 1
  | n + 1 => f n

#check f._eq_2

but f._eq_2 is unknown otherwise

Horațiu Cheval (Aug 14 2023 at 09:12):

My recalling about this was based on this thread

Kevin Buzzard (Aug 14 2023 at 09:17):

Try whatsnew in def f : ... (needs a mathlib import) to see what declarations are generated by a definition.

Alistair Tucker (Aug 14 2023 at 09:21):

Is this idiomatic?

It's probably more idiomatic to have Valid : TransactionHistory → Prop and then prove Decidable (Valid txs).

instance decValid : (txs : TransactionHistory)  Decidable (Valid txs)
  | [] => isTrue .empty
  | amount, .deposit :: txs =>
    match decValid txs with
    | isTrue prevValid => isTrue (.afterDeposit prevValid amount)
    | isFalse prevValid' => isFalse (fun | .afterDeposit v a => prevValid' v)
  | amount, .withdraw :: txs =>
    match decValid txs with
    | isTrue prevValid =>
      if sufficientBalance : amount <= balance txs then
        isTrue (.afterWithdraw prevValid amount sufficientBalance)
      else
        isFalse (fun | .afterWithdraw _ a sb => sufficientBalance sb)
    | isFalse prevValid' => isFalse (fun | .afterWithdraw v a sb => prevValid' v)

Pim Otte (Aug 14 2023 at 10:04):

Ah, yeah, that looks very clean. Thanks!


Last updated: Dec 20 2023 at 11:08 UTC