Zulip Chat Archive
Stream: general
Topic: Advent of Code 2024 - Day 4
James Sully (Dec 05 2024 at 06:23):
Here's my solution. Feedback appreciated, and please post your own solution!
import Utils
namespace Day04
abbrev CharGrid := Array (Array Char)
def parse (input : String) : CharGrid :=
let lines := input.lines.toArray
lines.map (·.toList.toArray)
structure V2 (α : Type) where
x : α
y : α
def V2.map (f : α → β) (v : V2 α) : V2 β := ⟨f v.x, f v.y⟩
def V2.mapM [Monad m] (f : α → m β) (u : V2 α) : m (V2 β) := do
let x ← f u.x
let y ← f u.y
return {x, y}
instance [HAdd α β γ] : HAdd (V2 α) (V2 β) (V2 γ) where
hAdd | ⟨u₁, u₂⟩, ⟨v₁, v₂⟩ => ⟨u₁+v₁, u₂+v₂⟩
instance [HMul α β γ] : HMul α (V2 β) (V2 γ) where
hMul | a, ⟨x, y⟩ => ⟨a*x, a*y⟩
def gridLookup (idx : V2 Nat) (grid : Array (Array α)) : Option α := do
let row ← grid[idx.y]?
let x ← row[idx.x]?
return x
def gridFindAll [Inhabited α] (p : α → Bool) (arr : Array (Array α))
: Array (V2 Nat) := Id.run do
let height := arr.size
let width := arr[0]!.size
let mut ret := #[]
for row in [:height] do
for col in [:width] do
if p arr[row]![col]! then
ret := ret.push {x := col, y := row}
ret
def directions : List (V2 Int) := [
⟨0, 1⟩, -- N
⟨1, 1⟩, -- NE
⟨1, 0⟩, -- E
⟨1, -1⟩, -- SE
⟨0, -1⟩, -- S
⟨-1, -1⟩, -- SW
⟨-1, 0⟩, -- W
⟨-1, 1⟩ -- NW
]
def xmas := "XMAS".toList
def isXmasInDir (grid : CharGrid) (start : V2 Int) (direction : V2 Int) : Bool :=
let coords : List (V2 Int) := List.range 4 |>.map (λ i ↦ start + Int.ofNat i * direction)
match do
let coords : List (V2 Nat) ← coords.mapM (V2.mapM Int.toNat')
let chars : List Char ← coords.mapM (gridLookup · grid)
return chars
with
| .none => false
| .some chars => chars = xmas
def countXmassStartingAt (grid : CharGrid) (start : V2 Int) : Nat :=
directions.countP (isXmasInDir grid start ·)
def part1 (input : CharGrid) : Nat := Id.run do
let xPositions : Array (V2 Nat) := gridFindAll (· = 'X') input
xPositions.map (·.map Int.ofNat)
|>.map (countXmassStartingAt input ·)
|>.toList
|>.sum
def mas := "MAS".toList
-- this converting between Int and Nat coords sucks
-- we'll use 'A' as the "origin"
def isCrossMasAt (grid : CharGrid) (aPosition : V2 Nat) : Bool :=
let aPosition' : V2 Int := aPosition.map Int.ofNat
match do
let diag1Deltas : List (V2 Int) := [⟨-1,-1⟩, ⟨0,0⟩, ⟨1, 1⟩]
let diag1Coords ← diag1Deltas.map (aPosition' + ·) |>.mapM (V2.mapM Int.toNat')
let diag1 ← diag1Coords.mapM (gridLookup · grid)
let diag2Deltas : List (V2 Int) := [⟨-1,1⟩, ⟨0,0⟩, ⟨1, -1⟩]
let diag2Coords ← diag2Deltas.map (aPosition' + ·) |>.mapM (V2.mapM Int.toNat')
let diag2 ← diag2Coords.mapM (gridLookup · grid)
return (diag1, diag2)
with
| .none => false
| .some (diag1, diag2) =>
(diag1 = mas || diag1.reverse = mas) && (diag2 = mas || diag2.reverse = mas)
def part2 (input : CharGrid) : Nat :=
let aPositions := gridFindAll (· = 'A') input
aPositions.toList.countP (isCrossMasAt input ·)
Kim Morrison (Dec 05 2024 at 07:19):
def gridFindAll [Inhabited α] (p : α → Bool) (arr : Array (Array α)) :
Array (V2 Nat) := Id.run do
let height := arr.size
let mut ret := #[]
for h₁ : row in [:height] do
for h₂ : col in [:arr[row].size] do
if p arr[row][col] then
ret := ret.push {x := col, y := row}
ret
Kim Morrison (Dec 05 2024 at 07:20):
You could also do that in a more functional style with List.findIdxs
from Batteries (although there is no Array
analogue at present).
Kim Morrison (Dec 05 2024 at 07:21):
def gridLookup (idx : V2 Nat) (grid : Array (Array α)) : Option α := do
let row ← grid[idx.y]?
let x ← row[idx.x]?
return x
does seem to be calling out for syntactical sugar to allow grid[idx.x, idx.y]?
Kim Morrison (Dec 05 2024 at 07:25):
Could you quarantine the Int
/Nat
pain by changing gridLookup
to accept Ints?
James Sully (Dec 05 2024 at 07:39):
that gridFindAll
is great, thanks, also eliminates the Inhabited
constraint
James Sully (Dec 05 2024 at 07:40):
I want to try and avoid batteries, I added it for the HashMap
, then removed it after you pointed out the Std
one hahaha. It's nice to avoid dependencies for AOC.
James Sully (Dec 05 2024 at 07:41):
Kim Morrison said:
Could you quarantine the
Int
/Nat
pain by changinggridLookup
to accept Ints?
I think this would probably be the practical choice yeah. Feels immoral since all negatives will be none
James Sully (Dec 05 2024 at 07:44):
Kim Morrison said:
grid[idx.x, idx.y]?
that would be rad
James Sully (Dec 05 2024 at 07:48):
I think the "correct" solution for the Int/Nat thing would be a fallible add V2 Nat -> V2 Int -> Option (V2 Nat)
But then you'd have to give up +
or make your own notation.
James Sully (Dec 05 2024 at 07:49):
wait could I do a HAdd
instance for that?
Kim Morrison (Dec 05 2024 at 07:57):
James Sully said:
Kim Morrison said:
grid[idx.x, idx.y]?
that would be rad
This is something someone should prototype in a downstream library. It shouldn't need any native language support to implement, I think.
Matt Russell (Dec 05 2024 at 08:07):
Day 4 solution from a Lean beginner, feedback / suggestions welcome!
https://github.com/mdr/aoc-2024/blob/main/Aoc2024/Day04/Solve.lean
James Sully (Dec 05 2024 at 08:17):
The Int/Nat situation improved quite a bit with a HAdd instance:
import Utils
namespace Day04
abbrev CharGrid := Array (Array Char)
def parse (input : String) : CharGrid :=
let lines := input.lines.toArray
lines.map (·.toList.toArray)
structure V2 (α : Type) where
x : α
y : α
def V2.map (f : α → β) (v : V2 α) : V2 β := ⟨f v.x, f v.y⟩
def V2.mapM [Monad m] (f : α → m β) (u : V2 α) : m (V2 β) := do
let x ← f u.x
let y ← f u.y
return {x, y}
instance [HAdd α β γ] : HAdd (V2 α) (V2 β) (V2 γ) where
hAdd | ⟨u₁, u₂⟩, ⟨v₁, v₂⟩ => ⟨u₁+v₁, u₂+v₂⟩
instance : HAdd (V2 Nat) (V2 Int) (Option (V2 Nat)) where
hAdd | ⟨u₁, u₂⟩, ⟨v₁, v₂⟩ => do
let x ← Int.ofNat u₁ + v₁ |>.toNat'
let y ← Int.ofNat u₂ + v₂ |>.toNat'
return ⟨x, y⟩
instance [HMul α β γ] : HMul α (V2 β) (V2 γ) where
hMul | a, ⟨x, y⟩ => ⟨a*x, a*y⟩
def gridLookup (idx : V2 Nat) (grid : Array (Array α)) : Option α := do
let row ← grid[idx.y]?
let x ← row[idx.x]?
return x
def gridFindAll (p : α → Bool) (arr : Array (Array α)) :
Array (V2 Nat) := Id.run do
let height := arr.size
let mut ret := #[]
for h₁ : row in [:height] do
for h₂ : col in [:arr[row].size] do
if p arr[row][col] then
ret := ret.push {x := col, y := row}
ret
def directions : List (V2 Int) := [
⟨0, 1⟩, -- N
⟨1, 1⟩, -- NE
⟨1, 0⟩, -- E
⟨1, -1⟩, -- SE
⟨0, -1⟩, -- S
⟨-1, -1⟩, -- SW
⟨-1, 0⟩, -- W
⟨-1, 1⟩ -- NW
]
def xmas := "XMAS".toList
def isXmasInDir (grid : CharGrid) (start : V2 Nat) (direction : V2 Int) : Bool :=
match do
let coords : List (V2 Nat) ← List.range 4 |>.mapM (λ i ↦ start + Int.ofNat i * direction)
let chars : List Char ← coords.mapM (gridLookup · grid)
return chars
with
| .none => false
| .some chars => chars = xmas
def countXmassStartingAt (grid : CharGrid) (start : V2 Nat) : Nat :=
directions.countP (isXmasInDir grid start ·)
def part1 (input : CharGrid) : Nat :=
let xPositions : Array (V2 Nat) := gridFindAll (· = 'X') input
xPositions
|>.map (countXmassStartingAt input ·)
|>.toList
|>.sum
def mas := "MAS".toList
-- we'll use 'A' as the "origin"
def isCrossMasAt (grid : CharGrid) (aPosition : V2 Nat) : Bool :=
match do
let diag1Deltas : List (V2 Int) := [⟨-1,-1⟩, ⟨0,0⟩, ⟨1, 1⟩]
let diag1Coords : List (V2 Nat) ← diag1Deltas.mapM (λ (d : V2 Int) ↦ aPosition + d)
let diag1 ← diag1Coords.mapM (gridLookup · grid)
let diag2Deltas : List (V2 Int) := [⟨-1,1⟩, ⟨0,0⟩, ⟨1, -1⟩]
let diag2Coords : List (V2 Nat) ← diag2Deltas.mapM (aPosition + ·)
let diag2 ← diag2Coords.mapM (gridLookup · grid)
.some (diag1, diag2)
with
| .none => false
| .some (diag1, diag2) =>
(diag1 = mas || diag1.reverse = mas) && (diag2 = mas || diag2.reverse = mas)
def part2 (input : CharGrid) : Nat :=
let aPositions := gridFindAll (· = 'A') input
aPositions.toList.countP (isCrossMasAt input ·)
I'm not entirely sure why I need the explicit lambda with a type annotation on the argument on this line
let diag1Coords : List (V2 Nat) ← diag1Deltas.mapM (λ (d : V2 Int) ↦ aPosition + d)
but the type checker gets upset without it.
I think this is probably a little too magical to really be nice code. Could be good with a semantic separation between point and vector types.
James Sully (Dec 05 2024 at 08:19):
Matt Russell said:
Day 4 solution from a Lean beginner, feedback / suggestions welcome!
https://github.com/mdr/aoc-2024/blob/main/Aoc2024/Day04/Solve.lean
Neat approach, I considered doing something like that briefly. Is List.diagonals
in your Utils
?
James Sully (Dec 05 2024 at 08:21):
Also I didn't know about #guard
, that's super cool (docs)
Matt Russell (Dec 05 2024 at 08:39):
Neat approach, I considered doing something like that briefly. Is
List.diagonals
in yourUtils
?
Thanks! Yeah, I stole diagonals
from a Haskell lib because I'm not smart enough to write the recursion myself: https://github.com/mdr/aoc-2024/blob/main/Aoc2024/Utils.lean#L89
James Sully (Dec 05 2024 at 08:40):
Ah yeah cool
Kim Morrison (Dec 05 2024 at 09:20):
Bonus points to make diagonals
tail recursive. :-) (Even more points for the lemma relating it to the "easy to prove about" version.)
Ayhon (Dec 05 2024 at 11:25):
Kinda late to this one. This is my solution:
import Lean.Data.RBMap
namespace Day4
abbrev Input := Array (Array Char)
def parse_input(ls: List String): Input :=
Array.mk $ ls
|>.filter (not ∘ String.isEmpty)
|>.map (Array.mk ·.trim.toList)
abbrev Board := Input
namespace Part1
def List.iterate(f: α -> α)(a: α)(n: Nat) :=
match n with
| 0 => []
| n'+1 => a :: List.iterate f (f a) n'
def rot90[Inhabited α](b: Array (Array α)): Array (Array α) :=
let n := b.size
let m := b[0]!.size
Array.ofFn fun (j: Fin m) =>
Array.ofFn fun (i: Fin n) =>
b[n-1-i]![j]!
def count_dir(s: String)(dir: Bool × Bool)(b: Board): Nat :=
let n := b.size - (if dir.1 then s.length-1 else 0)
let m := b[0]!.size - (if dir.2 then s.length-1 else 0)
List.ofFn (fun (i: Fin n) =>
List.ofFn (fun (j: Fin m) =>
if s.toList.enum.all (fun (o, c) =>
let ni := if dir.1 then i + o else i
let nj := if dir.2 then j + o else j
b[ni]![nj]! == c
)
then 1
else 0
)
|>.sum
)
|>.sum
def solve(a: Input): Int :=
let boards := List.iterate rot90 a 4
let count_diag := count_dir "XMAS" (true, true)
let count_straight := count_dir "XMAS" (false, true)
boards
|>.map (fun b => count_straight b + count_diag b)
|>.sum
end Part1
namespace Part2
abbrev Pattern := Array (Array (Option Char))
def Pattern.count_match (p: Pattern)(b: Board) :=
let bn := b.size
let bm := b[0]!.size
let pn := p.size
let pm := p[0]!.size
List.range (bn - pn + 1)
|>.map (fun i =>
List.range (bm - pm + 1)
|>.map (fun j =>
if ( List.range pn
|>.all fun oi =>
List.range pm
|>.all fun oj =>
match p[oi]![oj]! with
| none => true
| some c => c == b[i + oi]![j + oj]!)
then 1 else 0
)|>.sum
)|>.sum
def solve(a: Input): Int :=
let pattern := #[
#[some 'M', none, some 'M' ],
#[none, some 'A', none ],
#[some 'S', none, some 'S' ]
]
let patterns := Part1.List.iterate Part1.rot90 pattern 4
patterns.map (fun p => Pattern.count_match p a) |>.sum
end Part2
Kim Morrison (Dec 05 2024 at 11:32):
Ouch rot90
scares me, with all that runtime array indexing. Can you use Vector
?
Kim Morrison (Dec 05 2024 at 11:33):
Note for things like List.range pm |>.all
you also have docs#Nat.all
Ayhon (Dec 05 2024 at 11:43):
Kim Morrison said:
Ouch
rot90
scares me, with all that runtime array indexing. Can you useVector
?
Yes, that is fair. I have just spent too much time in a past project trying to convince Lean that array accesses were in fact in-bounds, and I didn't want to risk dealing with it in this Advent of Code.
I could use a Vector
, but then I'd need to do checks in the parser that ensures the grid is well formed (all rows have the same length), and I'm not too sure how to achieve that. Any tips?
James Sully (Dec 05 2024 at 13:32):
I spent a fair bit of time banging my head against the wall of { grid : Array (Array Char) // rect grid }
and variations on that theme.
James Sully (Dec 05 2024 at 13:33):
docs#Vector Oh cool, I had assumed Vectors were cons lists as in some other languages, not arrays
Mario Carneiro (Dec 05 2024 at 13:38):
Ayhon said:
I could use a
Vector
, but then I'd need to do checks in the parser that ensures the grid is well formed (all rows have the same length), and I'm not too sure how to achieve that. Any tips?
it's relatively easy to do such checks, you just use an if
statement and fail if it's false. In the true case you get a hypothesis which you can adjoin to the array to build a Vector
James Sully (Dec 05 2024 at 14:04):
I don't find these things easy at all! Here's my attempt which I've gotten stuck on:
def String.lines (s : String) : List String := s.split (· == '\n')
-- not totally clear on whether the size should be implicit or not?
abbrev CharGrid {w h : Nat} := Vector (Vector Char w) h
def parse (input : String) : Option CharGrid := do
let lines := input.lines.toArray
if h₁ : lines.size > 0 then
let width := lines[0].toList.toArray.size
let mut ret := #[]
for line in lines do
let lineArr := line.toList.toArray
if h₂ : lineArr.size = width then
let lineVec : Vector Char width :=
⟨lineArr, by assumption⟩
ret := ret.push lineVec
have height := ret.size
return ⟨ret, ???⟩
else
none
before I started trying to refactor it was
abbrev CharGrid := Array (Array Char)
def parse (input : String) : CharGrid :=
let lines := input.lines.toArray
lines.map (·.toList.toArray)
Mario Carneiro (Dec 05 2024 at 14:10):
What type is String.lines
? That's not a MWE
Mario Carneiro (Dec 05 2024 at 14:11):
I guess it must be String -> List String
James Sully (Dec 05 2024 at 14:11):
sorry, I'll edit
James Sully (Dec 05 2024 at 14:13):
I'm having trouble connecting to the playground to verify it has everything that it needs, but I think it does, and in any case I've added String.lines
Mario Carneiro (Dec 05 2024 at 14:16):
def String.lines (s : String) : List String := s.split (· == '\n')
structure CharGrid where
(w h : Nat)
grid : Vector (Vector Char w) h
def parse (input : String) : Option CharGrid := do
let first :: rest := input.lines | none
let first := first.toList.toArray
let w := first.size
let rest : List (Vector _ w) ← rest.mapM fun s =>
let s := s.toList.toArray
if h : _ then some ⟨s, h⟩ else none
let grid := (⟨first, rfl⟩ :: rest).toArray
return { w, h := grid.size, grid := ⟨grid, rfl⟩ }
James Sully (Dec 05 2024 at 14:16):
Ahh, I've been wanting a way to do partial pattern matches
James Sully (Dec 05 2024 at 14:17):
ok, so you have to store the size at runtime
James Sully (Dec 05 2024 at 14:20):
thanks for this. To me it doesn't seem obviously worth the complication
Mario Carneiro (Dec 05 2024 at 14:20):
it depends on whether you want to have access to the width and height and/or validate that it's a rectangle
Mario Carneiro (Dec 05 2024 at 14:20):
not sure if that's part of the spec
James Sully (Dec 05 2024 at 14:24):
This line is sick
if h : _ then some ⟨s, h⟩ else none
I didn't know you could elide that. makes sense in retrospect
Mario Carneiro (Dec 05 2024 at 14:25):
you can even write
return { w, h := _, grid := ⟨grid, rfl⟩ }
on the last line
Mario Carneiro (Dec 05 2024 at 14:25):
you can write code where both data and control flow is clear only to lean itself :)
Mario Carneiro (Dec 05 2024 at 14:31):
heh
structure CharGrid where
{w h : Nat}
grid : Vector (Vector Char w) h
def parse (input : String) : Option CharGrid := do
let first :: rest := input.lines | none
let rest ← rest.mapM fun s =>
if h : _ then some ⟨s.toList.toArray, h⟩ else none
return ⟨⟨(⟨first.toList.toArray, rfl⟩ :: rest).toArray, rfl⟩⟩
Ayhon (Dec 05 2024 at 15:56):
The line I find the most magical is this one.
let first :: rest := input.lines | none
If I understand correctly the semantics, the | none
means that if the pattern is not matched, none
is returned from the function directly. In some sense, it works similarly to the ?
operator in Rust. Am I right? Also, is this syntax documented anywhere? Would like to know more (if there is more to know)
Mario Carneiro (Dec 05 2024 at 16:08):
this is equivalent to the let else syntax of rust
Mario Carneiro (Dec 05 2024 at 16:08):
let Cons(first, rest) = input.lines() else { return None };
Mario Carneiro (Dec 05 2024 at 16:09):
the ?
operator in rust is more like the <-
operator in lean
Mario Carneiro (Dec 05 2024 at 16:10):
In general, the syntax
do
let pat := e
| foo
bar
is equivalent to
do
match e with
| pat => bar
| _ => foo
Greg Shuflin (Dec 09 2024 at 09:06):
Mario Carneiro said:
you can even write
return { w, h := _, grid := ⟨grid, rfl⟩ }
on the last line
Where is this syntax with ⟨⟩
documented?
Mario Carneiro (Dec 09 2024 at 09:15):
not sure, but you might find references under the name "anonymous constructor (bracket)"
Mario Carneiro (Dec 09 2024 at 09:20):
I didn't find it in the WIP language reference, but there is a short syntax docstring if you hover it:
The anonymous constructor
⟨e, ...⟩
is equivalent toc e ...
if the
expected type is an inductive type with a single constructorc
.
If more terms are given thanc
has parameters, the remaining arguments
are turned into a new anonymous constructor application. For example,
⟨a, b, c⟩ : α × (β × γ)
is equivalent to⟨a, ⟨b, c⟩⟩
.
Paul Nelson (Dec 29 2024 at 15:42):
abbrev Point := Nat × Nat
abbrev Direction := Int × Int
abbrev Grid := Array (Array Char)
def parse (input : String) : Grid :=
input.splitOn "\n" |>.filter (· ≠ "") |>.map String.toList |>.map List.toArray |> List.toArray
def check (grid : Grid) (start : Point) (dir : Direction) (pattern : String) : Bool :=
let (rows, cols) := (grid.size, grid[0]!.size)
pattern.toList.enum.all fun ⟨i, t⟩ =>
let (r, c) := (start.1 + i * dir.1, start.2 + i * dir.2)
if r ≥ 0 && c ≥ 0 && r < rows && c < cols then
let (r, c) := (r.toNat, c.toNat)
grid[r]![c]! == t
else
false
def indices (grid : Grid) : List Point :=
if grid.size = 0 then [] else
let rows := List.range grid.size
let cols := List.range grid[0]!.size
rows.flatMap (cols.map fun j => ⟨·, j⟩)
def part1 (input : Grid) : Nat :=
let dirs : List Direction := [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
let pattern := "XMAS"
indices input |>.flatMap (fun start =>
dirs |>.filter (check input start · pattern))
|>.length
def indicesWithRoom (grid : Grid) : List Point :=
if grid.size = 0 then [] else
let rows := List.range' 1 (grid.size - 2)
let cols := List.range' 1 (grid[0]!.size - 2)
rows.flatMap (cols.map fun j => ⟨·, j⟩)
def check2 (grid : Grid) (start : Point) : Bool :=
let deltas : List Direction := [(1, 1), (-1, 1), (-1, -1), (1, -1)]
let cornerChoices := ["MMSS", "MSSM", "SMMS", "SSMM"]
let (r, c) := (start.1, start.2)
grid[r]![c]! == 'A' && cornerChoices.any fun str =>
deltas.enum.all fun ⟨i, delta⟩ =>
let (r', c') := (r + delta.1, c + delta.2)
let (r', c') := (r'.toNat, c'.toNat)
grid[r']![c']! == str.toList[i]!
def part2 (input : Grid) : Nat :=
indicesWithRoom input |>.filter (check2 input) |>.length
Last updated: May 02 2025 at 03:31 UTC