MokkosuでLifeGameを作ってみた

# はじめに

関数型言語 [Mokkosu](http://lambdataro.sakura.ne.jp/mokkosu/) で LifeGame を作ってみました。

![LifeGame01.png](https://qiita-image-store.s3.amazonaws.com/0/8988/f7f3d9b4-b2cc-3244-18ad-89d66cb6c90a.png)

# ソースコード
````fsharp:LifeGame.mok
#=============================================================================
#! @file	LifeGame.mok
#! @brief	Conway's Game of Life
#! @author	kielnow
#=============================================================================
#__define "CONSOLE_APPLICATION";

include "Print.mok";
include "List.mok";
include "Graphics.mok";

using System;
using System.Collections;

#-----------------------------------------------------------------------------
# AFX
#-----------------------------------------------------------------------------
let object_to_int (o : {Object}) = cast<Object,Int32>(o);
let object_to_bool (o : {Object}) = cast<Object,Boolean>(o);
let int_to_object (o : Int) = cast<Int32,Object>(o);
let bool_to_object (o : Bool) = cast<Boolean,Object>(o);

let is_nil = {
  [] -> true;
  _ -> false;
};

fun map f = {
  [] -> [];
  x::xs -> f x :: map f xs;
};

fun concat = {
  [] -> [];
  x::xs -> x ++ concat xs;
};

let concat_map f lis = concat (map f lis);

let __for_bind lis f = concat_map f lis;

#-----------------------------------------------------------------------------
# Global constants
#-----------------------------------------------------------------------------
let (screenWidth,screenHeight) = (800,600);
let fps = 30;
let fpsInv = 1000 / fps;
let title = "Mokkosu Life Game";
let mokkosu_brush = new_solid_brush 103 128 170;

let (cellWidth,cellHeight) = (10,10);
let (fieldWidth,fieldHeight) = (screenWidth/cellWidth,screenHeight/cellHeight);
let fieldSize = fieldWidth * fieldHeight;
let lifeNum = fieldSize * 4/10;

#-----------------------------------------------------------------------------
# Global states
#-----------------------------------------------------------------------------
let random = new Random();
let fieldAlive = new ArrayList(fieldWidth*fieldHeight);
let fieldDensity = new ArrayList(fieldWidth*fieldHeight);
let counter = ref 0;
let inverse = ref false;

#-----------------------------------------------------------------------------
# Miscellaneous functions
#-----------------------------------------------------------------------------
let pack x y = y * fieldWidth + x;

let getFieldAlive (index : Int) = object_to_bool fieldAlive.get_Item(index);
let setFieldAlive (index : Int) alive = fieldAlive.set_Item(index, bool_to_object alive);

let getFieldDensity (index : Int) = object_to_int fieldDensity.get_Item(index);
let setFieldDensity (index : Int) density = fieldDensity.set_Item(index, int_to_object density);

#-----------------------------------------------------------------------------
# Life game
#-----------------------------------------------------------------------------
fun generateLife = {
  0 -> ();
  n -> 
    let index = random.Next(fieldSize) in
    if getFieldAlive index -> generateLife n
    else do
      setFieldAlive index true;
      generateLife (n-1);
      end;
};

let calcDensity () =
  let stencil =
    [(fieldWidth-1,fieldHeight-1),(0,fieldHeight-1),(1,fieldHeight-1),
     (fieldWidth-1,0),(1,0),
     (fieldWidth-1,1),(0,1),(1,1)]
  in ignore (
    for y <- 0 .. (fieldHeight-1);
        x <- 0 .. (fieldWidth-1);
    in let pos = map {(dx,dy) -> ((x+dx)%fieldWidth, (y+dy)%fieldHeight)} stencil
    in let density = sum <| map {p -> if getFieldAlive (uncurry pack p) -> 1 else 0} pos
    in do 
      setFieldDensity (pack x y) density;
    end
  );

let stepLife () = ignore (
  for index <- 0 .. (fieldSize-1)
  in let alive = getFieldAlive index
  in let density = getFieldDensity index
  in do
    if (not alive) && density == 3 -> setFieldAlive index true
    else if alive && (density < 2 || 3 < density) -> setFieldAlive index false
    else ();
  end
);

#-----------------------------------------------------------------------------
# Scene: init
#-----------------------------------------------------------------------------
let tick_init () = do
  println "tick_init";
  counter := 0;
  fieldAlive.Clear();
  fieldDensity.Clear();
  println "clear";
  iter (\index -> 
    do
      fieldAlive.Insert((index : Int), bool_to_object false);
      fieldDensity.Insert((index : Int), int_to_object 0);
    end
  ) <| 0 .. (fieldSize-1);
  println "init";
  generateLife lifeNum;
  calcDensity ();
  switch "main";
  end;

#-----------------------------------------------------------------------------
# Scene: main
#-----------------------------------------------------------------------------
let tick_main () = do
  if !counter == 0 -> println "tick_main" else ();
  counter := 1 + !counter;
  stepLife ();
  calcDensity ();
  redraw ();
  end;

let draw_main gr =
  let (fg,bg) = if !inverse -> (white_brush,mokkosu_brush) else (mokkosu_brush,white_brush) in
  do fill_rectangle gr mokkosu_brush 0 0 screenWidth screenHeight;
     ignore (
       for y <- 0 .. (fieldHeight-1);
           x <- 0 .. (fieldWidth-1);
       in let index = pack x y
       in let alive = getFieldAlive index
       in let density = getFieldDensity index
       in let brush = if alive -> fg else bg
       in do
         fill_rectangle gr brush (x*cellWidth) (y*cellHeight) (cellWidth-1) (cellHeight-1);
       end
     );
  end;

#-----------------------------------------------------------------------------
# Main
#-----------------------------------------------------------------------------
let main () = do
  # setup window
  set_size screenWidth screenHeight;
  set_title title;
  set_speed fpsInv;
  
  # setup scene: init
  scene "init" {
    ~Tick -> tick_init ();
    _ -> ()
  };
  
  # setup scene: main
  scene "main" {
    ~Tick -> tick_main ();
    ~Draw(gr) -> draw_main gr;
    ~MouseUp(RightButton,_,_) -> switch "init";
    ~MouseUp(LeftButton,_,_) -> inverse := not !inverse;
    _ -> ()
  };
  
  # startup
  show_window "init";
  end;

#-----------------------------------------------------------------------------
# Entry point
#-----------------------------------------------------------------------------
do main ();
````

# 解説

## 1. 標準ライブラリ関数の置き換えています
最新の Mokkosu 1.3.2 では、標準ライブラリ関数の実装が末尾再帰版になっているようなのですが、~~コンパイラの最適化が不十分なようで細かいクロージャが大量に生成されて激重なので、~~LifeGame を快適に動かすには少々重たいので標準ライブラリ関数の実装を非末尾再帰版に置き換えています。もちろんスタックオーバーフローする恐れはありますが LifeGame が快適に動かなかったので仕方がありません。今後のバージョンアップに期待しましょう。
途中の ``__for_bind`` は for 式を使用した際に内部的に使用される関数です。いわゆるリストモナドの bind に相当するものです。

## 2. .NETのArrayListを使っています
LifeGame のセルの情報は、~~Mokkosu 標準のリストなどのデータ構造を使用すると激重なのは容易に想像がついたので、~~.NET の ArrayList に格納しています。
.NET のクラスを Mokkosu から使用するには結構頑張らないといけないです。今後のバージョンアップに期待しましょう。

# 感想
.NET のクラスを~~普通に~~使用することができて Mokkosu すごい！と思いました。