Getting the no global variable thing

Hi,

I’m just working my way into Elixir and loving it… it’s crazy how you can get complicated stuff to work first time that would NEVER come our right first time in a procedural.

So … no global variables.

I’m working on a Bridge hand dealer as my exploration path, so I need a deck of cards to start with. Normally I’d create a data structure and assign it to a global variable and use that throughout the application … but … no global variables.

So here’s what I came up with

  def deck do
    [
      {0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {0, 10}, {0, 11}, {0, 12},
      {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7}, {1, 8}, {1, 9}, {1, 10}, {1, 11}, {1, 12},
      {2, 0}, {2, 1}, {2, 2}, {2, 3}, {2, 4}, {2, 5}, {2, 6}, {2, 7}, {2, 8}, {2, 9}, {2, 10}, {2, 11}, {2, 12},
      {3, 0}, {3, 1}, {3, 2}, {3, 3}, {3, 4}, {3, 5}, {3, 6}, {3, 7}, {3, 8}, {3, 9}, {3, 10}, {3, 11}, {3, 12}
    ]
  end

then to deal a hand I have

  def deal do
    dealing(Enum.shuffle(deck()))
  end

where dealing splits the list into four hands.

I’m hoping I don’t get something … as this makes my procedural brain go nuts … I’m getting a new copy of the deck every time?

Jonathan.

Yup :grin:
In the world of functional programming, everything is basically pass-by-value. Every call to deck() will return a new array containing 52 elements.

This can be both a good and bad thing. There’s not really a “heap” to think about allocating to hold the deck, and theres no reference passing to think about, either.

When Enum.shuffle(enumerable) is called, it will return an entirely new array, with the deck elements randomly shuffled.
When dealing(deck_of_cards) do is called, the four hands it returns are new copies as well.

functional programming encourages design like this. functions that take smaller inputs, and return consistent results with no side effects. In a sense, everything is pass-by-value. The result is software with (fewer) side effects, and (after some practice) is easier to grok and study.

also, try this:

heart = "H"
spade = "S"
diamond = "D"
club = "C"
suits = [heart, spade, diamond, club]
ranks = ~w(2 3 4 5 6 7 8 9 T J Q K A)
def deck do
  Enum.flat_map(suits, fn (suit) -> Enum.map(ranks, fn (rank) -> {suit, rank} end) end)
end

Instead of strings for the suits I’d use atoms, same for the ranks, which boils roughly to thhis:

defmodule Deck do
  # creates list of atoms for the suite
  @suits ~w[heart spade diamond club]a

  # creates list of ints and atoms according to a cards rank
  @ranks [2, 3, 4, 5, 6, 7, 8, 9, 10] ++ ~w[jack queen king ace]a

  # creates list of tuples of {suit, rank}
  @deck for s <- @suites, r <- @ranks, do: {r, s}

  @doc "returns a freshly unpacked deck in order"
  def fresh_deck(), do: @deck

  @doc "returns a shuffled deck"
  def shuffle(), do: Enum.shuffle(fresh_deck())

  @doc """
  draws a card from the deck, returns a tuple with
  the card as first element and the new deck as the
  second, will return `:empty` if there is no card in
  the deck
  """
  def draw(deck)
  def draw([card|deck]), do: {card, deck}
  def draw([]), do: :empty
end

For suites and ranks I use a sigil to create a list of atoms, as well as I do it for the ranks.

To generate the deck, I do use a comprehension to generate the tuples from the lists defined above. I do this in an attribute, because the list is generated once during compiletime this way and does not need to get regenerated every time we call fresh_deck/0.

The shuffle/0 should be pretty self-explainatory.

draw/1 just uses pattern matching to get the card and the new deck and returns it then.

If there is still stuff, that needs further explanation, feel free to ask.

1 Like

Every time you call deck(), you are re-using the same deck in memory. But when you shuffle the deck, the original deck is transformed, and then they become different copies. However, it is worth pointing out that each of those decks share the same cards (the tuples!). So when you shuffle, you only copy the container of the cards, the list, not the cards itself. To put it in other words, it is a shallow copy, not a deep one.

So while immutability adds the cost of transformation, it provides the benefit of transparent sharing, since it can share data structures throughout your system as they are guaranteed to never change.

4 Likes

As I posted above, this is not actually true for Erlang/Elixir. All of the literals in your module are allocated only once in memory when the module is loaded. From there on, every time you call the function, you get the exact same element in memory:

iex(1)> defmodule Foo do
...(1)> def bar do
...(1)> [:a, :b, :c]
...(1)> end
...(1)> end
iex(2)> :erts_debug.same(Foo.bar, Foo.bar)
true
iex(3)> :erts_debug.same(Foo.bar,  [:a, :b, :c])
false

I would be surprised if other functional languages do not perform the same optimization. At least the eager ones, not sure if laziness would affect this. OTP 20 optimizes this further by not copying literals when sending messages. And OTP 21 will optimize this even further by being able to share maps keys as well, thanks to a PR from @michalmuskala.

6 Likes