To spawn, or not to spawn?

That is the question! Is it better to keep everything in one process, or create a separate process for each piece of state that we need to manage? In this article, I'll talk a little bit about using or not using processes. I'll also show you how to decouple complex stateful logic from issues such as temporal behavior and interprocess communication.





But before starting, since the article will be long, I would like to outline the main points:





  • Use functions and modules to separate thought entities.





  • Use processes to separate runtime entities.





  • Don't use processes (even agents) to separate entities of thought.





The construct "thinking entities" here refers to the ideas that are in our mind, such as "order", "position in the order", "product", etc. If these concepts are too complex, then it is worth implementing them in separate modules and functions to separate different entities and keep every part of our code focused and coherent.





Using processes (eg agents) for this is a mistake people often make. This approach significantly misses the functionality of Elixir and instead tries to mimic objects by processes. The implementation is likely to be worse than a simple functional approach (or even an object-oriented programming language equivalent). Therefore, it is worth turning to processes only when there are tangible benefits from it. Code organization isn't one of those benefits, so it's not a good reason to use processes.





Processes are used to solve runtime problems - properties that can be observed on a running system. For example, you need to use multiple processes if you want to prevent the failure of one job from affecting other system functions. Another motivation is when you want to introduce parallelization potential by allowing you to run multiple jobs at the same time. This can improve the performance of your application and open up the potential for scalability in both directions. There are other, less common use cases for processes, but again, the separation of thought entities is not one of them.





Example

, ? . , ( ), .





- , , , . . , : () (). . 21, . ( ).





- , (2-10) , , 10. 1 11, , ( ) .





, . , . , , .





, , , , (), , , .





, , : , . - . , ยซยป , . , , , . , . : , , , . .





, , . , . . , . : , , ( ). , , , , , .





, . . , , . , . ( ) , , ( ) ( ). , , :-) (. : , ?)





, , . , , .





, , , - . , , , , . , , , :-)





, ? , . , , , , .





, , , .





, - . 52 . , .





, , . , , . , .





. , . :





@cards (
  for suit <- [:spades, :hearts, :diamonds, :clubs],
      rank <- [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace],
    do: %{suit: suit, rank: rank}
)

      
      



shuffle/0



:





def shuffled(), do:
  Enum.shuffle(@cards)

      
      



, take/1



, :





def take([card | rest]), do:
  {:ok, card, rest}
def take([]), do:
  {:error, :empty}

      
      



take/1



{:ok, card_taken, rest_of_the_deck}



, {:error, :empty}



. ( ) , .





:





deck = Blackjack.Deck.shuffled()

case Blackjack.Deck.take(deck) do
  {:ok, card, transformed_deck} ->
    # do something with the card and the transform deck
  {:error, :empty} ->
    # deck is empty -> do something else
end

      
      



, ยซ ยป, :





  • ,





  • ,





  • ,









, - . - Deck



, Deck



. ( ), , ( , , , - . .)





, . . shuffled_deck/0



take_card/1



. , , , . , - . (. : , )





, . , .





.





. . (:ok



:busted



). Blackjack.Hand.





. new/0



, deal/2



, . :





# create a deck
deck = Blackjack.Deck.shuffled()

# create a hand
hand = Blackjack.Hand.new()

# draw one card from the deck
{:ok, card, deck} = Blackjack.Deck.take(deck)

# give the card to the hand
result = Blackjack.Hand.deal(hand, card)

      
      



deal/2



{hand_status, transformed_hand}



, hand_status



:ok



:busted



.





, Blackjack.Round, . :













  • ,





  • ( / )









  • ,





, . , . . , , , , , . , , .





, , /, GenServer



:gen_statem



. (, ) .





, , . , , , , . , (netsplits), , . , , , event sourcing - .





, , . .





, . .





. , start/1



:





{instructions, round} = Blackjack.Round.start([:player_1, :player_2])
      
      



, , - . , :

















  • . - . :





    [
    {:notify_player, :player_1, {:deal_card, %{rank: 4, suit: :hearts}}},
    {:notify_player, :player_1, {:deal_card, %{rank: 8, suit: :diamonds}}},
    {:notify_player, :player_1, :move}
    ]
    
          
          



    - , , . , , . , :





  • 1,





  • 1,





  • 1,





    . , , GenServer



    , . , , . () , Round



    .





, round



, . , . , round



. , , instruction



.





, 1:





{instructions, round} = Blackjack.Round.move(round, :player_1, :hit)
      
      



, , . , , .





, :





[
  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},
  {:notify_player, :player_1, :busted},
  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},
  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},
  {:notify_player, :player_2, :move}
]
      
      



, 1 . 4 8 , , . 2 , , .





2:





{instructions, round} = Blackjack.Round.move(round, :player_2, :stand)

# instructions:
[
  {:notify_player, :player_1, {:winners, [:player_2]}}
  {:notify_player, :player_2, {:winners, [:player_2]}}
]

      
      



2 , . .





, Round



Deck



Hand



. Round



:





defp deal(round) do
  {:ok, card, deck} =
    with {:error, :empty} <- Blackjack.Deck.take(round.deck), do:
      Blackjack.Deck.take(Blackjack.Deck.shuffled())

  {hand_status, hand} = Hand.deal(round.current_hand, card)

  round =
    %Round{round | deck: deck, current_hand: hand}
    |> notify_player(round.current_player_id, {:deal_card, card})

  {hand_status, round}
end

      
      



, , . , , (:ok



:busted



) . :-)





notify_player



- , . (, GenServer Phoenix). - , . , , .





, , Round



. notify_player



. , , take_instructions



Round



, , .





, . , . - . , .





. , . , , . , , .





Blackjack.RoundServer, GenServer



. Agent



, , GenServer



. , , :-)





, start_playing/2



. start_link



, start_link



. , start_playing



- , .





: . - , . .





, :





@type player :: %{id: Round.player_id, callback_mod: module, callback_arg: any}
      
      



, . . , , callback_mod.some_function (some_arguments)



, some_arguments



, , callback_arg



, .





callback_mod



, :





  • , HTTP





  • , TCP





  • iex





  • ()





    . , .





, , :





@callback deal_card(RoundServer.callback_arg, Round.player_id,
  Blackjack.Deck.card) :: any
@callback move(RoundServer.callback_arg, Round.player_id) :: any
@callback busted(RoundServer.callback_arg, Round.player_id) :: any
@callback winners(RoundServer.callback_arg, Round.player_id, [Round.player_id])
  :: any
@callback unauthorized_move(RoundServer.callback_arg, Round.player_id) :: any
      
      



, . , . . , , , , .





- , . . asserting/refuting , RoundServer.move/3



, .





Round



, , .





. , . - , . , , . , , . , .





Blackjack.PlayerNotifier, GenServer



, - . start_playing/2



, .





, . , //(M/F/A) .





, , (, , ). , . :





[
  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},
  {:notify_player, :player_1, :busted},
  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},
  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},
  {:notify_player, :player_2, :move}
]
      
      



, player_2



, player_1



, . , . , , , , .





, : Round



, . .





OTP :blackjack



( Blackjack). , : Registry



( ) :simple_one_for_one



, .





, . . Phoenix, Cowboy, Ranch ( TCP), elli , . , .





Demo, , , GenServer, , :





$ iex -S mix
iex(1)> Demo.run

player_1: 4 of spades
player_1: 3 of hearts
player_1: thinking ...
player_1: hit
player_1: 8 of spades
player_1: thinking ...
player_1: stand

player_2: 10 of diamonds
player_2: 3 of spades
player_2: thinking ...
player_2: hit
player_2: 3 of diamonds
player_2: thinking ...
player_2: hit
player_2: king of spades
player_2: busted

...

      
      



, , :





, ? , ! , Deck



and Hand



, .





, . , . , . . , .





/ , . , , ( ), - . , . - , , . (netsplits), , - .





Finally, it's worth remembering the ultimate goal. Although I didn't go there (yet), I always planned that this code would be hosted on some kind of web server. So some decisions are made to support this scenario. Specifically, an implementation RoundServer



that accepts a callback module for each player allows me to connect to different types of clients using different technologies. This makes the blackjack service independent of specific libraries and frameworks (excluding standard libraries and OTP, of course) and makes it completely flexible.








All Articles