Fl4m3Ph03n1x

Fl4m3Ph03n1x

Functional Web Development with Elixir, OTP - Is OTP part of the entity layer?

Background

As the title implies, I am reading Funcional Web Development with Elixr, OTP and Phoenix and I have finished today chapter 4 where they introduce OTP and GenServers.

For those unfamiliar, the book walks you through the process of making a Battleships game (but with a different name).

Up until this chapter, everything was nicely separated - we had our business entities (Ships, Guesses, a Board, etc) and our Business Logic in the form of a state machine (which I think is rather brilliant).

So now we have the entities to play the game and the logic. This is where the Game entity comes along and this is what confused me.

Questions

The Game entity is a GenServer. Up until now, Ships and Guesses were merely modules with functions. But now we introduce an OTP behavior to the business entities. This brings up a few questions:

  1. Isn’t functional programming supposed to decouple logic and entities from third party concepts like OTP behaviours? I mean, if I decide to port this app tomorrow, I can’t use the functional core because it is coupled to OTP.
  2. Should OTP behaviours be part of the entity layer?
  3. Should I even care if an entity is an OTP behaviour or not?

Looking forward to someone with some architectural knowledge for some hints on how to see this.

Most Liked

peerreynders

peerreynders

Better?

# alias IslandsEngine.{Game, Rules}
# {:ok, game} = Game.start_link("Miles")
# Game.guess_coordinate(game, :player1, 1, 1)
# Game.add_player(game, "Trane")
# Game.position_island(game, :player1, :dot, 1, 1)
# Game.position_island(game, :player2, :square, 1, 1)
# state_data = :sys.get_state(game)
# state_data = :sys.replace_state(game, fn data -> %{state_data | rules: %Rules{state: :player1_turn}} end)
# state_data.rules.state
# Game.guess_coordinate(game, :player1, 5, 5)
# Game.guess_coordinate(game, :player1, 3, 1)
# Game.guess_coordinate(game, :player2, 1, 1)

defmodule IslandsEngine.State do
  alias IslandsEngine.{Board, Coordinate, Guesses, Island, Rules}

  @players [:player1, :player2]

  def init(name) do
    player1 = %{name: name, board: Board.new(), guesses: Guesses.new()}
    player2 = %{name: nil, board: Board.new(), guesses: Guesses.new()}
    %{player1: player1, player2: player2, rules: %Rules{}}
  end

  def add_player(state, name) do
    with {:ok, rules} <- Rules.check(state.rules, :add_player) do
      state
      |> update_player2_name(name)
      |> update_rules(rules)
      |> success()
    else
      :error -> :error
    end
  end

  def position_island(state, player, key, row, col) when player in @players do
    board = player_board(state, player)

    with {:ok, rules} <-
           Rules.check(state.rules, {:position_islands, player}),
         {:ok, coordinate} <-
           Coordinate.new(row, col),
         {:ok, island} <-
           Island.new(key, coordinate),
         %{} = board <-
           Board.position_island(board, key, island) do
      state
      |> update_board(player, board)
      |> update_rules(rules)
      |> success()
    else
      error -> error
    end
  end

  def set_islands(state, player) when player in @players do
    board = player_board(state, player)

    with {:ok, rules} <- Rules.check(state.rules, {:set_islands, player}),
         true <- Board.all_islands_positioned?(board) do
      state
      |> update_rules(rules)
      |> success({:ok, board})
    else
      :error -> :error
      false -> {:error, :not_all_islands_positioned}
    end
  end

  def guess_coordinate(state, player, row, col) when player in @players do
    opponent_key = opponent(player)
    opponent_board = player_board(state, opponent_key)

    with {:ok, rules} <-
           Rules.check(state.rules, {:guess_coordinate, player}),
         {:ok, coordinate} <-
           Coordinate.new(row, col),
         {hit_or_miss, forested_island, win_status, opponent_board} <-
           Board.guess(opponent_board, coordinate),
         {:ok, rules} <-
           Rules.check(rules, {:win_check, win_status}) do
      state
      |> update_board(opponent_key, opponent_board)
      |> update_guesses(player, hit_or_miss, coordinate)
      |> update_rules(rules)
      |> success({hit_or_miss, forested_island, win_status})
    else
      error ->
        error
    end
  end

  defp player_board(state, player),
    do: Map.get(state, player).board

  defp opponent(:player1),
    do: :player2

  defp opponent(:player2),
    do: :player1

  defp update_rules(state, rules),
    do: %{state | rules: rules}

  defp update_player2_name(state, name),
    do: put_in(state.player2.name, name)

  defp update_board(state, player, board),
    do: Map.update!(state, player, fn player -> %{player | board: board} end)

  defp update_guesses(state, player, hit_or_miss, coordinate) do
    update_in(state[player].guesses, fn guesses ->
      Guesses.add(guesses, hit_or_miss, coordinate)
    end)
  end

  defp success(state),
    do: {:ok, state}

  defp success(state, other),
    do: {:ok, state, other}
end

defmodule IslandsEngine.Game do
  use GenServer

  alias IslandsEngine.State

  ## --- API
  @players [:player1, :player2]

  def add_player(game, name) when is_binary(name),
    do: GenServer.call(game, {:add_player, name})

  def position_island(game, player, key, row, col) when player in @players,
    do: GenServer.call(game, {:position_island, player, key, row, col})

  def set_islands(game, player) when player in @players,
    do: GenServer.call(game, {:set_islands, player})

  def guess_coordinate(game, player, row, col) when player in @players,
    do: GenServer.call(game, {:guess_coordinate, player, row, col})

  ## ---

  def via_tuple(name), 
    do: {:via, Registry, {Registry.Game, name}}

  def start_link(name) when is_binary(name),
    do: GenServer.start_link(__MODULE__, name, name: via_tuple(name))

  def init(name),
    do: {:ok, State.init(name)}

  def handle_call({:add_player, name}, _from, state) do
    case State.add_player(state, name) do
      {:ok, next_state} ->
        reply_success(next_state, :ok)

      error ->
        {:reply, error, state}
    end
  end

  def handle_call({:position_island, player, key, row, col}, _from, state) do
    case State.position_island(state, player, key, row, col) do
      {:ok, next_state} ->
        reply_success(next_state, :ok)

      error ->
        {:reply, error, state}
    end
  end

  def handle_call({:set_islands, player}, _from, state) do
    case State.set_islands(state, player) do
      {:ok, next_state, reply} ->
        reply_success(next_state, reply)

      error ->
        {:reply, error, state}
    end
  end

  def handle_call({:guess_coordinate, player, row, col}, _from, state) do
    case State.guess_coordinate(state, player, row, col) do
      {:ok, next_state, reply} ->
        reply_success(next_state, reply)

      error ->
        {:reply, error, state}
    end
  end

  defp reply_success(state, reply), do: {:reply, reply, state}
end

Now State contains the logic/core (and can be tested separately) - Game has been reduced to a process (GenServer) shell.

lance

lance

Author of Functional Web Development with Elixir, OTP, and Phoenix

My first impression here is that I’m wary of trying to apply ideas from other ecosystems wholesale onto the BEAM ecosystem. When I was new to Elixir, I did it, and I know it did not help me.

Part of the idea of the book is to explore what’s possible in this new world. There are things that we can do on the BEAM that you would be hard pressed to do in other languages/ecosystems. Trying to apply rules/approaches/schemes from those other ecosystems onto this one seems unhelpful.

My strong preference would be to look at what we’re doing in the book and judge it on it’s own merits rather than comparing it to other systems or trying to fit it into a paradigm from another ecosystem.

That’s me trying to say, let’s please leave Java at the door. :^)

I’m also reminded of that old joke about the best answers to software questions always beginning with, “It depends.”

To me, questions like the one we’re looking at come into the realm of design decisions based on the constraints of the individual project. As we have no doubt seen, the only constant in software work is change.

The most important thing, then, is to design things with flexibility in mind. The individual choices we make are important, but the ability to easily change what we’ve done seems to me to be even more important.

Early in the book, we talk about having the ability to run thousands and thousands of games on the same node. I take that a constraint on the system. We need to solve for that in the design.

You’ve agreed that when we build a GenServer, we’re really defining a module and functions that work on some data. The change is that we decided that those functions should all run in a separate process (or processes).

To me, that says that we’re still working with the same things we were working with when we defined Board, Island, Guesses, and Coordinate, but by making this piece a GenServer, we satisfy the need for many games on the same node.

By using GenServer, we just did the two things at once - define the entity with a module, some functions, and some data as well as allow us to run it in multiple processes.

Here’s where your personal choice comes in. If this makes you feel uncomfortable, you can absolutely use the code for an intermediary module that @peerreynders wrote up. All will be well.

To me, the key point is that the design we have in the book makes that refactor a straight forward affair. You could do it wholesale. You could do it one api function/callback at a time.

It’s your choice. You can do it in response to changing requirements. You can manage the transition safely.

peerreynders

peerreynders

Is there a “shortage” of Erlang/Elixir patterns? Maybe, maybe not:

http://erlang.org/pipermail/erlang-questions/2007-October/030091.html

Garrett Smith attempted an Erlang specific repository but contributions stalled.

And even if a prescriptive pattern was followed - it could still be wrong if it is applied in the wrong context (this is a real problem in OOP with respect to design patterns).

Ultimately Game is just a process manipulating some data structures (Board, Coordinate, Guesses, Island, Rules) - and Game itself is going to be part of a supervision tree which itself is part of an OTP application.

For a discussion on how to structure supervision trees: The Hitchhiker’s Guide to the Unexpected
Data structure vs Process: To spawn, or not to spawn?
Structured Programming Using Processes (2004)

Building applications in INSERT_TECHNOLOGY_X_HERE doesn’t change how good applications are built

Yes, but going from Java to C# isn’t changing the paradigm so the solution shape isn’t impacted. But going to Erlang/Elixir you are shifting paradigms into “Concurrency Oriented Programming” which means that processes and most of OTP becomes part of the Core rather than being a

What seems to currently be challenging you the most is that a process can be an entity.

I’ve commented on the onion architecture before and in my view the most insightful aspect was that the domain layer gets to dictate the contracts while the infrastructure has to implement those contracts (e.g. nothing in the data store is influencing what is going on in the domain).

That particular advice still applies to Elixir - if that is the type of system that is being built.

Most of these architectures boil down to examining the structure and characteristics of a system along certain dimensions:

  • Boundaries
    • at all levels of granularity (types, user defined types (modules), processes, supervision trees, applications, systems)
    • physical vs logical boundaries
    • (high) cohesion of logic within any one boundary
    • (low) coupling between boundaries
      • dependencies from within the boundary
      • dependencies on the boundary
      • i.e. good inter-boundary contracts (hiding implementation details, correct (specification → implementation) direction, etc.)

Alongs those lines what are the problems that you see with the design of Game?

Now there are some games you can play to hide whether or not something is a data structure or a process. But processes that are part of a supervision tree are known to be processes - otherwise they couldn’t be part of the tree.

keathley

keathley

I guess I’m confused what you mean when you say the “entity” layer. In the context of the book, the games are built as data structure. They take in new data and provide a new data structure back to you. In order to take these data structures and make them into a useful game we put the data structure in a process and allow the process to manage the lifecycle of that game. I’m really not sure where the hangup is I guess.

As to throwing things out, I don’t think anyone is throwing out any established architectural patterns here. The book provides an alternative to all of the patterns you mentioned. At least I think it does. Out of all the patterns you’ve mentioned the only I’d ever heard of was “hexagonal” (a pattern that I’m not a fan of tbh). I don’t know what “onion” or “lasagna” architectures are but I guess they involve layers?

Either way I don’t think that you should take the book as the end all be all to design. If you want to just put stuff in a database that’s quite alright and that concept will generally scale well and lead to less stateful designs. But if you need to bring state into your system then the book shows you a way to layer together a functional interface with stateful processes.

peerreynders

peerreynders

Other languages don’t have the concurrency primitives (spawn, send, receive), process linking and monitoring that OTP is based on.

At the core a process is an infinitely recursing function where the updated state is supplied on each recursive call - which is equivalent to an infinite iteration.

One uses Erlang/Elixir to take advantage of these features and to be able to structure behaviour in a way that isn’t possible in other environments.

Would you find this code easy to port another language?

Fundamentally you should chose Elixir/Erlang because it makes it easier to express the solution to your problem - it follows that it would be more difficult to do in another language.

In his book Seven Languages in Seven Weeks , Bruce Tate suggests that “Erlang makes hard things easy and easy things hard.”

Maybe the problem is that you are trying to classify “Game” as an entity rather than as the “engine” of the application - this reminds me of the thought concerns vs. runtime concerns discussion with regards to To spawn, or not to spawn? which had an influence in the rewrite of the book.

Coupling :003:

Essentially the Game “entity” you are looking for is either the GenServer state or some significant part of it.

So indeed to allay your concerns you could factor that part out - away from the GenServer if you wish.


I presume we are talking about this:
https://media.pragprog.com/titles/lhelph/code/gen_server/lib/islands_engine/game.ex

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement