ようこそ。睡眠不足なプログラマのチラ裏です。

F#で shift/reset プログラミング。限定継続(風)モナドで四天王問題を解く。


発売されてすぐにPSVitaのP4Gをダウンロードしたのだが、どちらかというとエヴィディ・ヤングライフ・VB6♪な生活を送っていてなかなかプレイする暇がなくてつらい。今日はGoAzureのゆーすと見たり、この記事をこしらえていたりな休日を過ごしていましたが、皆さんいかがお過ごしでしょうか。


限定継続と shift/resetプログラミング

少し前に書籍「Scala実践プログラミング」のChapter10-2 継続のところ読んでいて、限定継続面白いなー shift/reset プログラミング面白いなーと思っていたので、shift/reset プログラミングについて検索してみました。すると、書籍「プログラミングの基礎」でも有名な、お茶の水女子大学の浅井健一氏の「shift/reset プログラミング入門」が出てきました。いきなり当たりを引いた!今日のおれはツイてるな!(テッテレー)と言う感じで、こちらとてもわかりやすくて素晴らしい資料です。関数型プログラミングや限定継続に興味のある方はぜひご一読を。




Scalaでは限定継続のサポートにより、以下のように shift/resetプログラミングをすることができます。

// result: 11
reset {
    shift { k: (Int=>Int) =>  k(10)
    } + 1
}

継続を限定しているreset部分について、shiftでその継続を取り出して k に束縛していて、 その k に 10 を適用することによって、1が加算されるので、結果は11になります。限定継続ってなんぞ?これの何が嬉しいのかわからないだって?まぁまぁそうおっしゃらずに。とりあえず「shift/reset プログラミング入門」を読みましょう。話はそれから。



ところで、Scala 2.8からサポートされたらしい限定継続ですが、F#にはいまのところそのような機能はないですし今後もサポートされる可能性は低そうです。でも、F#でちょっと真似してみたくなるよね。 shift/reset プログラミングちょっとあこがれちゃうよね。ということで、限定継続(風)モナドを作って F#でshift/reset プログラミングの雰囲気を感じてみましょう。



限定継続(風)モナド
ここで示すモナドはまぎれもなくモナドであるし、限定継続を意識した shift/reset なスタイルでプログラミングをすることができるようになるけど、いわゆるcall/cc的なことはしていませんので厳密には限定継続とは言えないので、限定継続(風)であるということに注意してください。F#でのモナド表現には、コンピューテーション式および、とてもCoolなライブラリFSharpxを利用しました。

namespace Monad.DCont

// 限定継続()モナド
[<AutoOpen>]
module DCont =
  open FSharpx

  type DCont<'a,'b,'c> = DCont of (('c -> 'a) -> 'b) 

  let dcontreturn x = fun k -> k x
  let shift f = fun k -> f (fun s -> dcontreturn <| k s) id

  type DContBuilder() =
    member this.Return(x) = dcontreturn x
    member this.ReturnFrom(m) = m
    member this.Bind(m, bind) =
      fun k -> m <| fun s -> bind s k 
    member this.Zero() = shift(fun _ -> id)
    member this.Combine(c1, c2) = this.Bind(c1, fun _ -> c2)
    member this.Delay(f) = f()
    member this.For(seq, f) = 
      Seq.fold
        (fun cc elem -> this.Combine(cc, f elem))
        (f <| Seq.head seq) <| Seq.skip 1 seq

  let reset = DContBuilder()
  let runDCont (f) = f id

  open Operators
  let inline returnM x = returnM reset x 
  let inline (>>=) m f = bindM reset m f
  let inline (=<<) f m = bindM reset m f
  let inline (<*>) f m = applyM reset reset f m
  let inline ap m f = f <*> m
  let inline map f m = liftM reset f m
  let inline (<!>) f m = map f m
  let inline lift2 f a b = returnM f <*> a <*> b
  let inline ( *>) x y = lift2 (fun _ z -> z) x y
  let inline ( <*) x y = lift2 (fun z _ -> z) x y
  let inline (>>.) m f = bindM reset m (fun _ -> f)
  let inline (>=>) f g = fun x -> f x >>= g
  let inline (<=<) x = flip (>=>) x

  let dcont f = fun x -> returnM <| f x 
  let shift' k = fun x -> 
    reset { let! a = k x
            return a}


モナド則の確認とか

namespace MonadicRetry.Test
open System

[<AutoOpen>]
module Tests = 
  open NUnit.Framework
  open FsUnit
  open Monad.DCont
      
  [<TestFixture>]
  type ``モナド関連確認`` () =
    let x = 1
    let m = reset { return 8 }
    let f x = reset { return 4 + x }
    let g x = reset { return 3 * x }

    let assertEqual (left, right) = 
      reset {let! a1 = left
             let! a2 = right
             let r = (a1 |> should equal (a2))
             printfn "%s" (sprintf "%d = %d , Result :%b" a1 a2 ((a1) = (a2)))
             return fun () -> 0} |> runDCont |> ignore

    let (==) left right = assertEqual (left, right)

    [<Test>] 
    // モナド則1: return x >>= f == f x
    member test.``01.モナド則1`` () =
      returnM x >>= f == f x 

    [<Test>] 
    // モナド則2: m >>= return == m
    member test.``02.モナド則2`` () =
      let m' = m >>= returnM
      m' == m

    [<Test>] 
    // モナド則3: (m >>= f) >>= g == m >>= (\x -> f x >>= g)
    member test.``03.モナド則3`` () =
      (m >>= f) >>= g == (m >>= (fun x -> f x >>= g))

    // Functor(関手)
    [<Test>] 
    //fmap id == id
    member test.``04.関手:functor1`` () =
      map id m == m

    [<Test>] 
    //fmap (f.g) == fmap f . fmap g
    member test.``05.関手:functor2`` () =
      let f x = x * 2 
      let g x = x + 2 
      m |> map (f >> g) == (m |> (map f >> map g))

    [<Test>] 
    // fmap :: (a -> b) -> f a -> f b
    // fmap f m == m >>= return . f
    member test.``06.関手:functor3`` () =
      let f x = x * 2 
      (map f m) == (m >>= (f >> returnM))

    // アプリカティブ: f <!> m1 <*> m2 == m1 >>= fun x -> m2 >>= fun y -> return f x y
    [<Test>] 
    member test.``07.アプリカティブ:applicative1`` () =
      let f x y = x * 2 + y * 2
      let m1 = reset { return 6 }
      let m2 = reset { return 9 }
      f <!> m1 <*> m2 == reset { let! a = m1
                                 let! b = m2
                                 return f a b }

    [<Test>] 
    member test.``08.アプリカティブ:applicative2`` () =
      let f x y z = x * 2 + y * 2 - z
      let m1 = reset { return 6 }
      let m2 = reset { return 9 }
      let m3 = reset { return 20 }
      f <!> m1 <*> m2 <*> m3 == reset { let! a = m1
                                        let! b = m2
                                        let! c = m3
                                        return f a b c}

    // Kleisli[<Test>] 
    member test.``09.クライスリ圏:kleisli composition1`` () =
      let x = 10
      let f x = 
          if x > 5
              then reset { return "hello" }
              else reset { return "world" }
      let g x =
          if x = "hello"
              then reset { return 777 }
              else reset { return 0 }
      (f x >>= g) == (f >=> g <| x)

  // nunit-gui-runner
  let main () = NUnit.Gui.AppEntry.Main([|System.Windows.Forms.Application.ExecutablePath|]) |> ignore
  main ()


ホンモノの限定継続モナドは、きっと腕の立つF#マスター達が実装してくれるんじゃないかな。期待しましよう。



限定継続(風)モナドを利用してみる

さっそく利用してみる。さきほどのScalaの限定継続の例を、限定継続(風)モナドをつかって、F#で書いてみます。

reset {let! a = shift(fun k -> k 10) 
       return a + 1 } 
|> runDCont |> printfn "%d"


意図通りに、11って出力される。コンピューテーション式でモナドを表現しているので、根本的には違うものの割とScalaと似たようなスタイルで記述できていい感じ。なんとなく雰囲気が醸し出せているね。雰囲気重視だよ!!!



限定継続(風)モナドで四天王問題


元ネタ
Scalaの限定継続で四天王問題を解いてみた - papamitra
http://d.hatena.ne.jp/papamitra/20100912/continuations


おお、あの四天王問題ですか。限定継続でAbmへの応用とかって面白いですね。



四天王問題

A「Dがやられたようだな…」B「ククク…奴は我ら四天王の中でも最弱…」C「私はBよりも弱い…」A「そして私は最強ではない…」B「四天王の中に私よりも弱いものが最低でも二人いる…」C「私はAよりも強い…」 ※以上の条件から四天王を強い順に並べよ(5点)


これを限定継続(風)モナドでF#で書くとこう

namespace TheBigFourProblem

module Program =
  open System
  open Monad.DCont
  open Monad.DCont.Amb

  let abcd () =
    reset {
      let numbers = [1;2;3;4]
      let! a = amb numbers
      let! b = amb numbers
      let! c = amb numbers
      let! d = amb numbers

      // 同じ強さではないことを仮定
      do! distinct [a;b;c;d] |> require

      // ADがやられたようだな…」B「ククク…奴は我ら四天王の中でも最弱…」
      do! d = 4 |> require

      // C「私はBよりも弱い…」
      do! b < c  |> require

      // A「そして私は最強ではない…」
      do! a = 1 |> not  |> require

      // B「四天王の中に私よりも弱いものが最低でも二人いる…」
      do! (b = 1 || b = 2) |> require

      // C「私はAよりも強い…」
      do! c < a  |> require

      // ※以上の条件から四天王ABCDを強い順に並べよ(5点)
      printfn "%s" <| sprintf "A:%d,B:%d,C:%d,D:%d" a b c d
    } 

  abcd ()
  |> runDCont
  |> ignore


実行結果

A:3,B:1,C:2,D:4


open Monad.DCont.Amb ってなんぞ? 限定継続(風)モナドでAmbを利用するために以下のようなmoduleを作って利用しています。

namespace Monad.DCont

module Amb =
  open System

  let rec amb list = 
    reset {
      if List.isEmpty  list then
        return! shift(fun _ -> returnM (List.empty))
      else
        return! shift(fun k -> k (List.head list) |> ignore
                               reset.Bind(amb (Seq.toList <| Seq.skip 1 (List.toSeq list)), k))
    } 

  let require p = reset { return! shift(fun k -> if (p) then (k ()) else shift(fun _ -> id)) }

  let distinct list = 
    let rec proc list = 
      match list with
      | x :: xs -> List.toArray xs |> fun a -> 
        if (Array.IndexOf(a,x)) < 0 && proc(xs) then 
          true 
        else false
      | _ -> true
    proc list 

元ネタ
Scalaの限定継続で四天王問題を解いてみた その2 - papamitra
http://d.hatena.ne.jp/papamitra/20100912/continuations2


もっと四天王問題!すこし難易度があがります。

A「Dがやられたようだな…」B「ククク奴は四天王でも最弱…」C「私はBよりも強い」A「私は最強ではないが最弱でもない」B「私はAより強いぞ」C「四天王NO.3は嘘つき」A「私とCとの実力差は離れている」 問:四天王を強い順に並べよ。但し正直者は真実、嘘つきは嘘しか言わないものとする。(100ポイント)

  let abcd2 () =
    reset {
      let numbers = [1;2;3;4]
      let! a = amb numbers
      let! b = amb numbers
      let! c = amb numbers
      let! d = amb numbers

      let! at = amb [true;false]
      let! bt = amb [true;false]
      let! ct = amb [true;false]
      let! dt = amb [true;false]

      // 同じ強さではないことを仮定
      do! distinct [a;b;c;d] |> require

      // // ADがやられたようだな…」B「ククク…奴は我ら四天王の中でも最弱…」
      do! ((bt && d = 4) || (bt |> not && d = 4 |> not)) |> require

      // C「私はBよりも強い」
      do! ((ct && c < b) || (ct |> not &&  b < c))  |> require

      // A「私は最強ではないが最弱でもない」
      do! ((at &&  (a = 1 |> not && a = 4 |> not)) || (at |> not && (a = 1 || a = 4))) |> require

      // B「私はAより強いぞ」
      do! ((bt && b < a) || (bt |> not && a < b)) |> require

      // C「四天王NO.3は嘘つき」
      do! (c = 3 |> not) |> require
      do! ((ct && ((at |> not && a=3) || (bt |> not && b=3) || (dt |> not && d=3))) || (ct |> not && ((at && a=3) || (bt && b=3) || (dt && d=3))))  |> require

      // A「私とCとの実力差は離れている」
      // 順位が隣合っていないと解釈する.
      do! ((at && (abs(a-c) = 1 |> not)) || (at |> not && (abs(a-c) = 1))) |> require

      // ※以上の条件から四天王ABCDを強い順に並べよ
      printfn "%s" <| sprintf "A:%A,B:%A,C:%A,D:%A" (a,at) (b,bt) (c,ct) (d,dt)
    } 

  abcd2 ()
  |> runDCont
  |> ignore

実行結果

A:(1, false),B:(4, false),C:(2, true),D:(3, false)


ちゃんと、四天王の強さの順番と、C以外は嘘つきであるという結果が導きだせましたね!




おまけ:ダイハード3のやつ
なんだか、非決定計算の問題を解くのが面白くなってきちゃったので、せっかくなので限定継続(風)モナドでどんどん解いていきます。


まずは、ダイハード3で出題された3ガロンと5ガロンの容器で4ガロンを量るってやつ。いわゆる、水差し問題とゆーやつですね。
これは、わざわざプログラミングで解くまでもないなぞなぞレベルの問いですが、書いてみます。

namespace PitcherProblem

module Program =
  open System
  open Monad.DCont
  open Monad.DCont.Amb

  // ダイハード3の 3ガロンと5ガロンの水差し問題
  let maxa = 5
  let maxb = 3

  let geta state = fst state
  let getb state = snd state

  let private action =
    [ // Aを満杯にする
      fun state -> maxa, getb state;
      // Aを空にする
      fun state -> 0, getb state;
      // AからBへ移す
      fun state -> 
        let a = geta state
        let b = getb state
        let w = maxb - b
        if a <= w then
          // 全部移しきった場合
          0, a + b
        else
          // Aに水が残る場合
          a - w, b + w;

      // Bを満杯にする
      fun state -> geta state, maxb;
      // Bを空にする
      fun state -> geta state, 0;
      // BからAへ移す
      fun state ->
        let a = geta state
        let b = getb state
        let w = maxa - a
        if b <= w then
          // 全部移しきった場合
          a + b, 0
        else
          // Aに水が残る場合
          a + w, b - w; ]
 
  let private solve answer = 
    let rec solve' n answer move =
      let x = (List.length move) - 1
      let prev = move.Item x 
      reset {
        if n = 0 && prev |> fst = answer || prev |> snd = answer then
            return! shift(fun k -> k move)
        else
            let! act = amb action
            let newstate = act prev
            let contains s list = List.exists(fun x -> x = s) list
            if prev = newstate || contains newstate move then
              return! shift(fun _ -> returnM move)
            else
              return! solve' (n-1) answer (move@[newstate]) }

    let m = List.length action
    solve' m answer [(0,0)]

  let pitcherProblem answer =
    let result = ref []
    reset {
      let! xs = solve answer
      result := !result@[xs]
      return xs
    } |> runDCont |> ignore
    !result

  pitcherProblem 4
  |> fun x -> x |> Seq.iter (printfn "%A")
              printfn "%s" <| sprintf "%d通り" x.Length


おまけ:地図の塗り分け

いかなる地図も、隣接する領域が異なる色になるように塗るには4色あれば十分DAZEという、いわゆる四色定理とゆーやつ。
実際に塗り分けしてみよう。

namespace ColorMapProblem

module Program =
  open System
  open Monad.DCont
  open Monad.DCont.Amb

  let colorMapProblem () =
    reset {
      let colors = ["red";"yellow";"green";"blue"]
      let! p = amb colors // Portugal:ポルトガル
      let! e = amb colors // Spain:スペイン
      let! f = amb colors // France:フランス
      let! b = amb colors // Belgium:ベルギー
      let! h = amb colors // Holland:オランダ
      let! g = amb colors // Germany:ドイツ
      let! l = amb colors // Luxemb:ルクセンブルク
      let! i = amb colors // Italy:イタリア
      let! s = amb colors // Switz:スイス
      let! a = amb colors // Austria:オーストリア

      let notcontains s list = List.exists(fun x -> x = s) list |> not
      // ポルトガルは、[スペイン]の色と違うよ
      do! notcontains p [e] |> require
      // スペインは、[フランス;ポルトガル]の色と違うよ
      do! notcontains e [f;p] |> require
      // 以下コメント略
      do! notcontains f [e;i;s;b;g;l] |> require
      do! notcontains b [f;h;l;g] |> require
      do! notcontains h [b;g] |> require
      do! notcontains g [f;a;s;h;b;l] |> require
      do! notcontains l [f;b;g] |> require
      do! notcontains i [f;a;s] |> require
      do! notcontains s [f;i;a;g] |> require
      do! notcontains a [i;s;g] |> require

      // 4色で塗り分ける組み合わせ
      printfn "%s" <| sprintf "Portugal:%s,Spain:%s,France:%s,Belgium:%s,Holland:%s,Germany:%s,Luxemb:%s,Italy:%s,Switz:%s,Austria:%s" p e f b h g l i s a
    } 

  colorMapProblem ()
  |> runDCont
  |> ignore


想像以上に塗り分けれるね!



おまけ:狼とヤギとキャベツ

いわゆる川渡り問題。

オオカミとヤギを連れキャベツを持った農夫が川岸にいる。川にはボートがあるが農夫の他には動物一頭かキャベツ一玉しか乗せられない。農夫がいなければオオカミはヤギを襲うし、ヤギはキャベツを食べてしまう。すべてを無事に対岸に渡すにはどうしたらよいか?

namespace FarmerGoatWolfCabbage

module Program =
  open System
  open Monad.DCont
  open Monad.DCont.Amb

  type Position = 
    | Left of Kind 
    | Right of Kind
  and Kind =
    | Farmer
    | Goat
    | Wolf 
    | Cabbage
  
  let swap = function | Left x -> Right(x) | Right x -> Left(x)
  let init = Left(Farmer), Left(Goat), Left(Wolf), Left(Cabbage)
  let ans = Right(Farmer), Right(Goat), Right(Wolf), Right(Cabbage)
  
  let (==) x y =
    match x,y with
    | Left _, Left _   -> true
    | Right _, Right _ -> true
    | _,_ -> false

  let private action =
    [ 
      // 農夫のみ移動
      fun state -> let f, g, w, c = state
                   swap f, g, w, c;
      // 農夫とヤギ
      fun state -> let f, g, w, c = state
                   swap f, swap g, w, c;
      // 農夫と狼
      fun state -> let f, g, w, c = state
                   swap f, g, swap w, c;
      // 農夫とキャベツ
      fun state -> let f, g, w, c = state
                   swap f, g, w, swap c;
      ]

  let safe state =
    let safegote = 
      let f,g,w,c = state
      if f == g then true
      else g == w |> not
    let safecabbage = 
      let f,g,w,c = state
      if f == c then true
      else g == c |> not
    safegote && safecabbage

  let private solve () = 
    let rec solve' move =
      let x = (List.length move) - 1
      let prev = move.Item x 
      reset {
        if prev = ans then
            return! shift(fun k -> k move)
        else
            let! act = amb action
            let newstate = act prev
            let contains s list = List.exists(fun x -> x = s) list
            if prev = newstate then  
              return! shift(fun _ -> returnM move)
            elif contains newstate move then
              return! shift(fun _ -> returnM move)
            elif  (safe newstate |> not) then
              return! shift(fun _ -> returnM move)
            else
              return! solve' (move@[newstate]) }

    let m = List.length action
    solve' [init]

  let farmerGoatWolfCabbageProblem () =
    let result = ref []
    reset {
      let! a = solve ()
      result := a
      return a
    } |> runDCont |> ignore
    !result

  farmerGoatWolfCabbageProblem ()
  |> fun x -> x |> Seq.iter(fun x ->
    let f,g,w,c = x
    let result = [f;g;w;c]
    printf "["
    result |> Seq.filter (fun x -> x |> function | Left _ -> true | _ -> false) 
           |> Seq.map (fun x -> x |> function | Left x -> x | Right x -> x)  
           |> Seq.iter (printf "%A;" )
    printf "] : "

    printf "["
    result |> Seq.filter (fun x -> x |> function | Right _ -> true | _ -> false) 
           |> Seq.map (fun x -> x |> function | Left x -> x | Right x -> x)  
           |> Seq.iter (printf "%A;")
    printfn "]"
    )


実行結果

[Farmer;Goat;Wolf;Cabbage;] : []
[Wolf;Cabbage;] : [Farmer;Goat;]
[Farmer;Wolf;Cabbage;] : [Goat;]
[Wolf;] : [Farmer;Goat;Cabbage;]
[Farmer;Goat;Wolf;] : [Cabbage;]
[Goat;] : [Farmer;Wolf;Cabbage;]
[Farmer;Goat;] : [Wolf;Cabbage;]
[] : [Farmer;Goat;Wolf;Cabbage;]


わーい、無事に川を渡れたよ!



おまけ:順列と組み合わせ

今度はちょっと趣向を変えて。書いているうちに、だんだんshift/reset スタイルなプログラミングに慣れてきたかもな気がするよ!

namespace PermutationAndCombination

module Program =
  open System
  open Monad.DCont
  open Monad.DCont.Amb

  let rec private selections n m lst result =
    let contains s list = List.exists(fun x -> x = s) list
    reset {
      if m = 0 || lst = [] then
        return! shift(fun k -> k result)
      else
        return! reset {
          let! x = amb [0..n-1]
          if contains (lst.Item x) result then
            return! shift(fun _ -> returnM result)
          else
            return! selections n (m-1) lst (result@[lst.Item x])}
    }  

  // 順列
  let permutations m lst =
    let n = List.length lst
    let result = ref []
    reset {
      let! xs = selections n m lst []
      xs.Length = m |> function 
        | false -> ()
        | true -> result := !result@[xs]
      return xs
    } |> runDCont |> ignore
    !result

  permutations 4 ['A'..'F']
  |> fun x -> x |> Seq.iter (printfn "%A")
              printfn "%s" <| sprintf "%d通り" x.Length


  // 組み合わせ
  let combinations m (lst: 'a list) =
    let n = List.length lst
    let contains r sourece = 
      sourece |> Seq.map  (fun x -> (set x, set r) ||> Set.difference = Set.empty)
              |> Seq.exists id

    let result = ref []
    reset {
      let! xs = selections n m lst []
      contains xs !result |> function
      | true   -> ()
      | false  -> result := !result@[xs]
      return xs
    } |> runDCont |> ignore
    !result

  combinations 4 ['A'..'F']
  |> fun x -> x |> Seq.iter (printfn "%A")
              printfn "%s" <| sprintf "%d通り" x.Length


なんか、おまけの方が多くなっちゃいましたね。てへぺろ☆(・ω<)
Amb以外の応用例も書くつもりでしたが、積みゲー消化したいのでそれはまた別の機会に。たぶん。

F# Implementation of BackPropagation Neural Network for Pattern Recognition(LifeGame)


この記事は、F# Advent Calendar 2011の21日目です。




きっかけは、11月19日に札幌で行われた第64回CLR/H勉強会で、愛甲健二さん(@07c00)がお話してくれた「コンピューターに萌えを教えてみたよ」というセッションです。「アダルトサイトの検知」のメカニズムだったり、愛甲さん自身の"萌えの嗜好"をコンピューターに学習させてみるという少しアレゲなテーマでのお話しでしたが、内容はとても真面目で面白かった。見慣れない数式など、その全てを理解することはできませんでしたが、ニューラルネットワークの雰囲気や概要がわかりました。オライリーの「集合知プログラミング」でニューラルネットワークについて少し読んだことがあったり、何となく見聞きしたことはありましたが、基本的な考え方を知ったのはそのときがはじめてです。とても面白くもっと知りたいと思ったので、勉強会の後にモクモクとニューラルネットワークに関する情報を集めて自分なりに勉強してみました。"脳を模してモデル化したアルゴリズムによって、コンピュータに学習能力をもたせる。" なんだか面白かっこいい!じゃないですか。いろいろと調べているうちに、これなら自分にも実装できそう!と思ったので、みんな大好きF#でやってみました。F#の記事というよりも、むしろニューラルネットワーク成分多い目だが、


「大丈夫だ、ゆるふわなので問題ない。」



ニューラルネットワークとは

情報分野におけるニューラルネットワークとは、われわれ人間の脳の神経回路の仕組みを模してモデル化したもので、コンピュータに学習能力を持たせることで、様々な問題を解決しようとするアプローチのひとつで、人工知能の一分野で機械学習というジャンルに属します。


もともとニューラルネットワークという研究分野は、人間が自然と行っているパターン認識や経験則を導き出したりする仕組みをモデル化して、ロボットが経験から学習していくことで、正しい反応や行動を獲得していく仕組みを実現することを目的とした側面が強かったようですが、次第に工学寄りにシフトしてきて、「データの中で明らかなものから、明らかではないものを予測する(ことをコンピュータにやらせるための)」技術や理論を指すことがほとんどになってきたようです。近年の自然言語処理や画像のパターン認識、データマイニング、あるいは信用リスク格付け予測など、ビジネス用途での応用分野における成功を要因に、普及と発展が進んでいて現在も広くその研究や応用が進められている。


教師あり学習というアプローチ

機械学習の扱う問題には、大きく分けて教師あり学習 (supervised learning) と、教師なし学習 (unsupervised learning) の2つがある。 単純にその2つに分類することができない、複合的な問題や独自に発展した特殊問題もあるようですが、基本的には、この2つに分類することができる。愛甲さんがお話してくれた、アダルトサイトの検知だったり、「コンピューターに萌えを教えてみたよ」は、ちょうど教師あり学習にあたります。


教師あり学習では、入力データ(条件として明らかとなっている情報)が与えられたとき、これに対する出力(答えが明らかではない情報)を正しく予測することが目的です。 もちろん、ただ入力を入れられただけでは、コンピューターは答えとして何を出力したらよいのかわかりません。そこで、訓練データ(あるいは教師データ)と呼ばれる、入出力のペアとしたデータを、あらかじめコンピューター複数与えます。「コレの入力があったら、コレを出力しなさい」というパターンをいくつか与えて機械に学習させます。新しい入力データが来たときに、それに対する正しい出力をするような機械(関数)を作るのが目的です。複雑で広い領域の問題では、すべてのパターンを機械に学習させることは不可能で、当然、あらかじめ学習に用いる訓練データの中には現れない入力データが与えられる場合もあります。そのようなデータに対応するために、与えられた訓練データを一般化して、未知のデータに対処して予測を出力する能力(汎化能力)がなるべく高くなるような、学習アルゴリズムを設計することが、教師あり学習の主要なテーマとなります。ニューラルネットワークは、汎化能力の高い教師あり学習のアプローチのひとつです。


F#でニューラルネットワーク

F#でバックプロパゲーションアルゴリズムを用いた3層パーセプトロンを実装しました。時間がなくて整理しきれなかった部分があり心残りな面もありますが、以下、NNモジュールです。参考になればと思い、普段は書かないような説明的なコメントも多めに書いてみました。


F#

namespace NN
open System

[<AutoOpen>]
module NN = 
  /// 訓練データパターン
  type Pattern = { Inputs : double list;  (* 入力 *)
                   TeachingSignal: double list (* 教師信号 *) }

  // 層をつくる
  let createLayer size build =
    let rec create size acc = 
      if size <= 0 then acc
      else create ((-) size 1) (acc@[build ()])
    create size []

  /// シグモイド関数
  /// 関数のある点での勾配を求めて誤差Eが少なくなる方向へ結合重みWを変化させていきます。
  let sigmoid input bias = 
    /// α(gain)1.0とするとき標準シグモイド関数と言う
    let gain : double = 5.0
    1.0 / (1.0 + Math.Exp(-gain * (input + bias))) 

  /// ニューロン
  type Neuron = 
    { mutable bias : double   // バイアス
      mutable error : double  // E
      mutable input : double  // 入力
      mutable output : double // 出力
      learnRate : double      // 学習レート
      weights : Weight list   // 重み
      } 

    /// 出力
    member this.Output 
      with get () = if (this.output <> Core.double.MinValue) then 
                      this.output
                    else
                      // 判別問題を学習させる場合は階段関数やシグモイド関数を用いる。回帰問題を学習させる場合は線形関数を用いる。
                      // 今回はシグモイドで 
                      sigmoid this.input this.bias
      and set (v) = this.output <- v

  // 重み
  and Weight = { In: Neuron; mutable Value:double }
  // 層
  and Layer = Neuron list

  /// 活性化
  let activate neuron = 
    neuron.input <- 0.0
    for w in neuron.weights do
      neuron.input <- neuron.input + w.Value * w.In.Output

  /// エラーフィードバック
  let errorFeedback (neuron:Neuron) (input:Neuron) = 
    neuron.Output * (1.0 - neuron.Output) |> fun derivative ->
    // より大きな重みで接続された前段のニューロンに対して、局所誤差の責任があると判定する。
    let w = neuron.weights |> List.find (fun t -> t.In = input)
    neuron.error * derivative * w.Value

  /// 各ニューロンの重みを局所誤差が小さくなるよう調整する。
  let adjustWeights (neuron:Neuron) (value:double) =
    neuron.Output * (1.0 - neuron.Output) |> fun derivative ->
    neuron.error <- value
    for i in [0..neuron.weights.Length-1] do
      // 出力と教師信号が異なれば、出力値を少しだけ教師信号寄りに重みを修正する
      neuron.weights.[i].Value <- neuron.weights.[i].Value + (neuron.error * neuron.learnRate * derivative * neuron.weights.[i].In.Output)
    // バイアスの補正
    neuron.bias <- neuron.bias + neuron.error * neuron.learnRate * derivative

  /// 素のニューロンを生成
  let createNewron () =
    { bias = 0.0
      error = 0.0
      input = 0.0
      output = Core.double.MinValue
      learnRate = 0.5
      weights = [] }

  /// 入力についてランダムな重みを持つニューロンを生成
  let createNewron' inputs (rnd:Random) =
    let createWeights () = 
      inputs |> List.map (fun input -> { In = input; Value = rnd.NextDouble() * 2.0 - 1.0 })
             |> List.fold (fun a b -> a@[b]) []
    { bias = 0.0
      error = 0.0
      input = 0.0
      output = Core.double.MinValue
      learnRate = 0.5
      weights = createWeights () }

  /// ネットワーク
  type Network = 
    { InputSize : int 
      MiddleSize : int 
      OutputSize : int 
      RestartAfter : int
      TryCount : int
      Inputs : Layer
      Middle : Layer
      Outputs : Layer
      Patterns : Pattern list }
  
  /// 入力層、中間層、出力層のニューロンを生成
  let createNeuron inputSize middleSize outputSize = 
    let rnd = new Random()
    let inputs = createLayer inputSize (fun () -> createNewron ())
    let middle = createLayer middleSize (fun () -> createNewron' inputs rnd)
    let outputs = createLayer outputSize (fun () -> createNewron' middle rnd)
    inputs, middle, outputs

  /// ニューラルネットワークの各ニューロンを活性化
  let networkActivate (network:Network) (pattern:Pattern) = 
      for i in [0..pattern.Inputs.Length - 1] do
        network.Inputs.[i].Output <- pattern.Inputs.[i]
      for neuron in network.Middle do
        activate neuron
      for output in network.Outputs do 
        activate output
      network.Outputs |> List.map (fun output -> output.Output)

  /// 初期化
  let initializeNetwork (network:Network) = 
    let inputs,middle,outputs = createNeuron network.InputSize network.MiddleSize network.OutputSize
    { network with Inputs = inputs; Middle = middle; Outputs = outputs; TryCount = 0 }

  /// 訓練データをNetworkに読み込む
  let loadPatterns (network:Network) (trainingData :(double list * double list) list) = 
    let rec create n acc = 
      if n <= 0 then acc
      else 
        let inputs,teachingSignal = trainingData.[n]
        create ((-) n 1) (acc@[{Inputs=inputs; TeachingSignal=teachingSignal}])
    { network with Patterns = create (trainingData.Length-1) [] }

  /// 訓練
  let training (network:Network) =
    /// 重み調整:バックプロパゲーション
    let adjustWeights (delta:double) = 
      // 個々のニューロンの期待される出力値と倍率(scaling factor)、要求された出力と実際の出力の差を計算する。これを局所誤差と言う。
      for output in network.Outputs do
        adjustWeights output delta
        for neuron in network.Middle do
          // そのように判定された前段のニューロンのさらに前段の中間層における隠れニューロン群について同様の処理を行う。
          adjustWeights neuron (errorFeedback output neuron)

    let mutable error = 0.0
    for pattern in network.Patterns do
      // ネットワークの出力とそのサンプルの最適解を比較する。各出力ニューロンについて誤差を計算する。
      for i in [0..pattern.TeachingSignal.Length - 1] do
        let output = (networkActivate network pattern).[i]
        let delta = pattern.TeachingSignal.[i] - output
        adjustWeights delta
        // 二乗誤差でEを求める
        error <- error + Math.Pow(delta, 2.0)
    { network with TryCount = network.TryCount + 1}, error

  /// 三層ネットワークを生成
  let createNetwork (inputs:Layer) (middle:Layer) (outputs:Layer) restartAfter = 
    { InputSize = inputs.Length 
      MiddleSize = middle.Length 
      OutputSize = outputs.Length 
      TryCount = 0
      RestartAfter = restartAfter
      Inputs = inputs
      Middle = middle
      Outputs = outputs
      Patterns = [] }


線形分離問題「OR」および「AND」、非線形分離問題 XORを解く
以下、NNモジュールを使って各問題を解く


F#

open System
open NN
open ListExModule

[<STAThread>]

// 三層分のニューロンを生成
let inputs,middle,outputs = createNeuron 2 3 1 
// ニューラルネットワークを構築
let mutable (network:Network,error:float) = createNetwork inputs middle outputs 500 , 1.0

let rec flat = function
  | [] -> []
  | x::_ when x = [] -> []
  | x::xs -> x @ flat xs

let rec insert v i lst =
    match i, lst with
    | 0, xs -> v::xs
    | i, x::xs -> x::insert v (i - 1) xs
    | i, [] -> failwith "境界外デス!"

let condition = [1..8]
let createPattern target ts (source: int list) =
  let inputs = 
    condition |> List.map (fun i -> if source |> List.exists (fun x -> x = i) then 1.0 else 0.0) 
              |> insert (if target = 1 then 1.0 else 0.0) 4
  inputs,[ts]


// AND問題 (線形分離可能)
let andProblem = [
                  [0.0; 0.0;], [0.0];
                  [0.0; 1.0;], [0.0];
                  [1.0; 0.0;], [0.0];
                  [1.0; 1.0;], [1.0];
                  ]

// OR問題 (線形分離可能)
let orProblem = [
                  [0.0; 0.0;], [0.0];
                  [0.0; 1.0;], [1.0];
                  [1.0; 0.0;], [1.0];
                  [1.0; 1.0;], [1.0];
                  ]


// XOR問題 (線形分離不可能)
let xorProblem = [
                  [0.0; 0.0;], [0.0];
                  [0.0; 1.0;], [1.0];
                  [1.0; 0.0;], [1.0];
                  [1.0; 1.0;], [0.0];
                  ]

// 訓練データをロード
network <- loadPatterns network xorProblem // ここではXORを解く

let main () = 

  /// 実行
  let run (network:Network) =
    while true do
      try
        Console.Write("Input x, y: ")
        let values = Console.ReadLine()
        let line = values.Split(',')
        let pattern = [0..network.InputSize-1] |> List.map (fun i -> Core.double.Parse(line.[i]))
        let inputs = List.init(network.InputSize) (fun i-> pattern.[i])
  
        for output in networkActivate network { Inputs=inputs; TeachingSignal = []} do
          printfn "%d(%f)" <| Convert.ToInt32(output) <| output
      with 
      | e -> Console.WriteLine(e.Message)


  // ニューラルネットワークを訓練する
  while error > 0.1 do
    let x,y = training network
    network <- x; error <- y
    printfn "Try %d\tError %f" x.TryCount y
    if network.TryCount > network.RestartAfter then
      network <- initializeNetwork network

  // 実行
  run network

main () 
Console.ReadKey () |> ignore 

非線形分離問題も問題なく解けますな。



パターン認識でライフゲーム
バックプロパゲーションアルゴリズムで3層パーセプトロンによって構築したニューラルネットでXOR判定をすることができた。ここで終わってもよかったのですが、せっかくなので欲張って、もう少しだけ複雑な非線形問題のパターン認識もやらせてみました。


第64回CLR/H勉強会の、@mentaroさんのセッションの最終デモで「ライフゲーム」が取り上げられていました。勉強会後に、「そういや、ライフゲームのセル生死判定は、判定対象セルとその周囲8つのセルをパターンとして捉えることがきて、セルの生死結果を教師データとするパターンをつくって、多数の訓練データで学習させることで、ニューラルネットワークライフゲームの生死判定をさせることができるんじゃね?」と思いました。それを実践してみようという。練習にはちょうど良いですね。判定対象セルと周囲の8つのセルを合わせた9つのセルを入力とし、生死の結果を教師データとする訓練データを作成して、ニューラルネットに食わせてシバけばおーけー!




以下、NNモジュールを使って、
F#+XNAで、ニューラルネットのパターン認識でライフゲームなコード


F#

namespace LG

open System
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open Microsoft.Xna.Framework.Input
open Microsoft.Xna.Framework.Content
open NN
open ListExModule

[<AutoOpen>]
module Assist =
  // リスト平坦化
  let rec flat = function
    | [] -> []
    | x::_ when x = [] -> []
    | x::xs -> x @ flat xs

  // リストへの挿入
  let rec insert v i lst =
      match i, lst with
      | 0, xs -> v::xs
      | i, x::xs -> x::insert v (i - 1) xs
      | i, [] -> failwith "境界外デス!"

  let condition = [1..8]

  // パターン生成
  let createPattern target ts (source: int list) =
    let inputs = 
      condition |> List.map (fun i -> if source |> List.exists (fun x -> x = i) then 1.0 else 0.0) 
                |> insert (if target = 1 then 1.0 else 0.0) 4
    inputs,[ts]

  // ライフゲームの教師データ生成
  let lifeGameTrainingData = 
    let pattern = [0..8] |> List.map (fun x -> combinations x condition)
    let survive = List.map (fun x -> x |> createPattern 1 1.0) // 生存
    let keep = List.map (fun x -> x |> createPattern 0 0.0)    // 維持
    let birth = List.map (fun x -> x |> createPattern 0 1.0)   // 誕生
    let die = List.map (fun x -> x |> createPattern 1 0.0)     // 過疎or過密
    pattern |> List.mapi (fun i x -> i |> function
                            | 2 -> survive x @ keep x
                            | 3 -> survive x @ birth x
                            | _ -> die x @ keep x) 
                            |> flat

  /// 初期ボード:グライダー銃
  let getGliderguns () =
    [|[|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;1;0;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;1;1;0;0;0;0;0;0;1;1;0;0;0;0;0;0;0;0;0;0;0;0;1;1;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;1;0;0;0;1;0;0;0;0;1;1;0;0;0;0;0;0;0;0;0;0;0;0;1;1;0;0;0;0;0;0|];
      [|0;0;1;1;0;0;0;0;0;0;0;0;1;0;0;0;0;0;1;0;0;0;1;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;1;1;0;0;0;0;0;0;0;0;1;0;0;0;1;0;1;1;0;0;0;0;1;0;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;1;0;0;0;0;0;1;0;0;0;0;0;0;0;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;1;0;0;0;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;1;1;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|];
      [|0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0|]|]

  /// 同じ長さを持つジャグ配列を二次元配列へ変換
  let convert (source:int [][]) = 
    (source.[0].GetLength(0),Array.length source) ||> fun row col -> Array2D.create row col 0 |> fun array ->
    seq { for i in 0..row - 1 do
          for j in 0..col - 1 do yield i,j } 
    |> Seq.iter (fun (i,j) -> array.[i,j] <- source.[j].[i])
    array

/// ライフゲーム
type LifeGame () as this = 
  inherit Game()
  // ゲームタイトル, GraphicsDeviceManager, SpriteBatch
  let gametitle, gmanager, spriteBatch = "LifeGame", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice)

  // 三層パーセプトロンの各ニューロンを生成 -> 入力:9 , 隠れ:17 , 出力:1
  let inputs,middle,outputs = createNeuron 9 17 1 
  // ニューラルネットワークを構築, error状態を取得
  let mutable (network:Network,error:float) = createNetwork inputs middle outputs 500 , 1.0
  // SpriteFont
  let font = lazy this.Content.Load<SpriteFont>(@"Content\font\SpriteFont1")
  // セルのテクスチャ
  let textureCell = lazy this.Content.Load<Texture2D>(@"Content\hagure")
  // セルエフェクト用マスクテクスチャ
  let normalmapTextureCell = lazy this.Content.Load<Texture2D>(@"Content\hagure_alpha")
  // HLSLエフェクト
  let normalmapEffect = lazy this.Content.Load<Effect>(@"Content\normalmap")

  // セルとセルの間の間隔
  let borderWidth, borderHeight = 0, 0
  // セル描画の開始位置
  let boardStartX, boardStartY = 0, 0
  // セルのサイズ(テクスチャのサイズによりけり)
  let cellWidth, cellHeight = 18, 17
  // ライフゲームの状態を表すボード
  let board = getGliderguns() |> convert
  // ボードのサイズ
  let width, height = Array2D.length1 board , Array2D.length2 board
  // ライフゲームの状態の更新制御
  let mutable runFlg = true
  let mutable nowRunFlg = false
  let mutable previousRunFlg = false
  
  // ライフゲームの世代交代インターバル
  let mutable interval = 10.0
  // マウスボタンのリリース状態
  let mutable mouseButtonReleased = false
  // 訓練が終了したか否か
  let mutable trainingEnd = false
  // マウスクリック位置の取得
  let getPos x y = new Vector2(float32(boardStartX + x * cellWidth + x * borderWidth), float32(boardStartY + y * cellHeight + y * borderHeight))

  // セル描画 サークル動作
  let moveInCircle (gameTime:GameTime) (speed:float) =
    let time = gameTime.TotalGameTime.TotalSeconds * speed
    let x = Math.Sin(time) |> float32
    let y = Math.Cos(time) |> float32
    new Vector2(x, y)

  // キー操作
  let operateKeys () = 
    let mouseState = Mouse.GetState()
    let keyboardState = Keyboard.GetState()
    if mouseState.LeftButton = ButtonState.Pressed && mouseButtonReleased && this.IsActive then
      // マウスボタン押下中
      mouseButtonReleased <- false
      let mouseStateX, mouseStateY = mouseState.X |> float32, mouseState.Y |> float32
      let mousePos = new Vector2(mouseStateX, mouseStateY )
      for x in [0..width-1] do
      for y in [0..height-1] do
        let pos = getPos x y
        if pos.X < mousePos.X && pos.X + float32(cellWidth) > mousePos.X && pos.Y < mousePos.Y && pos.Y + float32(cellHeight) > mousePos.Y then
          // マウスでクリックされたところのセルの生死状態のトグル
          board.[x, y] <- if board.[x, y] = 0 then 1 else 0
    else
      if mouseState.LeftButton <> ButtonState.Pressed then
        // マウスボタンをリリース
        mouseButtonReleased <- true
  
    // Pキーによる、PAUSE ON/OFF
    previousRunFlg <- nowRunFlg
    nowRunFlg <- keyboardState.IsKeyDown(Keys.P)
    if nowRunFlg && not previousRunFlg then
      runFlg <- not runFlg

  // ライフゲーム状態の更新
  let updateState = 
    let updateBoard () = 
      let tmp = Array2D.create width height 0
      for x in [0..width-1] do
        for y in [0..height-1] do
          let inputs = 
            // x7:左上, x8;上, x9:右上, x4:左, x5:評価対象のセル, x6:右, x1:左下, x2:下, x3:右下
            let x7 = if x-1 >= 0 && y-1 >= 0 && board.[x-1, y-1] = 1 then 1.0 else 0.0 
            let x8 = if y-1 >= 0 && board.[x, y-1] = 1 then 1.0 else 0.0 
            let x9 = if x+1 < width && y-1 >= 0 && board.[x+1, y-1] = 1 then 1.0 else 0.0 
            let x4 =  if x-1 >= 0 && board.[x-1, y] = 1 then 1.0 else 0.0 
            let x5 = board.[x, y] |> float
            let x6 = if x+1 < width && board.[x+1, y] = 1 then 1.0 else 0.0 
            let x1 = if x-1 > 0 && y+1 < height && board.[x-1, y+1] = 1 then 1.0 else 0.0 
            let x2 = if y+1 < height && board.[x, y+1] = 1 then 1.0 else 0.0
            let x3 = if x+1 < width && y+1 < height && board.[x+1, y+1] = 1 then 1.0 else 0.0 
            // ライフゲームのパターン
            [x7;x8;x9;
             x4;x5;x6;
             x1;x2;x3]

          // ニューラルネットワークで判定
          let outputs = networkActivate network { Inputs=inputs; TeachingSignal = []}
          // パターンに対する出力を取得
          let output = Convert.ToInt32(outputs.[0])
          tmp.[x, y] <- output
      
      // ボードに状態を反映
      for x in [0..width-1] do
        for y in [0..height-1] do
          board.[x, y] <- tmp.[x, y]

    let settim : double ref = ref 0.0
    (fun (gameTime:GameTime) -> 
      if runFlg then
        let nowMillSeconds = gameTime.TotalGameTime.TotalMilliseconds
        if !settim + interval < nowMillSeconds then
          settim := nowMillSeconds
          // インターバルごとに状態を更新
          updateBoard())

  let update =
    let lag = 300.
    let wait = ref 0.
    // ニューラルネットワークに訓練データを読み込み
    network <- loadPatterns network lifeGameTrainingData

    (fun gameTime ->
      wait := !wait + 60.
      if !wait > lag then
        wait := 0.
        if not trainingEnd then
          // 訓練データをロード
          if error > 0.1 then
            // ニューラルネットワークを訓練する
            let nw,err = training network
            network <- nw; error <- err

            if network.TryCount > network.RestartAfter then
              // 乱数の具合が悪かったり、ローカルミニマムにハマったりで訓練がなかなか終わらない場合は、最初から訓練しなおしてみる
              network <- initializeNetwork network
          else
            // 訓練おわりやしたー
            trainingEnd <- true
        else
          // ニューラルネットワークの訓練が終了したら、キー入力を受け付けたりライフゲームを開始
          operateKeys ()
          updateState gameTime)
  do
    // タイトルを設定
    this.Window.Title <- gametitle
    // ゲームループの間隔を設定 (60FPS)
    this.TargetElapsedTime <- TimeSpan.FromSeconds(1.0 / 60.)
    // マウスカーソルを表示
    this.IsMouseVisible <- true

  override this.Initialize() = 
    // ゲームウィンドウのサイズを設定
    gmanager.PreferredBackBufferWidth <- this.Width 
    gmanager.PreferredBackBufferHeight <- this.Height 
    base.Initialize () 
    
  /// ウィンドウの幅
  member this.Width with get () = cellWidth * width
  /// ウィンドウの高さ
  member this.Height with get () = cellHeight * height

  /// ライフゲームの状態を更新
  override this.Update (gameTime:GameTime) = 
    base.Update gameTime
    if Keyboard.GetState().IsKeyDown(Keys.Escape) then
      // Escが押されたらおしまい
      this.Exit()
    // ライフゲームクラスの状態を更新
    update gameTime 

  /// ライフゲームの状態を描画
  override this.Draw (gameTime:GameTime) = 
    base.Draw gameTime
    // テクスチャーデータのサンプリング方法をClampに設定
    gmanager.GraphicsDevice.SamplerStates.[1] <- new SamplerState(AddressU = TextureAddressMode.Clamp, AddressV = TextureAddressMode.Clamp, AddressW = TextureAddressMode.Clamp)
    // 背景を黒で塗りつぶし
    gmanager.GraphicsDevice.Clear(Color.Black)

    // ライフゲームクラスの状態を描画
    if not trainingEnd then    
      // ニューラルネットワークの訓練が終わるまでは、訓練の進捗を描画
      spriteBatch.Force().Begin()
      spriteBatch.Force().DrawString(font.Force (), String.Format("NeuralNework Training... Try:{0,3:##0}; Error:{1}", network.TryCount, error), Vector2(0.0f,0.0f), Color.White)
      spriteBatch.Force().End()
    else
      // 訓練終了後は、ライフゲームの状態を描画
      for x in [0..width-1] do
      for y in [0..height-1] do
        let pos = getPos x y
        if board.[x, y] = 0 then
          // 死んでるセルは真っ黒くろ助
          spriteBatch.Force().Begin()
          spriteBatch.Force().Draw(textureCell.Force(), pos, Color.Black)
          spriteBatch.Force().End()
        else
          // 生きてるセルは、セルのテクスチャを描画
          // テクスチャの描画に使用するエフェクトの設定
          let spinningLight = moveInCircle gameTime 5.0
          let time = gameTime.TotalGameTime.TotalSeconds
          let tiltUpAndDown = 0.5f + float32(Math.Cos(time * 0.75)) * 0.1f
          let lightDirection = new Vector3(spinningLight * tiltUpAndDown / 2.0f, tiltUpAndDown / 2.0f)
          lightDirection.Normalize()
          normalmapEffect.Force().Parameters.["LightDirection"].SetValue(lightDirection)
          gmanager.GraphicsDevice.Textures.[1] <- normalmapTextureCell.Force()

          // HLSLのエフェクトを使用して、セルのテクスチャを描画
          spriteBatch.Force().Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, normalmapEffect.Force())
          spriteBatch.Force().Draw(textureCell.Force(), pos, Color.White)
          spriteBatch.Force().End()

ライフゲームの生死判定を学習させるための訓練データは、F#で順列(Permutation)と組み合わせ(Combination)。YOU、Listモナドしちゃいなよ。集合モナドもあるよ。で書いた、
組み合わせ(Combination)を用いて全512パターンを作成しています。





セルを表している「はぐれメタル」の描画には、無駄にHLSL(High Level Shader Language)を使用しています。

HLSL

float3 LightDirection;
float3 LightColor = 2.0;
float3 AmbientColor = 0.1;

sampler TextureSampler : register(s0);
sampler NormalSampler : register(s1);

float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
    float4 tex = tex2D(TextureSampler, texCoord);
    float3 normal = tex2D(NormalSampler, texCoord);
    float lightAmount = max(dot(normal, LightDirection), 0.2);
    color.rgb *= AmbientColor + lightAmount * LightColor;
    return tex * color;
}

technique Normalmap
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 main();
    }
}

errorが0.1以下になるまで訓練するようにしているので、ローカルミニマムにハマってしまい、なかなか最後まで学習が完了しない。
早く収束させるには、中間層の隠れニューロンの数を調整したり訓練を甘くして学習レベルを下げるとよい。
この実装では運に左右される。ローカルミニマムに陥る問題を避ける方法はいくつかあるようだが、それはまた別のお話。



SkyDriveに、F#でニューラルネットワークソースコード一式を置いておきます。
SkyDrive - NN.zip

パケットの送受信量(F#) - ループとbreak

元ネタ
パケットの送受信量 (C#)(F#) - SIN@SAPPOROWORKSの覚書
http://d.hatena.ne.jp/spw0022/20111116/1321437712


確かにC#VBJavaなどの手続き型言語でのプログラミングに慣れていると、最初はそう思っちゃいますよね。
これぞ「ループでbreak脳の恐怖!」...って、私もその道を通りました。


方法1:再帰を使う

open System
open System.Net.NetworkInformation

let ar = 
    NetworkInterface.GetAllNetworkInterfaces()
    |>Seq.map(fun n -> n,n.GetIPv4Statistics())
    |>Seq.map(fun (n,s) -> n.Description,s.BytesReceived,s.BytesSent)

let rec loop func = 
  match func () with
  | ConsoleKey.X -> () 
  | _ -> loop func 

let func () = 
  printfn "%-45s\t%-10s\t%-10s" "Description" "Recv" "Send"
  printfn "-------------------------------------------------------------------------"
  ar|>Seq.iter(fun (d,r,s) -> printfn "%-15s\t%10d\t%10d" d r s)
  printfn ""
  printfn "何かのキーを押すと更新されます(Xで終了)"
  Console.ReadKey().Key |> fun key -> Console.Clear()
                                      key
loop func 

方法2:無限シーケンスを使う

open System
open System.Net.NetworkInformation

let ar = 
    NetworkInterface.GetAllNetworkInterfaces()
    |>Seq.map(fun n -> n,n.GetIPv4Statistics())
    |>Seq.map(fun (n,s) -> n.Description,s.BytesReceived,s.BytesSent)

let func () = 
  printfn "%-45s\t%-10s\t%-10s" "Description" "Recv" "Send"
  printfn "-------------------------------------------------------------------------"
  ar|>Seq.iter(fun (d,r,s) -> printfn "%-15s\t%10d\t%10d" d r s)
  printfn ""
  printfn "何かのキーを押すと更新されます(Xで終了)"
  let key = Console.ReadKey().Key
  Console.Clear()
  key 

let infiniteSeq = Seq.initInfinite (fun _ -> func())
let run item = if item = ConsoleKey.X then 
                 Some(item)
               else None
 
Seq.pick run infiniteSeq |> ignore


ループでbreakを表現する方法は、他にもいろいろあるでしょう。


■関連リンク

F#で楽々breakとcontinue。継続モナドまじパネぇっす!
http://d.hatena.ne.jp/zecl/20110322/p1

ふと、Seq.tryFindの変な(誰得な)使い方を思いついた。F#でbreakとcontinue再び。
http://d.hatena.ne.jp/zecl/20110822/p1



様々な角度から 物事を見ても、自分を見失わずにありたい。



補足
いくつか Seq.initInfinite 関係のツイートを拾ってみました。


ということで、Seq.initInfiniteのご利用は計画的に。

ARPテーブルの取得(F#) おまけもあるよ。


元ネタ:ARPテーブルの取得 (C#)(F#) - SIN@SAPPOROWORKSの覚書
http://d.hatena.ne.jp/spw0022/20111108/1320700838


SINさんがF#を書きまくっている今日この頃。F#の街札幌のF#りょくの高まりを感じざるを得ない。
F#らしい書き方かどうかはわかりませんが、SINさんのコードをベースにあまり深く考えずに。
コメント欄にお邪魔するには長いのでこちらで。


ARP(Address Resolution Protocol)テーブルの取得

#nowarn "9" "51"

open System
open System.Runtime.InteropServices
open System.Linq

[<DllImport("iphlpapi.dll")>]
extern int GetIpNetTable(IntPtr pTcpTable, int *pdwSize, bool bOrder);

[<Struct; StructLayout(LayoutKind.Sequential)>]
type MIB_IPNETROW =
    val Index:int
    val PhysAddrLen:int
    [<MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)>]
    val PhysAddr:byte []
    val Addr:int
    val Type:int

let ipstr(addr:int)=
    let b = BitConverter.GetBytes(addr)
    sprintf "%d.%d.%d.%d" b.[0] b.[1] b.[2] b.[3] 

let macstr(m:byte []) = 
    sprintf "%02x-%02x-%02x-%02x-%02x-%02x"  m.[0] m.[1] m.[2] m.[3] m.[4] m.[5]

let typeStr = ["";"その他";"無効";"動的";"静的"]

//取得部分
let ar = 
  let mutable size = 0
  GetIpNetTable(IntPtr.Zero, &&size, true) |> ignore
  let p = Marshal.AllocHGlobal(size)
  if GetIpNetTable(p, &&size, true) = 0 then 
    let end' = Marshal.ReadInt32(p) - 1
    let result = 
      let step = Marshal.SizeOf(typeof<MIB_IPNETROW>)
      let getPtr = 
        let ptr' = ref (IntPtr.Add(p, 4)) 
        (fun num -> if num=0 then !ptr'  else ptr':=IntPtr.Add(!ptr',step); !ptr') 
      [0..end'] |> List.map (fun x -> getPtr x) 
                |> List.map (fun ptr -> Marshal.PtrToStructure(ptr, typeof<MIB_IPNETROW>) :?> MIB_IPNETROW) 
    Marshal.FreeHGlobal(p) 
    result
  else []

//出力部分
printfn "インデックス\tインターネット アドレス\t物理アドレス\t種類"
ar |> List.toSeq 
   |> Seq.groupBy (fun n -> n.Index)
   |> Seq.iter (fun (i,ms) ->
                  printfn "\nインターフェース:0x%x\n  インターネット アドレス\t物理アドレス\t種類" i
                  ms |> Seq.iter (fun m -> printfn "  %-15s\t%s\t%s" <| ipstr(m.Addr) <| macstr(m.PhysAddr) <| typeStr.[m.Type]))
    
printfn "何かのキーを押してください。"
Console.ReadKey() |> ignore



■主な変更点とかモロモロ

  • ワーニングの波線が残り続けるのは、精神衛生上アレなので #nowarn で非表示に。
  • 取得部分をひとまとめの関数に。
  • なるべく、みゅーたぶり(mutableを使い)たくはないので、そのあたりをいじる。
  • 「for 〜 in 〜 do 〜」を利用することは、決してわるいことではありません。が、F#ではSeqモジュールを利用してLINQのノリで書ける。
  • 元ネタ「ar.Where(fun (x:MIB_IPNETROW) -> x.Index=i.Key)」で再度絞り込みする必要はなく、arのGroupByした結果をそのまま利用して結果を出力する。
  • 前方パイプライン演算子には負けますが、後方パイプライン演算子もなかなかかわいいです。
  • ふつうのF#erなので、奇をてらったへんたいコードは書きません。


おまけ

趣旨からはだいぶズレますが、コマンドラインからコマンドを実行して取得した値をそのまま書き出すズルしてみたり:p

open System

let filename = Environment.GetEnvironmentVariable("ComSpec")
let arguments = @"/c arp -a"
let psi = new Diagnostics.ProcessStartInfo(filename, arguments, CreateNoWindow = true, UseShellExecute = false, RedirectStandardInput = false, RedirectStandardOutput = true) 
let p = Diagnostics.Process.Start(psi)
p.WaitForExit()
let results = p.StandardOutput.ReadToEnd()
printfn "%s" results
Console.ReadKey () |> ignore


これはひどいw

とことんF#よぷよ! 第 63 回 CLR/H 勉強会で、F#とXNAを題材に発表します。

F# + XNAでとことんF#よぷよ!してみました




「ダークソウル」で心が折れそうな日々を送っている今日このごろですが、みなさんはいかがお過ごしでしょうか。
F# + XNAで「とことんぷよぷよ」っぽいものを実装してみました(はじめてのXNAゲームプログラミング)。
ただし、意図的、あるいは意図せずに元ネタのそれの仕様とは異なる場合があります。
動画の終盤、PAUSEをしてチート機能を使うことで19連鎖のデモをしています。



第 63 回 CLR/H 勉強会で、F#とXNAを題材にお話します。

日時 : 2011/10/15 (土) 13:30〜18:00(開場 13:00)
場所 : マイクロソフト北海道支店 セミナールーム (札幌市中央区北 5 条西 2 丁目 5 JR タワーオフィスプラザさっぽろ 20F)
      http://www.microsoft.com/ja-jp/mscorp/branch/hokkaido.aspx
参加費:500 円(会場費やお菓子代に使用させて頂きます)


【タイトル】
とことんF#よぷよ! - F# + XNAによるゲームプログラミング入門 -


【概要】
みんな大好き、落ち物パズルゲーム「ぷよぷよ」は今年で 20 周年を迎えました。
ぷよぷよ風落ち物パズルゲームの作り方と、関数型パラダイムを中心としたマルチパラダイム言語 F# による
XNA でのゲーム開発の基本についてお話します。
「ぷよぷよ風落ちゲーを 500 行程度で実装できる。そう、F# + XNA ならね!」


【スピーカー】
わたし


CLR/H 公式サイト
http://clr-h.jp/



また、マイクロソフトエバンジェリストで、著作に「The root of .NET Framework」や「実践 F# 関数型プログラミング入門」などがある
荒井省三さんが、「DLR + ASync + アルファ」というタイトルで濃いセッションをしてくださいます。ぜひお越しください。



事前に「とことんF#よぷよ!」のコードを晒してみる

ということで、F#でXNAなコードを勉強会前に晒してしまう大盤振る舞い(!?)
コードは決してうつくしくはありませんが、興味のある方はごらんください。


PuyoPuyo.fs

namespace PuyoPuyoLibrary
open System
type PuyoColors =
 | n = 0x00 | r = 0x01
 | y = 0x02 | p = 0x03
 | g = 0x04 | b = 0x05

type Union = 
  | None   = 0b0000 | Top    = 0b0001
  | Left   = 0b0010 | Bottom = 0b0100
  | Right  = 0b1000
      
type puyoObj = { position : int * int; pattern : PuyoColors[][]; color1: PuyoColors; color2: PuyoColors; hidden: bool; upside : bool}
type PuyoState = { pw : int; ph : int; width : int; height : int
                   gameover   : bool; pause : bool ;cheat : bool
                   totalScore : decimal; highScore : decimal; maxLevel : int
                   scoreBase  : int; magnifyingPower : int
                   rensa : int; union : int; colors : int; erased : int
                   current    : puyoObj; next : puyoObj array
                   field      : PuyoColors[,];  checkField : bool[,]
                   falling    : bool; allclear   : bool
                   etarget    : (int * int * PuyoColors) list} 

module PuyoPuyo =
  let patterns x y =
    [| [| enum 0;      x; enum 0; |]
       [| enum 0;      y; enum 0; |]
       [| enum 0; enum 0; enum 0; |] |]
  let none : PuyoColors [][] = [| [| |] |]
  let clearCheckField ps = { ps with checkField = Array2D.create ps.width ps.height false }
  let getLevel erased = erased / 40 |> fun x -> if x >= 999 then 999 else x + 1
      
  let create erased =
    let rand = new Random(System.DateTime.Now.Millisecond)
    let create' = fun s e -> enum (rand.Next(s, e)), enum (rand.Next(s, e))
    fun () -> (if getLevel erased < 3 then (1,4) ||> create'
               elif getLevel erased < 5 then (1,5) ||> create'
               else (1,6) ||> create') ||> fun x y -> { position = (1, 0); pattern = patterns x y; color1 = x; color2 = y; hidden = false; upside = true }
  let getPuyoObj = 
    let queue = new System.Collections.Generic.Queue<puyoObj>()
    fun erased -> queue.Count |> function
       | 0 -> seq {1..3} |> Seq.iter (fun x -> queue.Enqueue ( () |> create erased))
              queue.Dequeue(),queue.ToArray()
       | _ -> queue.Enqueue(() |> create erased)
              queue.Dequeue(),queue.ToArray()  
  
  let convert (source : PuyoColors [][]) = 
    (Array.length source, source.GetLength(0)) ||> fun row col -> Array2D.create row col PuyoColors.n |> fun array ->
    if source = none then array
    else seq { for i in 0..row - 1 do
               for j in 0..col - 1 do yield i,j } |> Seq.iter (fun (i,j) -> array.[j,i] <- source.[i].[j])
         array

  type Direction = | Right | Left | Down
  let move ps direction = ps.current.position ||> fun x y -> direction |> function
    | Right -> { ps.current with position = x + 1, y }
    | Left  -> { ps.current with position = x - 1, y }
    | Down  -> { ps.current with position = x    , y + 1 } 

  let descend ps = ps.current.position ||> fun x y -> 
    if y + 1 < ps.height  then { ps.current with position = x, y + 1 }
    else ps.current 

  let rotate puyo action = convert puyo.pattern |> fun pattern ->
    let len = Array2D.length1 pattern 
    seq { for i in 0..len - 1 do
          for j in 0..len - 1 do
          yield i,j,len,pattern} |> Seq.iter action
    puyo

  let avoidance ps exchange (c1,c2) = 
    let (|Insert|_|) c1 c2 ps = ps.current.position ||> fun x y ->
      let judge c f g=
        if (c <> PuyoColors.n && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n)))) &&
           (c <> PuyoColors.n && (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n)))) then
           f()
        else g() 
      judge c1 (fun () -> Some ps.current) (fun () -> judge c2 (fun () -> Some ps.current) (fun () -> None))
    let (|CollideLeft|_|) c1 c2 ps = ps.current.position ||> fun x y ->  
      if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && (x < 0 || (x >= 0 && (ps.field.[x,y] <> PuyoColors.n || ps.field.[x,y+1] <> PuyoColors.n ))) then
        Some ps.current 
      else None
    let (|CollideRight|_|) c1 c2 ps = ps.current.position ||> fun x y ->  
      if (c1 <> PuyoColors.n || c2 <> PuyoColors.n) && 
        (x+2 > ps.width - 1 || (x+2 <= ps.width - 1 && (ps.field.[x+2,y] <> PuyoColors.n || ps.field.[x+2,y+1] <> PuyoColors.n))) then
        Some ps.current       
      else None
    ps |> function
    | Insert c1 c2  puyo -> puyo
    | CollideLeft c1 c2 puyo -> move ps Right |> rotate <| exchange
    | CollideRight c1 c2  puyo -> move ps Left |> rotate <| exchange
    | _ -> ps.current |> rotate <| exchange

  let target ps = ps.current.pattern.[2].[1], ps.current.pattern.[0].[1]
  let rotateR ps =
    if ps.current.pattern = none then ps.current 
    else
      (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[i].[j] <- pattern.[i,len - 1 - j])
      |> avoidance ps <| target ps

  let rotateL ps =
    if ps.current.pattern = none then ps.current 
    else
      (fun (i,j,len,pattern:PuyoColors[,]) -> ps.current.pattern.[j].[i] <- pattern.[len - 1 - j,i])
      |> avoidance ps <| target ps

  let createState pw ph width height erased =
    let current,next = getPuyoObj erased
    { pw = pw; ph = ph; width = width; height = height; gameover = false; pause = false; cheat = false;
      totalScore = decimal 0; highScore = decimal 0; maxLevel = 0; scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; erased = 0;
      current = current; next = next;  field = Array2D.create width height PuyoColors.n
      checkField = Array2D.create width height false;  falling = false; allclear = false; etarget = List.empty }

  let nextPuyo ps = if not ps.cheat then getPuyoObj ps.erased ||> fun current next -> {ps with current= current; next = next} 
                           else { ps with current = { position = (1, 0); pattern = patterns PuyoColors.p PuyoColors.y; color1 = PuyoColors.p; color2 = PuyoColors.y; hidden = false; upside = true }; next = ps.next }
  let reset ps = nextPuyo { ps with erased = 0 } |> fun ps -> nextPuyo ps |> fun ps -> nextPuyo ps |> fun ps -> 
    { ps with field = Array2D.create ps.width ps.height PuyoColors.n; checkField = Array2D.create ps.width ps.height false; etarget = List.empty 
              gameover = false; pause = false; cheat = false; totalScore = decimal 0; maxLevel = ps.maxLevel; highScore = ps.highScore;
              scoreBase = 0; magnifyingPower = 0; rensa = 0; union = 0; colors = 0; allclear = false }

  let cheat ps = Array2D.create ps.width ps.height PuyoColors.n |> fun newfield ->
    let dic = dict[0,[0;0;0;2;1;1];1,[1;2;0;3;2;3];2,[3;2;3;2;1;1];3,[1;2;3;2;1;3];4,[2;3;2;1;2;3];5,[3;2;1;2;1;3];6,[2;3;2;1;2;1];7,[2;3;2;1;2;1];8,[2;1;1;3;1;3];9,[1;2;3;2;3;3];10,[3;1;2;3;2;1];11,[3;1;2;3;2;1];12,[3;1;2;3;2;1]]
    for y in 0..ps.height - 1 do dic.[y] |> List.iteri (fun x c -> newfield.[x, y] <- enum c)
    nextPuyo { ps with field = newfield; cheat = true } 

  let detectCollision puyo field = 
    let result = ref false
    puyo.pattern |> convert |> Array2D.iteri
      (fun i j c -> c |> function
         | PuyoColors.n -> ()
         | _ -> puyo.position ||> fun x y -> (x + i,y + j) ||> fun xi yj ->
                if xi < 0 || xi >= Array2D.length1 field ||
                   yj < 0 || yj >= Array2D.length2 field ||
                   field.[xi, yj] <> PuyoColors.n then 
                   result := true); !result

  let getAllclearScore ps = if ps.field = Array2D.create ps.width ps.height PuyoColors.n |> not then decimal 0
                            else decimal 3600 + decimal (getLevel ps.erased * 5) 
  let getScore ps =
    let rensaBonus n = dict [1,0;2,8;3,16;4,32;5,64;6,96;7,128;8,160;9,192;10,224;11,256;12,288;13,320;14,352;15,388;16,416;17,448;18,480;19,512] |> fun dic -> if n > 19 then dic.[19]  else dic.[n]
    let unitBounus n = dict [4,0;5,2;6,3;7,4;8,5;9,6;10,7;] |> fun dic -> if n > 10 then 10 else dic.[n]
    let colursBounus n = dict [0,0;1,0;2,3;3,6;4,12;5,24] |> fun dic -> dic.[n]
    let a,b,c,d = ps.union * 10,rensaBonus ps.rensa,unitBounus ps.union,colursBounus ps.colors // 基本得点,連鎖ボーナス,連結ボーナス,複色ボーナス
    b + c + d |> fun x -> if x = 0 then a,1 else a,x

  let fixed' ps = ps.current.pattern |> convert |> fun c ->
    seq { for i in 0..Array2D.length1 c - 1 do 
          for j in 0..Array2D.length2 c - 1 do
          c.[i,j] |> function
          | PuyoColors.n -> ()
          | _ -> ps.current.position ||> fun x y -> ps.field.[x + i, y + j] <- ps.current.pattern.[j].[i] } |> Seq.iter id
    { ps with current = {ps.current with hidden = true }; falling = true }

  let fall ps =
    seq { for x in 0..ps.width-1 do
          for y in [ps.height-1 .. -1 .. 0] do
          if ps.field.[x,y] = PuyoColors.n then
            for z in (y-1) .. -1 .. 0 do                                 
            if ps.field.[x,z] > PuyoColors.n then
              ps.field.[x,z+1] <- ps.field.[x,z]
              ps.field.[x,z] <- PuyoColors.n
              yield z } |> Seq.length 

  let erase ps = 
    let erase' x y ps = 
      if ps.field.[x,y] = PuyoColors.n then None
      else
        let result, list = ref 1, ref [x,y]
        let rec search x y result = 
          ps.checkField.[x,y] <- true
          let search' x y retsult f g predicate = 
            if predicate (f x) (g y) ps && 
                ps.checkField.[f x, g y] <> true &&
                ps.field.[f x, g y] = ps.field.[x,y] then 
              result := !result + 1
              list := !list@[f x, g y] 
              search (f x) (g y) result 
          search' x y result (fun x -> x+1) id (fun x y ps -> x < ps.width)
          search' x y result id (fun y -> y+1) (fun x y ps -> y < ps.height)
          search' x y result (fun x -> x-1) id (fun x y ps -> x > 0)
          search' x y result id (fun y -> y-1) (fun x y ps -> y > 0)
        search x y result
        if !result >= 4 then 
          List.map (fun (x,y) -> x,y,ps.field.[x,y] )!list |> Some
        else None
    seq { for x in 0..ps.width-1 do
          for y in ps.height-1 .. -1 .. 0 do
          let ecount = erase' x y (clearCheckField ps) |> function
            |Some x -> x
            | _ -> []
          yield! ecount } |> Seq.distinct |> Seq.toList 
    |> fun etarget -> let colors = List.map (fun (x,y,_) -> ps.field.[x,y]) etarget |> Seq.distinct |> Seq.toList 
                      etarget,List.length etarget, List.length colors, { ps with current = { ps.current with pattern = none } }

  let getUnion ps x y = 
    if x < 0 || x > ps.width - 1 || y > ps.height - 1 || ps.field.[x,y] = PuyoColors.n then Union.None
    else 
      let rise ps x y =  [y..(ps.height-1)] |> List.exists (fun y -> ps.field.[x,y] = PuyoColors.n)
      let lrunion n b f add = 
        if b || (ps.field.[f x,y] = ps.field.[x,y] &&
                 (y = ps.height - 1 
                 || (y < ps.height - 1 && (rise ps x y |> not && rise ps (f x) y |> not))
                 || (y < ps.height - 1 && (rise ps x y && rise ps (f x) y )))) |> not then n
        else add n
      let left n = lrunion n (x = 0) (fun x -> x - 1) ((+) Union.Left)
      let right n = lrunion n (x >= ps.width - 1) ((+) 1) ((+) Union.Right) 
      let tbunion n f g c = if f || ps.field.[x,g y] = ps.field.[x,y] |> not then n else n + c
      let top n = tbunion n (y = 0) (fun y -> y - 1) Union.Top
      let bottom n = tbunion n (y >= ps.height - 1) (fun y -> y + 1) Union.Bottom
      Union.None |> (top >> left >> right >> bottom)


PuyoGame.fs

namespace FSharpyopuyo
open System
open System.Runtime.Serialization
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Audio
open Microsoft.Xna.Framework.Graphics
open Microsoft.Xna.Framework.Input
open Microsoft.Xna.Framework.Storage
open PuyoPuyoLibrary

[<DataContract>]
type SaveGameData = { [<field:DataMember(Name="MaxLevel")>] MaxLevel : int; [<field:DataMember(Name="HighScore")>] HighScore : decimal }

type PuyoGame () as this = 
  inherit Game()
  let gametitle, gmanager, sprite = "とことんF#よぷよ!", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice)
  let mutable ps = PuyoPuyo.createState 32 32 6 13 0 
  let slowTimelag,fastTimelag, fps = 350.,230.,60.
  let backgroundTexture,puyoTexture,blinkTexture,gameoverTexture,allclearTexture,carbancleTexture,suketoudaraTexture = 
    ["background";"puyopuyo";"blink";"batanQ";"allclear";"carbuncle";"suketoudara"] 
    |> List.map (fun name -> lazy this.Content.Load<Texture2D>(@"Content\image\" + name)) |> function 
    | a::b::c::d::e::f::g::[] -> a,b,c,d,e,f,g | _ -> invalidArg "tlist" "リストの長さが違うよ。"
  let font = lazy this.Content.Load<SpriteFont>(@"Content\font\SpriteFont1")
  let gameSe = ["move";"rotate";"drop";"batanQ";"allclear";"pafu";] |> List.map 
                (fun name -> this.Content.Load<SoundEffect>(@"Content\sound\" + name).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let chainSe = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chain" + if i > 7  then string 7 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let chainVoice = [1..19] |> List.map (fun i -> this.Content.Load<SoundEffect>(@"Content\sound\chainvoice" + if i > 5 then string 5 else string i).CreateInstance() |> fun x -> x.Volume <- 0.3f; lazy x)
  let bgm = (this.Content.Load<SoundEffect>(@"Content\sound\MorningOfPuyoPuyo").CreateInstance() |> fun x -> x.Volume <- 0.2f; x.IsLooped <- true; lazy x)

  let soundPlay (se:Lazy<SoundEffectInstance>) = se.Force().Play()
  let checkPauseKey  = 
    let keyWait, pauseKeyDown, pauseTimelag =  ref 0., ref false, 700.
    (fun () -> if not ps.gameover then
                 keyWait := !keyWait + fps
                 if ps.pause && !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.F12) then 
                      ps <- PuyoPuyo.cheat ps;
                 if !keyWait > pauseTimelag && Keyboard.GetState().IsKeyDown(Keys.P) && !pauseKeyDown then 
                   gameSe.[5] |> soundPlay; bgm |> soundPlay; ps <- { ps with pause = false }; pauseKeyDown := false
                 elif !keyWait > pauseTimelag &&  Keyboard.GetState().IsKeyDown(Keys.P) && not !pauseKeyDown then 
                   gameSe.[5] |> soundPlay; bgm.Force().Stop(); ps <- { ps with pause = true }; pauseKeyDown := true 
                 if !keyWait > pauseTimelag then keyWait := 0.)
  let operateKeys = 
    let keyWait = ref 0.
    (fun () -> 
      keyWait := !keyWait + fps
      let operateKey key = 
        if not ps.gameover then
            key |> function
            | Keys.Z | Keys.Space | Keys.NumPad8 when !keyWait > slowTimelag -> 
              gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateL ps)
            | Keys.X | Keys.Up | Keys.NumPad5 when !keyWait > slowTimelag ->
              gameSe.[1] |> soundPlay; Some (PuyoPuyo.rotateR ps)
            | Keys.Right | Keys.NumPad6  when !keyWait > slowTimelag -> 
              gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Right)
            | Keys.Left  | Keys.NumPad4  when !keyWait > slowTimelag -> 
              gameSe.[0] |> soundPlay; Some (PuyoPuyo.move ps PuyoPuyo.Left)
            | Keys.Down  | Keys.NumPad2  when !keyWait > fastTimelag -> 
              if ps.current.hidden |> not then ps <- { ps with totalScore = (+) ps.totalScore <| decimal 1 }
              Some (PuyoPuyo.move ps PuyoPuyo.Down)
            | Keys.Escape -> this.Exit(); None
            | _ -> None
          else
            key |> function
            | Keys.Enter -> ps <- PuyoPuyo.reset ps; bgm |> soundPlay; None
            | Keys.Escape -> this.Exit(); None
            | _ -> None
        |> function 
          | Some x -> if not (PuyoPuyo.detectCollision x ps.field) then
                        ps <- { ps with current = x }
          | None -> ()
      let resetWait () = if !keyWait > slowTimelag then keyWait := 0.
      Keyboard.GetState().GetPressedKeys() |> Array.toList |> List.sort |> List.rev |> function
      | [x] -> operateKey x; resetWait ()
      | [x;y] -> operateKey x; operateKey y; resetWait ()
      | [x;y;z] | x::y::z::_  -> operateKey x; operateKey y; operateKey z; resetWait ()
      | _ -> () ) 

  let blink = let blinkWait, blink = ref 0., ref false
              fun () -> if ps.pause then !blink else
                        blinkWait := !blinkWait + fps
                        if !blinkWait > 5. * fps then blink := not !blink; blinkWait := 0.
                        !blink

  let saveFilename = "puyosys.sav"
  let saveStorage (device:StorageDevice) (level:int) score = 
    let result = device.BeginOpenContainer(gametitle, null, null)
    if result.AsyncWaitHandle.WaitOne() then
      use container = device.EndOpenContainer(result)
      result.AsyncWaitHandle.Close()
      if container <> null then
        if (container.FileExists(saveFilename)) then
          container.DeleteFile(saveFilename)
        use stream = container.CreateFile(saveFilename)
        let serializer = DataContractSerializer(typeof<SaveGameData>)
        serializer.WriteObject(stream , { MaxLevel = level; HighScore = score })
  
  let loadStorage (device:StorageDevice) = 
    let result = device.BeginOpenContainer(gametitle, null, null)
    let level,highscore = ref 1, decimal 0 |> ref
    if result.AsyncWaitHandle.WaitOne() then
      use container = device.EndOpenContainer(result)
      result.AsyncWaitHandle.Close()
      if container <> null && container.FileExists(saveFilename) then
        use stream = container.OpenFile(saveFilename, System.IO.FileMode.Open )
        let serializer = DataContractSerializer(typeof<SaveGameData>)
        let data = serializer.ReadObject(stream) :?> SaveGameData
        level := data.MaxLevel; highscore := data.HighScore 
    !level, !highscore

  let drawPuyo (c:PuyoColors, x, y, ((i, j) as point)) (location:Vector2) rect hw =
    let fx,fy = float32 (x+i), float32 (y+j)
    let lx,ly = float32 location.X, float32 location.Y 
    let draw f = sprite.Force().Draw(puyoTexture.Force(), Vector2(32.f * fx + lx,32.f * fy + ly |> f),  Nullable rect, Color.White)
                 if point = (1,1) then 
                   let texture = if blink () then blinkTexture.Force() else puyoTexture.Force() 
                   sprite.Force().Draw(texture, Vector2(32.f * fx + lx,32.f * fy + ly |> f),  Nullable rect, Color.White)
    c |> function
      | PuyoColors.n -> () 
      | _ ->  if hw && ps.current.upside then (fun x -> x - 16.f) |> draw else draw id 

  let uncoupling c adjustx = Rectangle(0, ps.ph * (int c - 1), ps.pw / adjustx, ps.ph)
  let drawText (msg:string) (v:Vector2) c = sprite.Force() |> function
    | x -> font.Force() |> fun font -> [font, msg, Vector2(v.X+3.f,v.Y+3.f), Color.Black; font, msg, v, c ] 
                                       |> List.iter (fun (font, msg, v, c) ->  x.DrawString(font, msg, v, c))
  let drawLiteral () = 
    ["NEXT",Vector2(240.f, 34.f),Color.MediumSpringGreen; "LEVEL",Vector2(240.f, 322.f),Color.MediumSpringGreen; "SC.",Vector2(32.f, 420.f),Color.HotPink; 
     "HSC.",Vector2(32.f, 2.f),Color.Orange; "MLV.",Vector2(230.f, 2.f),Color.Orange] |> List.iter (fun (s,v,c) -> drawText s v c)
 
  let drawScoreAndLevel () = 
    seq { yield ((PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0')), Vector2(255.f, 356.f), Color.HotPink 
          if ps.totalScore < ps.highScore then 
            yield string ps.highScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f), Color.Orange
          else yield string ps.totalScore |> fun s ->  s, Vector2(224.f - font.Force().MeasureString(s).X, 2.f) , Color.Orange
          if ps.etarget = List.empty then 
            yield string ps.totalScore |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.HotPink
          else yield string ps.scoreBase + "×" + (string ps.magnifyingPower).PadLeft(4,' ') 
                     |> fun s -> s, Vector2(224.f - font.Force().MeasureString(s).X, 420.f), Color.SkyBlue 
          if (PuyoPuyo.getLevel ps.erased) < ps.maxLevel then
            yield (string ps.maxLevel).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange
          else yield (PuyoPuyo.getLevel ps.erased |> string).PadLeft(3,'0') , Vector2(280.f , 2.f), Color.Orange }
    |> Seq.iter (fun (s,v,c) -> drawText s v c)

  let drawField = 
    let gameoverWait = ref 0.
    (fun location () ->
    let unit ps x y (c:PuyoColors) adjustx = 
      PuyoPuyo.getUnion ps x y |> fun r -> Convert.ToInt32(r |> int |> string) 
      |> fun x -> Rectangle(ps.pw * x,ps.ph * (int c - 1), ps.pw / adjustx, ps.ph)
    ps.field |> Array2D.iteri (fun i j c -> if j > 0 then drawPuyo(c, i, j, (0, 0)) location (unit ps i j c 1) false)
    if ps.gameover then
      sprite.Force().Draw(gameoverTexture.Force(), Vector2(50.f,400.f - float32 !gameoverWait / 40.f), Color(255, 255, 255, 128))
      if !gameoverWait <= 12000. then 
        bgm.Force().Stop() 
        gameSe.[3] |> soundPlay 
        gameoverWait := !gameoverWait + fps * 3.
      else
        ["つづける:Enter",10.f;"やめる:Esc",50.f] |> List.iter (fun (msg,y) -> drawText msg <| Vector2(45.f, 160.f + y) <| Color.Gold)
    else
      gameoverWait := 0.
      if ps.current.hidden |> not then
        ps.current.position ||> fun x y ->
        (ps.current.pattern) |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri 
          (fun i j c -> if y + j > 0 then drawPuyo(c, x , y, (i, j)) location (uncoupling c 1) true)
        sprite.Force().Draw(backgroundTexture.Force(), Vector2(0.f,0.f),Nullable(Rectangle(0,0,256,32)), Color.White)) <| Vector2(32.f, 0.f) 

  let drawErase = 
    let animeWait = ref 0.
    (fun f (location:Vector2) () ->
      if ps.etarget <> List.empty then
        animeWait := !animeWait + fps
        let draw union = List.iter (fun (x,y,c) -> if y > 0 then drawPuyo(c, x, y, (0, 0)) location (Rectangle(ps.pw * union ,ps.ph * (int c - 1), ps.pw, ps.ph)) false) ps.etarget
        if !animeWait < 780. then draw 16 elif !animeWait < 1560. then draw 17
        else animeWait := 0.; f()) <| (fun () -> ps <- { ps with etarget = List.empty }) <| Vector2(32.f, 0.f)

  let drawAnimation (texture:Lazy<Texture2D>) width hight max vector = 
    let animeWait, counter = ref 0., ref 0
    (fun (location:Vector2) () -> 
      animeWait := !animeWait + fps
      if !animeWait > 780. - (30. * float (PuyoPuyo.getLevel ps.erased + 1)) then 
        animeWait := 0.; 
        if not ps.pause then incr counter  
      sprite.Force().Draw(texture.Force(), location, Nullable (Rectangle(width * !counter,0,width,hight)),  Color.White)
      if !counter = max then   counter := 0) <| vector
  let drawCarbancle = drawAnimation <| carbancleTexture <| 25 <| 25 <| 37 <| Vector2(260.f,388.f)
  let drawSukesoudara = drawAnimation <| suketoudaraTexture <| 72 <| 54 <| 10 <| Vector2(236.f,228.f)

  let drawNext () = 
    let drawPuyo' pattern location adjustx =  pattern |> PuyoPuyoLibrary.PuyoPuyo.convert |> Array2D.iteri
                                               (fun i j c -> drawPuyo (c, 1, j, (0, 0)) location (uncoupling c adjustx) false)
    Vector2(224.f, 96.f) |> fun location -> [ps.next.[0].pattern, location, 1; ps.next.[1].pattern, (Vector2(location.X + 32.f, location.Y + 32.f)), 2] 
      |> List.iter (fun (p,v,a) ->  drawPuyo' p v a) 

  let drawAllClear () = if ps.allclear then sprite.Force().Draw(allclearTexture.Force(), Vector2(45.f,50.f),  Color.White)
  let drawPause () = if ps.pause then drawText "PAUSE" <| Vector2(96.f,150.f) <| Color.Gold 
  let drawRensa = 
    let rensaWait, rensaCount = ref 0., ref 0 
    (fun () -> if ps.pause |> not then rensaWait := !rensaWait + fps * 1.5
               if ps.rensa > 0 && !rensaWait < 25. * fps then
                 (3,7) ||> fun x y -> drawText (string ps.rensa + "れんさ") <| Vector2(32.f * float32 x, 32.f * float32 y - float32(!rensaWait / 40.)) <| Color.HotPink 
               if ps.rensa <> !rensaCount && !rensaWait > 50. * fps then
                 rensaCount := ps.rensa; rensaWait := 0. )
  let save () = 
    let save maxLevel highScore = 
      let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null)
      let device = StorageDevice.EndShowSelector(result)
      saveStorage device maxLevel highScore 
      ps <- { ps with maxLevel = maxLevel; highScore = highScore }
    let maxlevelAndHighScore () = if PuyoPuyo.getLevel ps.erased > ps.maxLevel then PuyoPuyo.getLevel ps.erased else ps.maxLevel 
                                , if ps.totalScore > ps.highScore then ps.totalScore else ps.highScore
    if PuyoPuyo.getLevel ps.erased > ps.maxLevel || ps.totalScore > ps.highScore then
      maxlevelAndHighScore () ||> fun maxLevel highScore -> save maxLevel highScore

  let update = 
    let updateWait, updateTimelag = ref 0., ref (72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased)))
    let chain = 
      (fun cont ->
        if ps.etarget = List.empty && PuyoPuyo.fall ps > 0 then
          ps <- { ps with falling = true }
          updateTimelag := 24. * fps + (15. * float (PuyoPuyo.getLevel ps.erased + 1))
        else
          PuyoPuyo.erase ps |> function
          | etarget,union,colors,newps when union <= 0 -> 
            ps <- { newps with rensa = 0; union = union; colors = colors } 
            PuyoPuyo.getAllclearScore ps |> fun z -> 
            if z > decimal 0 then
              gameSe.[4] |> soundPlay
              ps <- { ps with totalScore = (z |> fun x -> x + ps.totalScore); allclear = true }
            ps <- { ps with falling = false }
            cont ()
            if PuyoPuyo.detectCollision ps.current ps.field then
              ps <- { ps with gameover = true } ; save()
            updateTimelag := 72. * fps + (15. * float (PuyoPuyo.getLevel ps.erased))
          | etarget,union,colors,newps -> 
            List.iter (fun (x,y,_) -> ps.field.[x,y] <- PuyoColors.n) etarget
            async { newps.rensa |> (fun x -> if x > 19 then chainVoice.[19] else chainVoice.[x]) |> soundPlay } |> Async.Start
            async { newps.rensa |> (fun x -> if x > 19 then chainSe.[19] else chainSe.[x]) |> soundPlay } |> Async.Start
            ps <- { newps with rensa = newps.rensa + 1; union = union; colors = colors; erased = newps.erased + union; cheat = false }
            let scoreBase,magnifyingPower = PuyoPuyo.getScore ps
            ps <- { ps with totalScore = decimal (scoreBase * magnifyingPower) + ps.totalScore; 
                            allclear = false; etarget = etarget; scoreBase = scoreBase; magnifyingPower = magnifyingPower })
    fun () -> updateWait := !updateWait + fps + (15. * float (PuyoPuyo.getLevel ps.erased))
              if !updateWait > !updateTimelag then
                updateWait := 0.
                let puyo = PuyoPuyo.descend ps
                if PuyoPuyo.detectCollision puyo ps.field |> not then
                  ps <- { ps with current = puyo }
                if ps.falling && ps.etarget = List.empty then
                    chain (fun () -> ps <- PuyoPuyo.nextPuyo ps)
              if !updateWait < 36. * fps then ps <- { ps with current = { ps.current with upside = true } } 
              else ps <- { ps with current = { ps.current with upside = false } }
  do
    this.Window.Title <- gametitle
    (320,448) ||> fun x y -> gmanager.PreferredBackBufferWidth <- x ; gmanager.PreferredBackBufferHeight <- y
    this.TargetElapsedTime <- TimeSpan.FromSeconds(1.0 / fps)

  override thi.Initialize() = base.Initialize() |> fun _ ->
    let result = StorageDevice.BeginShowSelector(PlayerIndex.One, null, null)
    let device = StorageDevice.EndShowSelector(result)
    loadStorage device ||> fun maxLevel highScore -> ps <- { ps with maxLevel = maxLevel; highScore = highScore }
    bgm |> soundPlay

  override this.Update(gameTime) = 
    checkPauseKey ()
    if ps.pause |>  not then
      base.Update gameTime
      [operateKeys;update] |> List.iter (fun f -> f ())
      if not ps.gameover then
        if PuyoPuyo.detectCollision (PuyoPuyo.descend ps) ps.field then
          if not ps.falling then 
            gameSe.[2] |> soundPlay; ps <- PuyoPuyo.fixed' ps
        elif ps.current.pattern = PuyoPuyo.none && not ps.falling then 
             ps <- PuyoPuyo.nextPuyo ps

  override this.Draw(gameTime) = base.Draw gameTime |> fun _ ->
    gmanager.GraphicsDevice.Clear(Color.Black)
    sprite.Force().Begin ()
    sprite.Force().Draw(backgroundTexture.Force(), Vector2.Zero, Color.White)
    [drawAllClear;drawField;drawNext;drawSukesoudara;drawCarbancle;drawErase;drawRensa;drawLiteral;drawScoreAndLevel;drawPause] |> List.iter (fun f -> f())
    sprite.Force().End ()

  override this.EndRun () = base.EndRun(); save()

module Program =
  [<EntryPoint>]
  let main (args : string[]) = use game = new PuyoGame() in game.Run(); 0

画像データ、音データに関しましては、動画をごらん頂けばわかるとおり配布することはできません。お察しください。
コードを読み理解するとができれば、どんな画像や音データを用意すれば適切に動作させられるかわかるはず。
なお、コードは読めなくてもまったく問題ありません。それでは、CLR/H勉強会でお会いしましょう。

F#でのパターンマッチ。「match a with 〜」よりも 「a |> function 〜」を使おうぜ(異論は認める)


以前、「F#では、パターンマッチを「match x with」と書く流派と「x |> function」と書く流派がございます。」という記事を書きました。
ほぼほぼ置き換えることが可能な、「match a with 〜」と 「a |> function 〜」ですが、
実は完全互換ではありません。これはカンスウガタゲンガーには割と気づきにくい違いかもしれません。
細かすぎて伝わらない程度の違いですが、カンスウガタゲンガーの@nakamura_toさんにリアクション頂いたのでちょっと書いてみます。


「match a with 〜」と 「a |> function 〜」の違い

結論から申し上げますと、functionによるパターンマッチは"クロージャー"であるということです。


「a |> function 〜」をまじまじと見つめていただくとわかるように、とてもクロージャー感を醸し出しています。
「あ〜、言われてみればそうですね。」という感じではないでしょうか。
つまり、「functionによるパターンマッチではmutableな変数をキャプチャすることができない。」という違いがあります。

module Sample =

  let hoge = 
    let hoge0 () = 
      let mutable s = "ほげ"
      match s with
      | "ふが" -> ()
      | x -> printfn "%s" s
    hoge1 ()

    let hoge1 () = 
      let mutable s = "ほげ"
      s |> function
      | "ふが" -> ()
      | x -> printfn "%s" s // mutableな変数をキャプチャできないのでエラー
    hoge1 ()

    let hoge0 () = 
      let s = ref "ほげ"
      match !s with
      | "ふが" -> ()
      | x -> printfn "%s" !s 
    hoge0 ()

    let hoge1 () = 
      let s = ref "ほげ"
      !s |> function
      | "ふが" -> ()
      | x -> printfn "%s" !s // Reference CellsはクロージャーでキャプチャできるのでOK
    hoge1 ()

    ()

open System
open Sample
hoge
Console.ReadLine () |> ignore


関数プログラミングパラダイムを主体としているカンスウガタゲンガーは、
極力 mutable を用いないプログラミングをします。mutable を利用するとしても用途はとても限定的です。
また、パターンマッチさせた値を用いずに、直接 mutable な変数をキャプチャするような書き方も好ましくないので、通常そのような書き方はしないでしょう。
ですから、このわずかな違いを意識する機会はあまりないかもしれません。
完全互換ではありませんが、一般的なカンスウガタゲンガーが書く「match a with 〜」は、ほとんどの場合「a |> function 〜」に置き換え可能でしょう。


いっそのこと、パターンマッチは「a |> function 〜」で行うように統一してしまったほうが(コーディング規約的な意味で)、
パターンマッチ内で直接 mutable な変数をキャプチャするような書き方を抑止することができるので、
カンスウガタゲンガー的にはうれしいのかもしれません(異論は認める)。
好みというよりだだの自己満ですが、「a |> function 〜」の方がイケてると思うので私は多用しまくりんぐです。



F#の勉強会とかやりたい

北海道旭川市近郊でF#の勉強をしてる人 or してみたい人いるかなあ。
もしいたら、何かコメントいただきたく。

ふと、Seq.tryFindの変な(誰得な)使い方を思いついた。F#でbreakとcontinue再び。

以前、「F#で楽々breakとcontinue。継続モナドまじパネぇっす!」を書きました。
確かに楽々ではあるんですが、継続モナドとかマジで難しいですよ。
しかも、Visual Studioデバッグとかまともにできないですし...(´・ω・`)ショボーンな気持ちになっちゃいます。


F#でbreakとcontinue再び

ふと、Seq.tryFindの変な使い方を思いついちゃいました。
ループのbreakとcontinueっぽいものを表現するのに利用できるのではないか、と。


例えば、こんな風に書けます。「do! continue' else」のところがカッコワルイのはご愛嬌。

open System

printfn "%s" "----- for"
let hoge = 
  let x = ref "/(^o^)\"
  loop {for i in [1..10] do
          if i = 5 then
            printfn "%s" "five"
            do! continue' else 
          if i = 2 then
            printfn "%s" "two"
            do! continue' else
          printfn "%d" i
          if i = 7 then
            printfn "%s" "!!!"
            x := "\(^o^)/"
            return break'
            printfn "%d" i
          printfn "%s" "!" }
  !x
hoge |> printfn "%s"

printfn "%s" "----- while"
let fuga = 
  let x = ref "/(^o^)\"
  loop {let i = ref 0
        while !i < 6 do
          i := !i + 1
          if !i = 5 then
            printfn "%s" "five"
            do! continue' else
          if !i = 2 then
            printfn "%s" "two"
            do! continue' else
          printfn "%d" !i
          if !i = 7 then
            printfn "%s" "!!!"
            x := "\(^o^)/"
            return break'
            printfn "%d" !i
          printfn "%s" "!"}
  !x
fuga |> printfn "%s"
Console.ReadLine () |> ignore


実行結果

----- for
1
!
two
3
!
4
!
five
6
!
7
!!!
\(^o^)/
----- while
1
!
two
3
!
4
!
five
6
!
/(^o^)\

LoopBuilder

Seq.tryFindの使い方が変ですw optionの使い方が変ですw

// へぼいループビルダー
type LoopBuilder () =
  let while' gd body = 
    (fun _ ->
      let b = gd() 
      if b then
        if Option.isSome (body ()) then Some ()
        else body () |> (fun _ -> None)
      else 
        Some ()) |> Seq.initInfinite
  member this.While(gd,body) =
      while' gd body |> Seq.tryFind (fun x -> Option.isSome x) |> ignore
  member this.For (s, f) =
      s |> Seq.tryFind (fun x -> Option.isSome (f x)) |> ignore
  member this.Zero () = None
  member this.Combine (a,b) = a |> function
    |Some x -> Some x
    |_ -> b()
  member this.Return (x) = x 
  member this.ReturnFrom (x) = Some x 
  member this.Bind (m,f) = m |> function
    |Some x -> f x |> Some
    |_ -> None
  member this.Delay f = f 
  member this.Run f = f ()

let break' = Some ()
let continue' = None
let loop = LoopBuilder ()


わー!まったく難しいことをしていないシンプルな実装で、ループのbreakとcontinueな動作を表現できちゃったっぽいよ?
でも、やっぱり「do! continue' else」の部分がトテモカコワルイ。誰得かと(´・ω・`)


Imperative computation builder

ちゃんとカッコヨクやりたい人は、tomaspさんの「 Imperative computation builder 」あたりをあたった方が間違いなくよいです。
http://tomasp.net/blog/imperative-ii-break.aspx
http://fssnip.net/40



お知らせ

第61回CLR/H勉強会(TechParty2011)で、F# MVPのぶひささん( @nobuhisa_k )と、ASP.NET MVPさかもとさん( @jsakamoto )と、「F#パネルディスカッション 2011」に登壇します。
F#の魅力についてお話したいと思います。MVPな方2名にフルボッコにされる予定です。オフラインまたはオンラインでぜひぜひご参加ください。


CLR/H 公式ページ
http://clr-h.jp/

TechParty2011
http://techparty2011.iinaa.net/