This is a console app to simulate navigating a rover across the Martian surface. The user is first asked to specify the size of the plateau to explore, and then an initial position and facing direction on that surface. The user may then provide instructions:
- M = move one grid unit
- L = rotate 90 degrees to the left
- R = rotate 90 degrees to the right
- Q = quit the program
Load MarsRover.sln
in Visual Studio, from where the project can be run and tested using Visual Studio's user interface.
- Clone or download the repository.
cd
into the repo folder:
cd mars-rover
cd
into theMarsRover
project.
cd MarsRover
- Run the project:
dotnet run
This will build and run the project.
cd
into the tests folder in the repo:
cd MarsRover.Tests
- Run the tests with:
dotnet test
I've used a pure functional approach to structuring the app. This means inter alia using:
- Value types instead of reference types. For example, the
MissionControl
class never changes state when the rover changes position. Instead, a newMissionControl
class is created with a new rover each time there is a change. - Recursion instead of loops.
- Monadic types from the excellent LanguageExt library, especially
Option
andEither
. The use of the latter means that it is never necessary tothrow
andcatch
exceptions. Instead, functions that may not succeed return anEither<string, T>
, whereT
is the return type of the corresponding non-monadic function. If the function succeeds,T
is returned wrapped in theEither
monad. If it fails, astring
carrying the error message is returned, also wrapped in theEither
. (For an introduction to the practice and advantages of functional programming in C#, see Paul's Louth's blog. On monads in particular, see his introduction.)
One of the great benefits of wrapping return values in monadic types, like Either
, is that it is possible to use LINQ query expressions with them. Consider the following code from the ConsoleUI
of the MarsRover
project:
public static Either<string, MissionControl> GetInitialSetup()
{
return from plateauSize in GetPlateauSize(None)
from plateau in Plateau.FromPlateauSize(plateauSize)
from missionControl in MissionControl.FromPlateau(plateau)
from position in GetInitialPosition(plateauSize, None)
from updatedMissionControl in missionControl.AddRover(position)
select updatedMissionControl;
}
As Paul Louth shows, this structure parallels do
notation in Haskell. The equivalent function in Haskell might be written like this:
GetInitialSetup :: Either String MissionControl
GetInitialSetup = do
plateauSize <- GetPlateauSize Nothing
plateau <- Plateau.FromPlateauSize plateauSize
missionControl <- MissionControl.FromPlateau plateau
position <- GetInitialPosition PlateauSize Nothing
updatedMissionControl <- AddRover missionControl position
pure updatedMissionControl
From this the similarity of the structure of the two is readily apparent:
C# | Haskell |
---|---|
from x in y |
x <- y |
select x |
pure x |
This project was written as a learning exercise as part of the Northcoders bootcamp.
The main project, MarsRover
has one dependency:
- LanguageExt: Extensions for functional programming in C# (MIT)
The test project depends on: