Discussion:
[elm-discuss] Looking for a better way to use `andThen` chains (and other beginner questions)
Stefan Matthias Aust
2017-04-21 21:37:58 UTC
Permalink
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 ->
if p.no == planet.no then
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
|> setOwner team.no
|> updatePlanetInStar star
|> updateStarIngame game1
in
Ok game2
)
)

I'm really sorry for this long mail :-)

Stefan
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Peter Damoc
2017-04-23 07:28:23 UTC
Permalink
Post by Stefan Matthias Aust
*My first question*: Is there any way to create a constrained type like
"something that has a `no` field?
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)
You could use record pattern matching to create generic functions but in
the above case that won't work because the container name is different.

Here is how it would have looked if the container name would have been the
same (in this case `children`)

findChild : Int -> { b | no : Int, children : List { a | no : Int } } ->
Maybe { a | no : Int }
findChild no parent =
List.head (List.filter (\child -> child.no == no) parent.children)

I used Int for the `no` but you you can leave that also as a parameter
(e.g. `c`)
Post by Stefan Matthias Aust
*Second question*: Is there an easy way to replace an element in a list?
You can create a generic helper like
swapIfSameNo : { a | no : Int } -> { a | no : Int } -> { a | no : Int }
swapIfSameNo a b =
if a.no == b.no then
a
else
b

and then the updates would be simpler

updatePlanetInStar : Star -> Planet -> Star
updatePlanetInStar star planet =
{ star | planets = List.map (swapIfSameNo planet) star.planets }


The same comments from above apply. If you have a generic name for the
container (children) you can make this a generic updateChild function that
would work on both.
Post by Stefan Matthias Aust
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?
What make you think Array is unsupported?

Regarding indexes in arrays (or lists for that matter), it would be lovely
to be able to say things like that but it is unsafe. You might be using an
out of bounds index.
There are no solutions in contexts like these, only tradeoffs. Elm choses
to make things more verbose and more explicit in order to guarantee safety.
That's the tradeoff that Elm chose.
Post by Stefan Matthias Aust
*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"?
What I would try to do different is split buildDefense in two between
finding the proper planet and doing something with it

findPlanet : TeamNo -> StarNo -> PlanetNo -> Game -> Result Error ( Planet,
Star )
findPlanet teamNo starNo planetNo game =
findTeamR teamNo game
|> Result.andThen
(\team ->
findStarR starNo game
|> Result.andThen
(\star ->
findPlanetR planetNo star
|> Result.andThen
(\planet ->
if planet.owner /= team.no then
Err NotYourPlanet
else
Ok ( planet, star )
)
)
)


buildDefense : TeamNo -> StarNo -> PlanetNo -> Int -> Game -> Result Error
Game
buildDefense teamNo starNo planetNo amount game =
let
planetAndStar =
findPlanet teamNo starNo planetNo

updateGame ( planet, star ) =
if amount < 1 then
Err InvalidAmount
else if planet.resources < amount * defenseCost then
Err NotEnoughResources
else
planet
|> addDefense amount
|> subtractResources (amount * defenseCost)
|> updatePlanet star
|> updateStar game
|> Ok
in
Result.andThen updateGame planetAndStar
--
There is NO FATE, we are the creators.
blog: http://damoc.ro/
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Max Goldstein
2017-04-23 18:09:47 UTC
Permalink
of lists with numbered
items, have you thought about using a dictionary
<http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Dict>? Something
like this?

import Dict exposing (Dict)

type alias No = Int
type alias Game = { stars : Dict No Star, teams : Dict No Team }
type alias Team = { name : String, color : String, species : String }
type alias Star = { name : String, planets : Dict No Planet }
type alias Planet = { name : String, minerals : Int, colony : Maybe Colony }
type alias Colony = { team : No, defense : Int }

getStarPlanet : No -> No -> Game -> Result String Planet
getStarPlanet starNo planetNo {stars} =
Dict.get starNo stars
|> Result.fromMaybe "Star not found"
|> Result.andThen (\{planets} -> Dict.get planetNo planets |>
Result.fromMaybe "Planet not found")

It's still a little repetitive, and you need to nest a Result.fromMaybe
inside a Result.andThen callback, but it works. (You should maybe consider
a union type for these errors instead of strings.)

I've also tried to anticipate that you'll have uncolonized planets, which
will still have things like minerals, size, atmosphere, temperature, things
like that. Some of those will also have colonies which will have a team,
defense, buildings, colonists, and so on.

Incidentally, if you're okay fixing the number of teams at compile-time,
you might be able to avoid the inconsistent state of a colony whose team is
not in the team dictionary.

type TeamNo = Red | Blue
type alias Teams = { red : Team, blue : Team }
getTeam : TeamNo -> Teams -> Team
getTeam no teams =
if no == Red then teams.red else teams.blue

If you're looking for a somewhat broader background on data modeling in
Elm, this talk is a must-watch: Making Impossible States Impossible
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Max Goldstein
2017-04-23 18:21:35 UTC
Permalink
Post by Max Goldstein
If you're looking for a somewhat broader background on data modeling in
Elm, this talk is a must-watch: Making Impossible States Impossible
Sorry, looks like the YouTube link got put at the top of the post.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Loading...