Zulip Chat Archive

Stream: general

Topic: Code Reviews


Shubham Kumar 🦀 (he/him) (Dec 12 2023 at 09:41):

I was looking for a topic related to writing code looking/idiomatic code in lean4, is it ok if I create a new one?

The following code is for AoC Day1 I wasn't sure if I should put in https://leanprover.zulipchat.com/#narrow/stream/113488-general/topic/Advent.20of.20Code.3F if it is I'll close this topic

def dedup [BEq α] (lst : List α) : List α :=
  List.foldl (fun acc x =>
    match acc with
    | [] => x :: acc
    | hd :: _ => if hd == x then acc else x :: acc
  ) [] lst
  |> List.reverse

def CharToString (ch : Char) : String :=
  String.str "" ch

def removeEmptyStrings (lst : List String) : List String :=
  List.filter (fun s => s != "") lst

def splitAtDigitsHelper
  (state : String × List String)
  (ch : Char) : String × List String :=
  let (word, lst) := state
  if Char.isDigit ch then
    -- if character digit found change word to empty string
    -- append character as a string to the list
    -- append it before the word that is generated
    ("", List.append lst [ word, CharToString ch ])
  else
    -- if character not a digit keep appending character to the word
    (String.append word (CharToString ch), lst)

def splitAtDigits (s : String) : List String :=
  let initState := ("", [])
  String.foldl splitAtDigitsHelper initState s
  |> fun (first, lst) => List.append lst [first]
  |> removeEmptyStrings

def isStringDigit (s : String) : Bool :=
  match String.toNat? s with
  | none => false
  | some _ => true

def filterListStringDigit (lst : List String) : List String :=
  List.filter isStringDigit lst

def eliminateAllExceptFirstLast (lst : List String) : List String :=
  let first := List.head? lst
  let last := List.getLast? lst
  match first, last with
  | none, none => []
  | some first, some last => [first, last]
  | _, _ => []

def ListToNat (n : Nat) (lst : List String) : Nat :=
  let StringToNat s :=
    match String.toNat? s with
    | none => 0
    | some n => n

  match lst with
  | [] => n
  | hd :: tl => ListToNat (10 * n + StringToNat hd) tl

def getNumbers (lst : List String) : List Nat :=
  List.map (fun s =>
   splitAtDigits s |>
   filterListStringDigit |>
   eliminateAllExceptFirstLast |>
   (ListToNat 0)) lst

def toSum (lst : List Nat) : Nat :=
  List.foldl (fun acc n => acc + n) 0 lst

def WordToNumber s :=
  match s with
  | "one" => "1"
  | "two" => "2"
  | "three" => "3"
  | "four" => "4"
  | "five" => "5"
  | "six" => "6"
  | "seven" => "7"
  | "eight" => "8"
  | "nine" => "9"
  | _ => s

def firstPass (listOfList : List (List String)) : List (List String) :=
  List.map (List.map WordToNumber) listOfList

def substringExtract (start : Nat) (finish : Nat) (s : String) : String  :=
  let start := String.Pos.mk start
  let finish := String.Pos.mk finish
  let substring := String.toSubstring s
  Substring.extract substring start finish
  |> Substring.toString

def getRest (at_ : Nat) (s : String) : String :=
  let at_ := String.Pos.mk at_
  let last := String.Pos.mk (String.length s)
  let substring := String.toSubstring s
  Substring.extract substring at_ last
  |> Substring.toString

def getWord (s : String) (startIndex : Nat) (endIndex : Nat) : String :=
  substringExtract startIndex endIndex s

def getNDigitWord (s : String) (index : Nat) (N : Nat) : String :=
  getWord s index (index + N) |> WordToNumber

def getNDigitIndexedWords (s : String) (N : Nat) : List (Nat × String) :=
  List.range (String.length s - 1)
  |> List.map (fun index => (index, getNDigitWord s index N))
  |> List.filter (fun (_, word) => isStringDigit word)

def get3DigitIndexWords (s : String) : List (Nat × String) :=
  getNDigitIndexedWords s 3

def get4DigitIndexWords (s : String) : List (Nat × String) :=
  getNDigitIndexedWords s 4

def get5DigitIndexWords (s : String) : List (Nat × String) :=
  getNDigitIndexedWords s 5

def concatDigitsInOrder (s : String) : List String :=
  let indexedThreeDigitWords := get3DigitIndexWords s
  let indexedFourDigitWords := get4DigitIndexWords s
  let indexedFiveDigitWords := get5DigitIndexWords s
  let indexedDigitWords := indexedThreeDigitWords ++ indexedFourDigitWords ++ indexedFiveDigitWords

  List.toArray indexedDigitWords
  |> flip Array.qsort (fun (a, _) (b, _) => a < b)
  |> Array.toList
  |> List.map Prod.snd

def getFirstAndLastOfList? (lst : List α) : Option (α × α) :=
  match List.head? lst, List.getLast? lst with
  | some x, some y => some (x, y)
  | _, _ => none

def secondPass (listOfList : List (List String)) : Nat :=
  let f acc digit :=
    if isStringDigit digit then
      acc ++ [digit]
    else
      acc ++ concatDigitsInOrder digit |> dedup

  let sum acc x :=
    let first := String.toNat? (Prod.fst x)
    let second := String.toNat? (Prod.snd x)

    match first, second with
    | some x, some y => acc + (10 * x + y)
    | _, _ => acc

  listOfList
  |> List.map (List.foldl f [])
  |> List.filterMap getFirstAndLastOfList?
  |> List.foldl sum 0

def day1Part1Fn (path : String) : IO Nat := do
  let input <- IO.FS.readFile path
  pure (String.splitOn input "\n" |> getNumbers |> toSum)

def day1Part2Fn (path : String) : IO Nat := do
  let input <- IO.FS.readFile path
  String.splitOn input "\n"
  |> List.map splitAtDigits
  |> firstPass
  |> secondPass
  |> pure

Jannis Limperg (Dec 12 2023 at 11:19):

Some suggestions:

  • You can use dot notation much more aggressively. For example, List.filter ... lst can be written as lst.filter ..., and similar throughout.
  • ListToNat and WordToNumber should be listToNat and wordToNumber. Only types are capitalised.
  • Chains of list transformations, e.g. in secondPass, are not as efficient in Lean as they are in Haskell because Lean doesn't optimise away the intermediate lists. Also, arrays are just generally faster in most cases. Probably doesn't matter for this problem, but something to keep in mind.

Shubham Kumar 🦀 (he/him) (Dec 12 2023 at 11:48):

Jannis Limperg said:

Some suggestions:

  • You can use dot notation much more aggressively. For example, List.filter ... lst can be written as lst.filter ..., and similar throughout.
  • ListToNat and WordToNumber should be listToNat and wordToNumber. Only types are capitalised.
  • Chains of list transformations, e.g. in secondPass, are not as efficient in Lean as they are in Haskell because Lean doesn't optimise away the intermediate lists. Also, arrays are just generally faster in most cases. Probably doesn't matter for this problem, but something to keep in mind.

Thanks! I forgot that I've skipped on camel case in those 2 functions

What is the best alternative for writing chain transformations in Lean? Any specifc style that you can suggest?

ohhaimark (Dec 12 2023 at 11:52):

firstPass and List.map splitAtDigits could be fused I think.
... |>.map (g ∘ f) instead of ... |>.map f |>.map g

Shubham Kumar 🦀 (he/him) (Dec 12 2023 at 11:56):

Makes sense, to avoid list unboxing 2 times right?

Thanks for the suggestion :D

ohhaimark said:

firstPass and List.map splitAtDigits could be fused I think.
... |>.map (g ∘ f) instead of ... |>.map f |>.map g

Jannis Limperg (Dec 12 2023 at 12:22):

Yeah, hand-fusing the transformations basically. It's ugly but more efficient. You could also explore the monadic for loops (which can be used in pure code with Id.run do, where Id is the identity monad). They let you write your folds like in C, which is arguably more natural for complicated folds.

Kyle Miller (Dec 12 2023 at 17:01):

@Shubham Kumar 🦀 (he/him) Is dedup just supposed to remove consecutive duplicates, or all duplicates? I noticed #eval dedup [1,2,3,1] returns [1,2,3,1]

Kyle Miller (Dec 12 2023 at 17:34):

Made some changes and added comments where I did.

comments

Shubham Kumar 🦀 (he/him) (Dec 13 2023 at 07:49):

Kyle Miller said:

Shubham Kumar 🦀 (he/him) Is dedup just supposed to remove consecutive duplicates, or all duplicates? I noticed #eval dedup [1,2,3,1] returns [1,2,3,1]

no I think you caught an error in my logic, I wanted to remove all the duplicates.
Also thanks for the detailed review for the next problem I'll try to include all of them :pray:

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 12:52):

Is there a better way by using dot notation to write the following function?

  let toBlank (str : String) := str.map (fun ch =>
      match ch with
      | ':' => ' '
      | ',' => ' '
      | ';' => ' '
      | _   => ch
    )

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 13:12):

Also is there a way to use filterMap on Strings, using Substring or String.Iterator? Because essentially I want to filterMap a string but strings are immutable (I'm guessing) so changing in size would not work with the constraint that length remains constant (again guessing)

Alex J. Best (Dec 16 2023 at 13:16):

Not dot notation but you can save your fingers some typing with

  let toBlank (str : String) := str.map (
      fun
      | ':' | ',' | ';' => ' '
      | _   => ch
    )

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 13:17):

Alex J. Best said:

Not dot notation but you can save your fingers some typing with

  let toBlank (str : String) := str.map (
      fun
      | ':' | ',' | ';' => ' '
      | _   => ch
    )

Thanks @Alex J. Best :pray:

Joachim Breitner (Dec 16 2023 at 13:18):

Did you mean

  let toBlank (str : String) := str.map (
      fun
      | ':' | ',' | ';' => ' '
      | ch  => ch
    )

Also maybe these ugly parentheses aren't needed, didn't check.

Yaël Dillies (Dec 16 2023 at 13:18):

You can use dot notation as such

  let toBlank : String := .map fun
      | ':' | ',' | ';' => ' '
      | ch  => ch

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 13:20):

Yaël Dillies said:

You can use dot notation as such

  let toBlank : String := .map fun
      | ':' | ',' | ';' => ' '
      | ch  => ch

Thanks @Yaël Dillies !

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 13:24):

So @Yaël Dillies I am getting an error if I constraint the output type

type mismatch
  String.map fun x =>
    match x with
    | Char.ofNat 58 => Char.ofNat 32
    | Char.ofNat 44 => Char.ofNat 32
    | Char.ofNat 59 => Char.ofNat 32
    | ch => ch
has type
  String → String : Type
but is expected to have type
  String : Type Lean 4

Yaël Dillies (Dec 16 2023 at 13:25):

Ah sorry, it should of course be String → String, not String.

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 13:25):

Yeah that makes sense! sorry my mistake :smile:

Yaël Dillies (Dec 16 2023 at 13:25):

So either

  let toBlank : String → String := .map fun
      | ':' | ',' | ';' => ' '
      | ch  => ch

or

  let toBlank := String.map fun
      | ':' | ',' | ';' => ' '
      | ch  => ch

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 14:07):

so I had a question I was trying to use dot notation for the following function

def filterNoise lst :=
  let filterChars := fun
    | ';' | ':' | ',' => none
    | ch => some ch
  let filterString (str : String) : String :=
    let charList : List Char :=
      str.toList |>.filterMap filterChars
    charList.foldl (init := "") String.push

-- instead of using lst.map filterString
  List.map filterString lst

But using lst.map filterString results in the following error

invalid field notation, type is not of the form (C ...) where C is a constant
  lst
has type
  ?m.6 Lean 4

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 14:07):

why is that? Semantically they should be the same right?

Yaël Dillies (Dec 16 2023 at 14:09):

This just tells you that Lean doesn't know what the type of lst is, hence it can't look up any dot notation!

Yaël Dillies (Dec 16 2023 at 14:09):

def filterNoise (lst : List _) := should work.

Shubham Kumar 🦀 (he/him) (Dec 16 2023 at 14:09):

ah ok, I didn't understand the error. Thanks @Yaël Dillies again :pray:


Last updated: Dec 20 2023 at 11:08 UTC