Hi there!
Just for fun, I started to learn Elm. I wonder how to idiomatically solve the following problems.
I decided to create a little 4X strategy game. Currently, I'm tinkering with the best representation of such a game and how to implement game commands that update the model.
Here's a simplified run down: The game has stars and players. Each star has planets. Planets have properties like size, population, industry, defense, etc. Planets can be owned by players. Spaceships (organized as fleets (aka taskforces aka tfs) can move between stars. They belong to players. One kind of ship is the transport and it can be used to colonize unoccupied planets.
I refer stars, planets, players and fleets by number (aka `no`) because (this is my current understanding) I have no other way to refer to records and I need a way for the user to enter commands (I intent to eventually create a retro text mode interface). So a command looks like `BuildDefense TeamNo StarNo PlanetNo Amount` or `LandTransports TeamNo TfNo StarNo PlanetNo Amount`. All that types are aliases for `Int`.
This is an excerpt from type definitions:
Game = { stars : List Star, teams : List Team }
Star = { no: StarNo, planets : List Planet }
Planet = { no : PlanetNo, defense : Int }
Command = BuildDefense ... | LandTransports ...
My first question: Is there any way to create a constrained type like "something that has a `no` field?
Right now, I have a lot of nearly identical code like this:
findStar : StarNo -> Game -> Maybe Star
findStar no game =
List.head (List.filter (\star -> star.no == no) game.stars)
findPlanet : PlanetNo -> Star -> Maybe Planet
findPlanet no star =
List.head (List.filter (\planet -> planet.no == no) star.planets)
In other languages, I could create some kind of protocol or interface or category and then declare `Star` or `Planet` to be conforming. Then I could implement common operations for conforming types.
I also didn't find some kind of "find first" operation in the standard library (which is frankly surprisingly small). It's easy enough to implement but why do I need to?
find : (a -> Bool) -> List a -> Maybe a
find test list =
case list of
head :: tail ->
if test (head) then
Just head
else
find test tail
other ->
Nothing
Because I like terse code, I used the much more compact although less efficient variant.
Also, mainly for esthetic reasons, I'd prefer to extend the `List` module so that my function reads `List.find` because if I have to use `List.map` and `Maybe.map` and `Result.map` and `Random.map` everywhere, I'd stay consistent.
Sidenote, would be using `<|` more idiomatic?
findPlanet no star =
List.head <| List.filter (\planet -> planet.no == no) star.planets
Second question: Is there an easy way to replace an element in a list?
As I have to recreate the whole game if I change something, I crafted a lot of helper function:
planet
|> addDefense amount
|> subtractResources amount * defenseCost
|> updatePlanetInStar star
|> updateStarInGame game
|> Ok
This ready nicely.
The `updatePlanetInStar` and `updateStarInGame` functions are however very verbose:
updatePlanetInStar : Star -> Planet -> Star
updatePlanetInStar star planet =
{ star
| planets =
List.map
(\p ->
planet
else
p
)
star.planets
}
Again, I'm unable to abstract it further.
Sidenote: I'd love to use
{ game | stars[x].planets[y] = planet }
and let the compiler deal with all that stupid boilerplate code. I realize that for this to work, I'd have to use `Array` (a rather unsupported type) instead of `List` but that would be fine. That way, I could alias my `no` to indices. By the way, do I really had to create my own `nth` function for Lists?
Third question. When implementing `BuildDefense`, I need to convert the no's to records and this could fail. Instead of working with `Maybe`, I use a `Result Error a` type and `Error` is a sum type that contains all the errors that could occur. However, I get some really ugly chaining…
buildDefense : TeamNo -> StarNo -> PlanetNo -> Int -> Game -> Result Error Game
buildDefense teamNo starNo planetNo amount game =
findTeamR teamNo game
|> Result.andThen
(\team ->
findStarR starNo game
|> Result.andThen
(\star ->
findPlanetR planetNo star
|> Result.andThen
(\planet ->
if amount < 1 then
Err InvalidAmount
else if planet.owner /= team.no then Err NotYourPlanet
else if planet.resources < amount * defenseCost then
Err NotEnoughResources
else
planet
|> addDefense amount
|> subtractResources (amount * defenseCost)
|> updatePlanet star
|> updateStar game
|> Ok
)
)
)
How am I supposed to write this "the Elm way"?
In other languages, I'd probably use exceptions. And I don't want to invert the control flow by creating tiny help functions where I'm currently using anonymous functions. I want to read the control from from top to bottom. BTW, `findTeamR` is `findTeam`, wrapped in `Result.fromMaybe InvalidTeam`. And while I could use `Result.map2` to resolve team and star in parallel, I cannot do this for the planet which is dependent on the star and therefore I didn't bother.
Here's my final code snippet:
landTransports : TeamNo -> TfNo -> StarNo -> PlanetNo -> Int -> Game -> Result Error Game
landTransports teamNo tfNo starNo planetNo amount game =
findTeamAndTfR teamNo tfNo game
|> Result.andThen
(\( team, tf ) ->
findStarAndPlanetR starNo planetNo game
|> Result.andThen
(\( star, planet ) ->
if amount < 1 || amount > tf.ships.transports then
Err InvalidAmount
else if tf.dest /= star.no || tf.eta /= 0 then Err TfNotAtStar
else if (planet.owner /= team.no) && (planet.owner /= noTeam) then Err PlanetNotYours
else if planet.population + amount > planet.size then
Err TooMuchPopulation
else
let
game1 =
tf
|> substractShips { noShips | transports = amount }
|> updateTfInTeam team
|> updateTeamInGame game
game2 =
planet
|> addPopulation amount
|> updatePlanetInStar star
|> updateStarIngame game1
in
Ok game2
)
)
I'm really sorry for this long mail :-)
Stefan