Struggling to capture functional programming from an OOP perspective

Hello
I am coming from a strong OOP background and am using a simple project to re visualize my approaches as functional instead of OO.

The tiny project at hand so far is to just create a deck of cards(list of strings) and to shuffle it into a random order.

I know the nature of the issue is that I’m not 100% utilizing functional programming so I would love to get other approaches that’ll hopefully correct my thinking. Here is my code so far along with the tests.

Card Module:

defmodule Cards do
  @moduledoc """
  Documentation for `Cards`.
  """
  def create_deck do
    ["Ace", "Two", "Three"]
  end

  def shuffle_deck(deck) do
    shuffled = Enum.shuffle(deck)

    if shuffled == deck do
      shuffled = shuffle_deck(deck)
    end
    shuffled
  end
end

Card Test:

defmodule CardsTest do
  use ExUnit.Case
  doctest Cards

  test "create_deck can create a list of 3 string elements" do
    assert Cards.create_deck == ["Ace", "Two", "Three"]
  end

  test "shuffle_deck can change the order of the previous deck randomly" do
    deck = Cards.create_deck
    shuffled_deck = Cards.shuffle_deck(deck)

    assert shuffled_deck != deck
    assert Enum.count(shuffled_deck) == Enum.count(deck)
  end
end

Here are my floating thoughts.
1: I know I will fail the shuffle test because a random shuffle can possibly be shuffled into the same order.
2: I know that in FP variables are immutable. I still approached the reassigning of shuffled as pointing to another address with a new value. I know this is incorrect because I have tested this and see that shuffled results to the same List

deck = ["Ace", "Two", "Three"]

shuffled = Enum.shuffle(deck)
IO.inspect shuffled
shuffled = Enum.shuffle(deck)
IO.inspect shuffled

3: My immediate thought would be to re shuffle using either a while loop or recursion until shuffled does not equate to deck. From what it seems, using while approach seems discouraging in an FP perspective.

Can anyone shed some light on a better approach to rethinking this situation in a more FP manner? Thanks!

Hello :slight_smile:

This is a scope issue. Your code is equivalent to this:

  def shuffle_deck(deck) do
    shuffled = Enum.shuffle(deck)

    if shuffled == deck do
      some_unused_variable = shuffle_deck(deck)
    end
    
    shuffled
  end

You must have a compiler warning about the unused variable:

variable “some_unused_variable” is unused (if the variable is not meant to be used, prefix it with an underscore)

In Elixir, the if expression will return a value, so you could write it like that:

  def shuffle_deck(deck) do
    shuffled = Enum.shuffle(deck)

    if shuffled == deck do
      shuffle_deck(deck)
    else
      shuffled
    end
  end

This also should work:

  def shuffle_deck(deck) do
    case Enum.shuffle(deck) do
      ^deck -> shuffle_deck(deck)
      shuffled -> shuffled
    end
  end

I would not say that the “while” approach is discouraged. As in any paradigm, “doing stuff as long as a condition is true” is a common problem. You will use recursion and carry state along, but the underlying solved problem is the same.

3 Likes

Ahh thanks so much for that catch! :slight_smile:

To be completely honest, in terms of scoping I would have assumed that shuffled being outside of the if/else block would allow it to be re-assigned, but that doesn’t seem to be the case. I hope I’m not asking for too much if you could talk about how shuffled as the first line of the function isn’t being modified by the recursive shuffle_deck call in the if statement? Sorry if it’s obvious.

Also thanks for the syntactic-sugar/refactored version of the answer!

It boils down to how Erlang uses variables. In Erlang, once bound, a name (a variable) cannot be rebound to something else, ever.

In Elixir, the syntax allows to rebind a name, but it is only a syntactic sugar. The compiler will translate the variables names to something like shuffled@1, shuffled@2, etc.

You cannot rebind variables from an upper scope, because the compiler cannot know what happens when you rebind the variable in the if clause, and not in the else clause. It was once supported but it was messy.

The fact that there is a recursive call has no importance here. Calling the same function (recursion) or any other function behaves equally : new scopes, new variables, no state is carried around automatically.

If you need to rebind a variable for later use, you could do that:

    shuffled = Enum.shuffle(deck)

    shuffled =
      if shuffled == deck do
        shuffle_deck(deck)
      else
        shuffled
      end

    do_something_else(shuffled)

This is not a good piece of code, because it would better to separate the recursion and the usage of the shuffled deck.

def shuffle_and_do_stuff(deck) do
  deck
  |> shuffle_recursively_until_different()
  |> and_then_do_something_else()
end

But in your case you just return it, so you just return the if expression.

1 Like

Thank you so much for breaking it down with details! I figured I would only need to learn about Erlang if I needed to use libraries in pure Erlang, but that may not be the case then. That rebinding approach using if statements is extremely new to me and at the very least opens my mind to different approaches, even if you mentioned it not being good code.

Truly appreciate the details :slight_smile:

1 Like

I advise this blog post.

https://www.theerlangelist.com/article/spawn_or_not

It’s about cards, and deck, but not only…

It’s about how You would make a card game in a concurrent world.

2 Likes

I think one way to help your understanding is that everything is an expression. An expression takes arguments and returns a value.

If, case, cond, def, defmodule, … All expressions.

4 Likes

I’ll definitely give it a good read, thank you for sharing an extremely/coincidentally related article! Cheers to the concurrent world

Awesome, yes it may be a bit easier said than done for me based on my previous understandings in other languages but I really like your approach and idea :slight_smile: I’ll keep that in mind and make sure to give each approach a more “expression” focused respect!

Besides what has been pointed regarding assignment we must decide if a shuffled deck that has the same order should be considered reshuffled or not. You’ll also run into the problem if a deck is reduced to a set of the same cards as every reshuffle will yield the same result and loop infinitely.

def shuffle_deck([h | _] = deck, force_new_order \\ false) do
    case Enum.all?(deck, fn(card) -> card == h end) do
        # if all cards are equal, true, there's no point in reshuffling
        # as it will always be the same so return the deck
        true -> deck
        # if a single card is different than the remaining, reshuffle
        # once and check if it needs more according to force_new_order
        false -> 
          deck
          |> Enum.shuffle()
          |> maybe_reshuffle(deck, force_new_order)
    end
end

# we assume this function is only called after the first argument has been shuffled already
# if force new order is false, then we can return the shuffled deck straightaway
def maybe_reshuffle(deck, _, false), do: deck

# if it's not and both shuffled and non-shuffled decks match, reshuffle
# we use the fact that naming two arguments the same in a function head
# effectively makes it so that both have to represent exactly the same thing
def maybe_reshuffle(same, same, true), do: maybe_reshuffle(Enum.shuffle(same), same, true)

# if we're here this means the shuffled deck is no longer matching the original deck order
# force new order doesn't matter, but will be true anyway - otherwise the first clause would match
# return the deck
def maybe_reshuffle(deck, _, _), do: deck


You can test randomness by defining a seed yourself.
See how Elixir tests the Enum.shuffle/1 function in the doc_tests
https://hexdocs.pm/elixir/Enum.html#shuffle/1

or look for the source code of the tests for that module

by this I mean, you can find a seed for which Enum.suffle will return the original deck in the first run, and check for your functions to return a different deck.