Almah

Almah

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: javascript - Toy Robot Simulator - Code Review Stack Exchange

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:

Most Liked

Almah

Almah

Ah, my apologies. I jumped the gun a little and forgot to give context. The Toy Robot challenge can be found here (javascript - Toy Robot Simulator - Code Review Stack Exchange) but to summarize:

  • Place a robot on a 5x5 grid.
  • Feed the robot commands such as
    • PLACE 1,1,NORTH # Places the robot on grid (1,1) of the board facing north.
    • MOVE # Move the robot one square in the direction it’s facing
    • LEFT # Turn the robot left
  • Ensure that the robot cannot leave the board and prevent illegal behavior

The Command Pattern is a way to abstract behavior (the commands that act on the robot) and encapsulate all data necessary to complete the command in one place. I’m unaware of an elixir-based solution, I’m sorry. It might also be that the pattern is expressed naturally in functional code and doesn’t need to be “patternized.”

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Right in some sense the “Command Pattern” from OO is just about passing around a data object, and in a functional language all that you pass around is data, so it doesn’t really need a formal name.

The normal thing to do in Elixir here would be to have some sort of %Command{} struct that contained the information a command needed. In reviewing the prompt though a struct is almost more complicated than you need as there just aren’t that many instructions, you could probably just get away with a handful of atoms and maybe a tuple for {:place, x, y}.

Probably the other big question is whether the command should itself have a callback to execute itself, or whether the world should execute the command. If you want to stick with having the command execute itself then that’s fine, but the world needs to run a validity check afterward on the resulting world. It shouldn’t be up to the command to validate the world, the world should validate the world.

al2o3cr

al2o3cr

Here’s a version that’s in Elixir but feels more Erlang-y, in particular:

  • most control flow is pattern-matching
  • most heads are single expressions
  • everything is made of atoms, tuples, and lists
  • the way step accumulates “commands” is modeled after gen_statem’s “actions”. This keeps side-effects like printing to stdout out of functions like run

I find this style useful for small one-off tasks like Advent of Code; bigger and longer-lived code can benefit from investing in more-complex features:

  • structs instead of tuples so data is more self-describing
  • better modularity, encapsulating things like to_dir / from_dir separately
  • polymorphic dispatch (via protocols, etc) to decouple “parsing commands” and “running commands”

But those approaches take longer to write :stuck_out_tongue: so stay tuned.

defmodule RobotCommands do
  @commands ~r/
    \A(?|
    (?:(PLACE)\ (\d+),(\d+),(NORTH|SOUTH|EAST|WEST))
    | (MOVE)
    | (LEFT)
    | (RIGHT)
    | (REPORT)
    )\z
    /x

  @max_x 5
  @max_y 5

  def read(stream) do
    stream
    |> Stream.map(&String.trim/1)
    |> Stream.map(&Regex.run(@commands, &1, capture: :all_but_first))
    |> Stream.map(&parse/1)
  end

  def parse(["PLACE", x_string, y_string, dir_string]) do
    {:place, String.to_integer(x_string), String.to_integer(y_string), to_dir(dir_string)}
  end

  def parse(["MOVE"]), do: :move
  def parse(["LEFT"]), do: :left
  def parse(["RIGHT"]), do: :right
  def parse(["REPORT"]), do: :report
  def parse(_), do: :nop

  defp to_dir("NORTH"), do: :north
  defp to_dir("SOUTH"), do: :south
  defp to_dir("EAST"), do: :east
  defp to_dir("WEST"), do: :west

  defp from_dir(:north), do: "NORTH"
  defp from_dir(:south), do: "SOUTH"
  defp from_dir(:east), do: "EAST"
  defp from_dir(:west), do: "WEST"

  defguardp in_bounds(x, y) when x >= 0 and x <= @max_x and y >= 0 and y <= @max_y

  def step(command, nil), do: step(command, {nil, []})
  def step(command, {state, outputs}) do
    case run(command, state) do
      {new_pos, new_dir} ->
        {{new_pos, new_dir}, outputs}
      {new_pos, new_dir, new_outputs} ->
        {{new_pos, new_dir}, outputs ++ new_outputs}
    end
  end

  def run({:place, x, y, dir}, _) when in_bounds(x, y) do
    {{x, y}, dir}
  end
  def run(_, nil), do: nil
  def run(:move, {pos, dir}), do: {bound(pos, move(pos, dir)), dir}
  def run(:left, {pos, dir}), do: {pos, rotate_left(dir)}
  def run(:right, {pos, dir}), do: {pos, rotate_right(dir)}
  def run(:report, {pos, dir}), do: {pos, dir, [{:report, pos, dir}]}
  def run(:nop, state), do: state

  defp bound(_, {new_x, new_y}) when in_bounds(new_x, new_y), do: {new_x, new_y}
  defp bound(old_pos, _), do: old_pos

  defp move({x, y}, :north), do: {x, y+1}
  defp move({x, y}, :south), do: {x, y-1}
  defp move({x, y}, :east), do: {x+1, y}
  defp move({x, y}, :west), do: {x-1, y}

  defp rotate_left(:north), do: :west
  defp rotate_left(:south), do: :east
  defp rotate_left(:east), do: :north
  defp rotate_left(:west), do: :south

  defp rotate_right(:north), do: :east
  defp rotate_right(:south), do: :west
  defp rotate_right(:east), do: :south
  defp rotate_right(:west), do: :north

  def side_effects({_robot, commands} = result) do
    Enum.each(commands, &do_command/1)

    result
  end

  defp do_command({:report, {x, y}, dir}), do: IO.puts("#{x},#{y},#{from_dir(dir)}")
end

File.stream!("input.txt")
|> RobotCommands.read()
|> Enum.reduce(nil, &RobotCommands.step/2)
|> RobotCommands.side_effects()

christhekeele

christhekeele

Could you share a link to the problem you are solving? I cannot find one in the repository you provide, and what you are trying to solve (with or without a Elixirish version of the command pattern) is unclear.

christhekeele

christhekeele

This feels like a fun project to tackle with a CLI app, with a graphical interface!

So, I build a Elixir project seed for it, if people want to try solving it that way: challenges/toy-robot at latest · christhekeele/challenges · GitHub

More instructions on how to clone the seed and view my attempt at solving here.

Where Next?

Popular in Questions Top

Tee
can someone please explain to me how Enum.reduce works with maps
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New

Other popular topics Top

openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
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
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
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
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement