Advent of Code #4 (in Crystal)
Tags: crystal, adventofcode
Published: 2023-12-04 15:39:53

If you've followed [any](https://dev.to/sethcalebweeks/pragmatic-functional-programming-in-javascript-2lll) [of](https://dev.to/sethcalebweeks/is-functional-programming-worth-the-hype-pragmatic-javascript-series-no) [my](https://dev.to/sethcalebweeks/why-are-there-so-many-articles-on-map-filter-and-reduce-4n1f) [blog](https://dev.to/sethcalebweeks/reduces-many-uses-57dl) [posts](https://dev.to/sethcalebweeks/making-a-monoid-the-art-of-mush-mashing-5cjd), you know that I am a fan of functional programming. And although I'm still a fan of the functional paradigm, I've been trying to improve my knowledge of object oriented programming and finding use cases to apply that knowledge.

Don't get me wrong, I still think that FP has advantages over OOP in many if not most situations. Even when an OOP solution might be more ergonomic or simple or elegant, it often introduces foot-guns that might not be immediately apparent. Specifically, the practice of mutable state can cause subtle issues, even if it is nicely encapsulated in an object.

Anyway, I solved part 1 with a tidy functional solution. You can see most of that process in [this video](https://youtu.be/hLmZfu4Pfi4). When it came to part 2, it seemed like a good use case to apply some OOP. So I did a large refactor and made sure that part 1 still gave me the right answer.

The `Card` type alias worked well for part 1, but I needed to store the number of cards as well for part 2. This meant expanding the type of `Card` from `Hash(String, Array(Int32))` to `Hash(String, Array(Int32) | Int32)`. But then, any place in the code where I am setting the value, I have to explicitly define the type. As far as I am aware, Crystal doesn't have literal hash types, meaning I could not define a hash with specific keys and corresponding value types. At this point, an object (class) seemed like the better option.

I actually really like how the refactor turned out. The parsing of the card was moved to the `from_str` constructor, and other derived values were split up into their respective sections. And the code for part 1 became a simple one liner.

Here's where the foot-gun comes: part 2 involves mutating the cards in the `Cards` hash. Fortunately, part 1 leaves the cards list alone, but if you were to reuse the `cards` variable after part 2, it would contain cards with counts that have been modified. Maybe not the biggest deal, but it's something to keep in mind.

Alright, enough about that. Here's the code:
```crystal
input = File.read("input").strip

alias Cards = Hash(Int32, Card)

class Card

  @winning : Array(Int32)
  @have : Array(Int32)
  property count : Int32

  def initialize(@winning, @have, @count = 1)
  end

  def self.from_str(str)
    winning, have = str.split('|')
    winning = winning.scan(/\d+/).map(&.[0].to_i)
    have = have.scan(/\d+/).map(&.[0].to_i)
    self.new(winning, have)
  end

  def wins
    (@winning&(@have)).size
  end

  def worth
    self.wins > 0 ? 2 ** (self.wins - 1) : 0
  end

  def duplicate(times)
    @count += times
  end

end

cards = input.split("\n").reduce(Cards.new) do |cards, line|
  head, body = line.split(':')
  card_number = head.lchop("Card ").to_i
  cards.merge({card_number => Card.from_str(body)})
end

part1 = cards.values.map(&.worth).sum

puts part1

part2 = cards.reduce(0) do |sum, (card_number, card)|
  if card.wins > 0
    ((card_number + 1)..(card_number + card.wins)).each do |number|
      cards[number].duplicate(card.count)
    end
  end
  sum + card.count
end

puts part2
```