So, I decided that the quickest way for me to "get going" in Scala was to generate a simple and small version of Life in Java. And then directly convert the class from Java to Scala, treating Scala as "Java without the semicolons". Said more specifically, use Java's imperative style the starting point, a basic clump of digital clay, so-to-speak. And then, over time (and multiple blog entries) I will slowly mold the clay clump into something more useful and interesting.
As a simple recap (which is a copy and paste from the Wiki link - go there to see actual gif animations of the simulation in action), here is the basic domain requirements of Life:
The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, live or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:So, my first step is to imagine how I would approach this in Java. My goal is to write a small amount of Java code that achieves this goal. However, it will be quite annoying to have to include all the overhead of having to render the resulting animation. So my very first pass at this problem is to just get the simple rule set working on a relatively simple Java class. The resulting two files are below (and not even very well named). I
The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed—births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick (in other words, each generation is a pure function of the preceding one). The rules continue to be applied repeatedly to create further generations.
- Any live cell with fewer than two live neighbours dies, as if caused by under-population.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by overcrowding.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Additionally, while the Java code functions, it is a LONG way from ideal in many ways (OO design, performance, configuration, multi-threading, etc.). However, I figured that it would be more interesting (and fun) if this series showed the evolution of the Java code towards what is considered ideal and just how much boilerplate that ends up requiring. This will create a great basis from which to compare the Scala code, which I hope (and will only be able to do so until I have actually written the Scala code) will be quite a bit shorter with substantially less (if any) boilerplate required.
I am just glad to finally have this post completed. Java code's below. And, the code as an Eclipse workspace is located in a bitbucket.org/Mercurial SCM repository.
Sidenote: What a FREAKING challenge to get code displayed in a blog web page entry. ARGH! Of the +20 hours I spent on this post, +10 of them was chasing all sorts of tangents attempting to figure out how to get the 120 lines of Java code displayed with some semblance of acceptable syntax highlighting. It was worth trying to figure it out, though. I really like the results. And now I get to use it in all future posts, pretty simply, too. If you are interested, here's a link to the solution I eventually found (and an awesome helper).
File 1 of 2: FrameVersion1.java (NOT ideal code - super simple first pass)
package com.blogspot.fromjavatoscala.life.java; public class FrameVersion1 { public static final int[][] NEIGHBOR_OFFSETS = //{x, y} pairs new int[][] { {-1, -1}, {0, -1}, {1, -1} , {-1, 0}, {1, 0} , {-1, 1}, {0, 1}, {1, 1} }; public static boolean[][] next(boolean [][] content) { boolean[][] result = new boolean[content.length][content[0].length]; //iterate through rows for (int y = 0, yCount = content.length; y < yCount; ++y) { //iterate through columns of particular row for (int x = 0, xCount = content.length; x < xCount; ++x) { result[y][x] = calcState(content, x, y); } } return result; } private static boolean calcState(boolean [][] content, int x, int y) { boolean result = false; int neighborsAliveCount = 0; for (int i = 0, count = NEIGHBOR_OFFSETS.length; (i < count); ++i) { int[] offset = NEIGHBOR_OFFSETS[i]; if (isAlive(content, x + offset[0], y + offset[1])) { ++neighborsAliveCount; } } boolean self = content[y][x]; if (self && (neighborsAliveCount < 2)) { result = false; //live cell dies from aloneness (too few live neighbors) } else if ( self && ((neighborsAliveCount == 2) || (neighborsAliveCount == 3)) ) { result = true; //live cell lives (sufficient live neighbors) } else if (self && (neighborsAliveCount > 3)) { result = false; //live cell dies from overcrowding (too many live neighbors) } else if ((!self) && (neighborsAliveCount == 3)) { result = true; //dead cell births (precisely enough neighbors) } return result; } private static boolean isAlive(boolean [][] content, int x, int y) { boolean result = false; if ( (y >= 0) && (y < content.length) && (x >= 0) && (x < content[y].length) ) { result = content[y][x]; } return result; } }
File 2 of 2: Main.java (NOT ideal code - super simple first pass)
package com.blogspot.fromjavatoscala.life.java; import java.util.ArrayList; import java.util.List; import java.util.Random; public class Main { public static void main(String[] args) { List<String> frames = new ArrayList<String>(); boolean[][] content = generateInitialContent(); String output = displayFrame(content); frames.add(output); printOutput(0, output); boolean finished = false; int i = 0; while (!finished) { content = FrameVersion1.next(content); output = displayFrame(content); finished = frames.contains(output); if (!finished) { frames.add(output); printOutput(i + 1, output); } ++i; } } private static void printOutput(int tick, String output) { System.out.println("Tick #" + tick); System.out.println(output); } private static boolean[][] generateInitialContent() { boolean [][] result = new boolean[25][25]; //results in 121 frames before repeating Random random = new Random(0); for (int y = 0, yCount = result.length; y < yCount; ++y) { for (int x = 0, xCount = result.length; x < xCount; ++x) { result[y][x] = random.nextBoolean(); } } return result; } private static String displayFrame(boolean[][] content) { String result = ""; for (int y = 0, yCount = content.length; y < yCount; ++y) { for (int x = 0, xCount = content.length; x < xCount; ++x) { result += (content[y][x] ? "#" : " "); } result += "\n"; } return result; } }
That's an interesting example problem to choose. I like it - Life has been one of my favorites for 20+ years - we had a "live simulation" of it at a Science Fiction Con one year :)
ReplyDeleteI think you picked a good one to show the advantages of a functional language, too, since it is heavily algorithmic.
BTW, when I read this using Google Reader (my preferred way to follow all the blogs) your colorization didn't show, but the formatting was better. It had no scroll bars, etc. Funny how hard it is to make the leap to the web sometimes, huh?