Need help "elixirizing" my Toy Robot solution

Hey everyone!

I’m working my way into elixir and loving it so far. However, coming from an OOP background with Java/Kotlin, I don’t think I’m writing intuitive or “elixir-style” code. I’d love some feedback on my Toy Robot solution which you can find here, but I’ll paste the relevant bits I think need to be improved.

What is the Toy Robot Challenge? Here is a CodeReview question that has the entire Toy Robot brief in case you’re curious: https://codereview.stackexchange.com/questions/236006/toy-robot-simulator

I’ve tried to implement a version of the Command pattern, but I don’t think I did it very well. Every command has a Behavior they… inherit? from:

(I have omitted my error handling for brevity)

defmodule ToyRobot.Commands.Command do
  @type state :: :uninitialized | :initialized
  @type t() :: {state(), Board.t(), Robot.t()}

  @callback execute(any(), t()) :: t()
end

defmodule ToyRobot.Commands.PlaceCommand do
  @behaviour ToyRobot.Commands.Command

  @impl ToyRobot.Commands.Command
  def execute(%{x: x, y: y, facing: facing}, {:uninitialized, board, _robot}) do
    {:initialized, board, %ToyRobot.Robot{x: x, y: y, facing: facing}}
  end

  @impl ToyRobot.Commands.Command
  def execute(%{}, {:initialized, _, _} = state) do
    state
  end
end

defmodule ToyRobot.Commands.LeftCommand do
  @behaviour ToyRobot.Commands.Command

  @impl ToyRobot.Commands.Command
  def execute(%{}, {:initialized, board, robot}) do
    {:initialized, board, ToyRobot.Robot.left(robot)}
  end

  # Error handling
end

Each command is then sent to a GenServer which contains the state information about the world:

defmodule ToyRobot.Boundary.World do
  use GenServer

  def start_link(options \\ []) do
    GenServer.start_link(__MODULE__, {:uninitialized, %ToyRobot.Board{width: 5, height: 5}, nil}, options)
  end

  def get_world(manager \\ __MODULE__) do
    GenServer.call(manager, {:get_world})
  end

  def execute_command(manager \\ __MODULE__, {name, command_args, callback_fn}) do
    GenServer.call(manager, {:execute, name, command_args, callback_fn})
  end

  def init(world) do
    {:ok, world}
  end

  def handle_call({:initialize}, _from, world) do
    {:reply, :ok, world}
  end

  def handle_call({:get_world}, _from, world) do
    {:reply, {:ok, world}, world}
  end

  def handle_call({:execute, _name, command_args, callback_fn}, _from, world) do
    {:reply, :ok, callback_fn.(command_args, world)}
  end
end

As you can see, a command is created and contains:

  • Name of the command
  • Arguments to invoke the command
  • The command function that will operate on its provided args and the world state

The GenServer contains the world state and is the one to execute the command by invoking the command function with callback_fn.(command_args, world)

Finally, my CommandParser would parse a string to a command which can then be executed. The code is unfinished, but you can roughly see what that should be like here:

defmodule ToyRobot.CommandParser do
  @moduledoc """
  Parses commands from a file or command line.
  """
  alias ToyRobot.Commands.{PlaceCommand, MoveCommand, LeftCommand, RightCommand, ReportCommand, TeleportCommand}

  def parse("PLACE " <> args) do
    [x, y, facing] = String.split(args, ",")


    {:place, %{x: 1, y: 1, facing: :north}, &PlaceCommand.execute/2}
  end

  def parse("MOVE") do
    {:move, %{}, &MoveCommand.execute/2}
  end

  # Other commands
end

I have a feeling that my Command-code is very, well, Java-oriented and I’m doing too much work for what I’m trying to achieve. I just don’t know how to do it better yet. Any help would be greatly appreciated! :slight_smile: