JEG2

JEG2

Author of Designing Elixir Systems with OTP

I’ve been playing with several simulations for a while now, to learn more about how Elixir’s processes work. Now I am cleaning up some ideas in preparation for a training at ElixirConf.

I’ve seen multiple things that work well and plenty that don’t. I have a lot of ideas about what I want to show and talk about. However, I would love to see how others would approach the same challenges. Maybe your ideas are even better than mine!

If you’re interested in contributing, I would love to hear how you would design a solution to the following problem. Don’t feel like you have to write code. You can just describe what processes and data structures you would use.

I may have followup questions, depending on how this goes. :smiley:

Here’s the problem:

Draw a 64 lily pads wide by 128 lily pads high pond with 3,000 turtles and 3,000 frogs distributed randomly across them. No critter should share a pad. Each critter periodically counts surrounding neighbors of the same type. If the same type count is less than 30% of the total number of neighbors (up to eight), the critter will try to move to an open pad in a random direction, some random distance between one and five steps away.

Thanks in advance for any input you provide!

Showing Posts 1 to 10

terakilobyte

terakilobyte

Do they all evaluate the 30% rule at the same time or 1 by 1?

If they are below 30% but one of their species jumps in and raises above that threshold, should they still move or no?

Qqwy

Qqwy

TypeCheck Core Team

All right, let’s give this a go.

64*128 + 3000+3000 = 14192. This is quite a lot, but I think I would still go for the full ‘everything is a process’ approach here, because it makes it the most easy to extend the system in the future (e.g. adding extra logic to a lilypad, adding extra logic to either of the critters, the moving logic, et cetera).

Every Lilypad is a process

Every Lilypad is a process. These are started in two steps by the main process (the ‘world’):

  1. Fill a map with the {x,y} tuples as keys, and their values will be the PIDs of the Lilypond-process at that location. (optionally passing each of them their position at startup).
  2. Connect all Lilypond processes, by telling each of them the PIDs of their horizontal, vertical and diagonal neighbours. When passed these PIDs, a Lilypad will start monitoring them.

When a Lilypad process crashes, this is seen by its neighbours, and it is then removed from their neighbours-list, so no critter can try to travel to a Lilypad that doesn’t exist anymore.

The World is linked with trapping exits to the Lilypads, so if one of them crashes, a new one is started that responds to the same location, which is then added to its neighbours (and the neighbours to itself).

Every Critter is a process.

Every Critter is a process. They are started by the world after the Lilypads have been generated. To do this, the map of (coordinates => pids) is shuffled, and then the first part of that is used as their new homes.

Critters are processes that determine ever so often if they want to move. They use Process.send_after to send themselves a message periodically to see if they are lonely (less than 30% of their eight neighbouring lilypads is taken. I in other words: less than three neighbours)

To check if they are lonely or not, they do the following:

  1. Ask the Lilypad they are standing on for a list of its neighbours
  2. Ask each of these neighbours if it contains something.

If there isn’t enough, we have a list of neighbours that are empty, so we can pick one at random and travel in that direction. After traveling, we repeat this procedure, with the only exception being that the connection we travel in is now fixed (so if we cannot travel further in a certain direction because a Lilypad is already taken, we stop).

‘Travel’ means that the Critter unlinks and deregisters itself from the Lilypad it came from, and links+registers itself to the Lilypad it went to.

Problems with this approach

The only ‘glaring’ problem with this solution is what happens when a Lilypad with a critter on it crashes: Right now, the Critter is not restarted, it just ‘drowns’(disappears) when the Lilypad it stood on ‘sinks’(crashes). This might be prevented by having the Critter<->Lilypad connection use monitors instead, and have a Critter that had to swim because the Lilypad it stood on does no longer exist periodically ping the World process to ask if there is already a new Lilypad for this location.

To simplify all of the ‘check if a Lilypad exists at a certain location’ checks, one could use name registration for the processes. However, with the amount of processes we have here, this might actually slow the system down. So this is something we should measure before we go one way or the other.

edit: as suggested below, this answer also has problems with drawing the resulting state.

Why this approach?

  • All of the components are easily extendable.
  • When one Lilypad or Critter dies, the rest of the system is unaffected.
  • There is no single-process bottleneck. The World process is only used during startup and during ‘where is the new lilypad’ resolution in the case one of them has crashed and needs to be restarted.
JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

I’ve purposely tried not to specify timing, because I feel like that’s a very interesting variable here. I’m open to either approach.

Yes, next time the critter checks they would decide they are now unhappy and seek to move.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

This is a neat approach.

It does change the way the simulation works a little bit from the way I have seen it done in the past. I have seen a critter jump out of a group when surrounded (because they can move up to five spaces). Using your approach though, the intervening pads must also be open.

I do think this is a cool aspect of your processes-as-a-datastructure solution.

However, I think there’s one aspect you haven’t yet considered: how do we manage the drawing of the current state of the pond?

bbense

bbense

However, I think there’s one aspect you haven’t yet considered: how do we manage the drawing of the current state of the pond?

MOAR PROCESSES…

The View of the pond is essentially a log of all the state changes. The tricky problem is ordering these state changes. One approach would be to use mnesia to create a virtual shared log, another approach would be to simply timestamp each state change and use standard logging services. The recent changes to time in Erlang 18 ( i.e. an absolute incremental timer) would be useful.

The current process model emits events, you need a separate set of processes to consume and visualize the events.

gregvaughn

gregvaughn

I could do it in a single process. I’ve done Conway’s Game of Life that way. This would be my approach if I were to interpret the critter movement as being a synchronized “turn”.

However, I suspect you’re looking to use this as an exercise in concurrency, so plan B would be to treat each critter as a separate process. I’d expect to have the lily pad map stored in an ETS table, likely with a helper function to get stats of what’s in neighboring cells. One other separate rendering process can get a snapshot of the ETS data and display it on a timed interval.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

Any thoughts on the differences of using ets verses an Agent for this? Just curious.

One problem I’ve seen some, using similar approaches to this, is the need to copy the current state into a rending process. A 64 by 128 pond is over 8,000 pads. Depending on the data structure, this memory copy can take time, enough to slow the rendering refresh rates down.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

14,192, actually.

gregvaughn

gregvaughn

ETS can allow parallel reads (IIRC). An Agent is going to serialize all reads. I haven’t done much with ETS yet, but this could be a great place to use match specifications. I need to try to find some spare time to try to code some of this. It’s starting to sound like fun :slight_smile:

I’m getting all hand-wavey now because my knowledge is not that concrete. ETS tables can have a single owning process that is the only one allowed to write. This could be the one responsible for rendering. Each critter process would have to send a message in order to move. The receiving process could have a cached version of the data optimized for rendering that it updates as well as updating ETS.

I’m just guessing here without perf numbers, but that render-optimized data structure might be best served as an Erlang array because it needs random access updates.

JEG2

JEG2 OP

Author of Designing Elixir Systems with OTP

Question for the Processes All The Way Down Team: it seems that the only state we need to store is the current location of the critters and we could choose to keep that in critter processes or lily pad processes, so is there really a good argument for doing both?

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews