DEV Community

Caleb Weeks
Caleb Weeks

Posted on

Advent of Code #4 (in Crystal)

If you've followed any of my blog posts, 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. 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:

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)