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

ステップアップでわかるコンピュテーション式。TryWith や TryFinally などの実装にぜひ活用したい Delayと Run

全国1億2千万人の F# ファンの皆様いかがお過ごしでしょうか。理解できるわけもないとわかっていながらも調子に乗って「型システム入門 プログラミング言語と型の理論」を買ってしまった系の痛いおじさんです。10年後、20年後にわかることができてたらいいなくらいのノリで読んでいます。が、早くも挫折の兆しです。歩道の雪はまだまだ残ってるながらも極寒の地もようやく春めいてきました。最近やけに眠いです。春眠暁をなんとやら。プライベートな時間に勉強をするモチベーションがあまりないので、最近は映画観たりゲームばっかりやっています(下り坂)。


自分が理解していることについてちょっと書いてみたい

わたしの誤字ツイートがかなり恥ずかしい感じですが、こんなやり取りがありました。


F# 専用の DBアクセスライブラリ「Tranq」を開発なさっている、ナイス F#er のなかむらさん(@nakamura_to)にリアクションを頂いたので、ある程度の前提知識がある方向け(?)にちょっと書いてみます。ガブさん(@gab_km)の「できる!コンピュテーション式」あたりを理解していることが望ましいです。モナド的なお話はあまりありません。モッナード側面のお話はいげ太さん(@igeta)がしてくれるらしい(?)


Delay と Run を使ったほうがなぜ楽になるのか。その答えは「なぜ Delayメソッドがコンピュテーション式の基本メソッドとして提供されているのか。」の理由を紐解くとおのずと見えてきます。結論から言うと、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分などは、コンピュテーション式そのものが評価された後で評価されて欲しいからです。その答えはF#の言語仕様およびコンパイラソースコードの中にあります。Delay には、他のメソッドとは少し違う特徴があります。この記事では Maybeモナドを例に、コンピュテーション式が標準でサポートする各メソッドはなんのために用意されているのか? その明確な意味について、順を追ってゆる〜く見て行きたいと思います。「ステップアップでわかるコンピュテーション式」的な何かです。


Step1 : 背景にモナドを持つ Bind と Return メソッド


Bind と Return メソッド、その背景にはモナドがあります。
ですが、"モナドとはまったく関係なく自由に利用することができるメソッドである" ということをあらかじめ申し上げておきます。



くどいと思われる方は読みとばしていただいて結構です。Delay と TryWith 関連の詳細な話題は Step5 以降で扱っています。

  • Step2 : 一手間を省く ReturnFromメソッド

  • Step3 : コンピュテーション式の流れをコントロールする Delay と Combineメソッド

  • Step4 : 「if式のelseを省略したい!」 Zeroメソッド

  • Step5 : 「try...with 式を使いたい!」 TryWithメソッド

  • Step6 : 「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の評価を遅延する

  • Step7 : 最終的なコンピュテーション式の型を決定する Runメソッド

  • Step8 : 「try...finaly式を使いたい!」TryFinallyメソッド

  • 寄り道 : F#3.0のコンパイラのソースコードを読もう

  • Step9 : 「use 束縛を使いたい!」Usingメソッド

  • Step10 : 「while...do式を使いたい!」Whileメソッド

  • Step11 : 「for...do式を使いたい!」Forメソッド

  • コンピュテーション式で Maybeモナド を表現すると、以下のようになります。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
    
      let maybe = new MaybeBuilder()
    

    いわゆる「ビルダークラス」と呼ばれるクラスを定義し、そのインスタンスを使ってコンピュテーション式として使えるようにしたものです。たとえば以下のように Maybe の文脈の計算を簡単に、しかも読みやすく書くことができるようになります。


      maybe { 
        let! a = Some 15
        let! b = Some 20
        return a + b }
      |> printfn "%A" 
      // Some 35
    
      maybe { 
        let! a = Some "F#!"
        return a + a }
      |> printfn "%A" 
      // Some "F#!F#!"
    

    Bindメソッドは、コンピュテーション式の let! および do! に対して呼び出されるものです。let! キーワードで値を束縛して次の計算に渡したり、do! キーワードで処理を行えるようになります。Returnメソッドは、コンピュテーション式の return に対して呼び出されるものです。Maybeモナド のようにコンテナ(ここではOption型)を扱うような場合、いわゆるコンテナに値を包むような操作を定義します*1。Bindメソッドは、HaskellMonad の(>>=)演算子に。Returnメソッドは、HaskellMonad の return に対応するように意識されて用意されたものです。




    Haskell Monad の定義を見てみましょう。

    class Monad m where
       (>>=) :: m a -> (a -> m b) -> m b
       return :: a -> m a
    

    コンピュテーション式 (F#) - MSDNライブラリのページでは、Bind と Retrun のシグネチャは以下のように示されています。
    http://msdn.microsoft.com/ja-jp/library/vstudio/dd233182.aspx


    引数と対応付けて書くと、こうですね。

    builder.Bind(m:M<'T>, f:('T -> M<'U>)) : M<'U> 
    builder.Return(a:'T) : M<'T> 
    

    もちろんMaybeBuilderはこのシグネチャにそっています。

      type MaybeBuilder() =
        member b.Bind(m:option<'a>, f:('a -> option<'b>)) : option<'b> = Option.bind f m
        member b..Return(a:'a) : option<'a> = Some a
    

    F# には Haskellでいう型クラスはありません。ですから、Haskell の m a と F# の M<'T> はまったく同じものを意味するわけではありませんが、
    コンピュテーション式が提供された背景として、HaskellMonad があることがわかります。



    コンピュテーション式は、モナドを表現するのにとても都合がいいように設計されています。でも実際には、Bind や Return などのコンピュテーション式で利用可能な各メソッドシグネチャにそのような制約はありません。ビルダークラスの各メソッドの定義には、引数の数のみが制限されます。なので、わりかし自由度の高い計算の構築ができるようになっています。制限がゆるいおかげでいろいろ好き勝手ができます。コンピュテーション式はモナドだけのための構文ではないのです。


      type HogeBuilder() =
        member b.Return(a:int->int) : int -> int = a
        member b.Bind(m:int -> int, f: (int -> int)-> (int -> int)) = f m
      let hoge = new HogeBuilder()
    
      hoge { 
        let! a = fun x -> 5 + x
        let! b = fun x -> 20 - x
        return a >> b  }
      |> fun f-> f 0 |>  printfn "%A" 
      // 15
    


    自由とはときとして不自由である。という話もあります。



    Step2 : 一手間を省く ReturnFromメソッド

    ReturnFrom は定義をしなくても別段困ることはありませんが、あると便利です。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
    
        // add
        member b.ReturnFrom(m) = m
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        return! None }
      |> printfn "%A" 
      // <null>None
    
      maybe { 
        let! a = Some "F# is fun!"
        return a }
      |> printfn "%A" 
      // Some "F# is fun!"
    
      maybe { 
        return! Some "F# is fun!" }
      |> printfn "%A" 
      // Some "F# is fun!"
    


    一度 Bind (!let)で受けてから、Return(return)をするという手間を省くことができるようになります。
    コンピュテーション式をより書きやすくするためにあるもの。という位置づけのものと考えて差し支えないでしょう。



    Step3 : コンピュテーション式の流れをコントロールする Delay と Combineメソッド

    コンピュテーション式で、「if式を使いたい!」というモチベーションが発生したら、Combineメソッドを実装しましょう。
    コンピュテーション式の式の流れをコントロールしたい場合は、Combineメソッドを実装しましょう。その名の意味のとおり、計算式を結合するためのものです。


    ※コメントでNobuhisaさん(@nobuhisa_k)からツッコミをいただきました。ありがとうございます!


    修正前、Combineメソッドを実装していないと、コンピュテーション式の中で if式そのものが利用できないという誤解を与える記述がありましたが、それは誤りです。
    if式をただ使うだけであれば、Combineメソッドを実装する必要はありません。以下のように利用することができます。


      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        let! c = Some "C#"
        let! fs = Some "F#"
        let! vb = Some "VB"
        let! cpp = Some "C++"
    
        if (fs > vb) && (vb > c) then
          return vb
        elif c > vb then
          return c
        else
          return fs}
      |> printfn "%A" 
      // Some "F#" 
    


    しかし、if式の後にも式を続けて記述したい場合はどうでしょう?






    if 式だけで式の流れが完結している場合は問題ありませんが、コンピュテーション式の中で式の流れの制御が必要になった場合、Combineメソッドの実装が必要となります。
    これは if式の利用に限ったことではなく、極端な話、以下のような記述をするためには Combineメソッドを実装する必要があります。





      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
    
        // add
        member b.Combine(x, y) = x |> ignore; y
    
      let maybe = new MaybeBuilder()
    



    これは一体どういうことえだってばよ!?
    F#3.0の言語仕様によると、Combineはコンパイラによって以下のように展開されるので、Delayが必須だということですね。

    T(ce1; ce2, V, C, q) = C(b.Combine({| ce1 |}0, b.Delay(fun () -> {| ce2 |}0)))


    6.3.10Computation Expressions - The F# 3.0 Language Specification
    http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = x |> ignore; y
    
        // add
        member b.Delay (f) = f()
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        let! c = Some "C#"
        let! fs = Some "F#"
        let! vb = Some "VB"
        let! cpp = Some "C++"
    
        if (fs > vb) && (vb > c) then
          return vb
        elif c > vb then
          return c
        else
          return cpp
        return fs
        }
      |> printfn "%A" 
      // Some "F#" 
    
      maybe { 
        let! c = Some "C#"
        let! fs = Some "F#"
        let! vb = Some "VB"
        let! cpp = Some "C++"
    
        return c
        return vb
        return cpp
        return fs}
      |> printfn "%A" 
      // Some "F#" 
    


    コンピュテーション式の計算式の流れは、上から下へ(左から右へ)流れています、その流れを制御するのが Combineメソッドです。Combineメソッドは、Haskell の MonadPlus の mplus にあたるものとして解釈される場合もありますが、コンピュテーション式の中の式の流れを制御するためのものと理解するとよいでしょう。



    Step4 : 「if式のelseを省略したい!」 Zeroメソッド


    コンピュテーション式で、「if式のelse以下を省略したい!」というモチベーションが発生したら、Zeroメソッドを実装しましょう。



      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = x |> ignore; y
        member b.Delay (f) = f()
    
        // add
        member b.Zero() = None
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        if false then
          return "F#" }
      |>  printfn "%A" 
      // <null>None
    
      maybe { 
        if true then
          return "F#" }
      |> printfn "%A" 
      // Some "F#" 
    

    コンピュテーション式の計算式の流れのなかで式が省略されたとき、None が流れていることがわかります。Zeroメソッドは、Haskell の MonadPlus の mzero の意味として解釈される場合もありますが、コンピュテーション式においては、上(左)の計算式が省略された場合に利用される既定値を表すものと理解するとよいでしょう。


    Step5 : 「try...with 式を使いたい!」 TryWithメソッド


    コンピュテーション式で、「try...with 式を使いたい!」というモチベーションが発生したら、TryWithメソッドを実装しましょう。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = x |> ignore; y
        member b.Delay(f) = f() 
        member b.Zero() = None
    
        // add
        member b.TryWith (m, hander) = try m with ex -> hander ex
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        let! x = None
        try 
          return x / 0
        with
        | e -> return 0
      }
      |> printfn "%A" 
      // <null>None
    
      maybe { 
        let! x = Some 10
        try 
          return x / 0
        with
        | ex -> printf "%s" ex.Message 
                return 0
      }
      |> printfn "%A" 
      // 例外が発生するがキャッチできない
    


    「0 で除算しようとしました。Some 0」 と出力されてされて欲しいところですが、コンピュテーション式そのものの評価がされる前に、0除算が先に評価されてしまっている。Delay、TryWith いずれのメソッドシグネチャも、MSDN に書いてある通りに実装したのに、この有様です。それもそのはず。「member b.Delay(f:unit-> option<'a>) = f() 」を見れば明らか。そもそも Delay されていた処理を、即時評価してしまっているのですから、こうなります。




    builder { cexpr } は、どのように展開されるか? これについては、本家の F#3.0 の言語仕様を参照しましょう。


    6.3.10Computation Expressions - The F# 3.0 Language Specification
    http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835


    Combineメソッドのときもそうでしたが、コンピュテーション式がコンパイラによって変換されたときに、自動的に Delay メソッドが挿入されるタイミングが他にもいくつかある。それが以下のとおり、

    T(e, V, C, q) where e : the computation expression being translated
              V : a set of scoped variables
              C : continuation (or context where “e” occurs,
                up to a hole to be filled by the result of translating “e”)
              q : Boolean that indicates whether a custom operator is allowed

    T(while e do ce, V, C, q) = T(ce, V, lv.C(b.While(fun () -> e, b.Delay(fun () -> v))), q)

    T(try ce with pi -> cei, V, C, q) =
    Assert(not q); C(b.TryWith(b.Delay(fun () -> {| ce |}0), fun pi -> {| cei |}0))

    T(try ce finally e, V, C, q) =
    Assert(not q); C(b.TryFinally(b.Delay(fun () -> {| ce |}0), fun () -> e))

    T(ce1; ce2, V, C, q) = C(b.Combine({| ce1 |}0, b.Delay(fun () -> {| ce2 |}0)))


    これが示しているのは、while...do 式に対応する、Whileメソッド。try...with 式に対応するTryWithメソッド。try...finally式に対応するTryFinallyメソッド。そして、式の流れをコントロールする Combineメソッドの4つの式を変換するときに、暗黙的にDelayメソッドの呼び出しが挿入されることを意味している。F# は正確評価の言語なので、「member b.Delay(f:unit-> option<'a>) : unit -> option<'a> = f 」という様に、この時点では評価をせずにそのまま式を遅延した状態を維持しないと、コンピュテーション式そのものが評価される前に「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分が評価されてしまうので、うまくない。ということです。MSDNで示されているシグネチャは、標準的なモナドベースで書かれていて、実用的なコンピュテーション式の書き方については言及していない感があり、わかりにくいところがあります。


    これを踏まえて、次のステップへ行ってみましょう。


    Step6 : 「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の評価を遅延する

    Step5 を踏まえて、次のように実装を変更してみましょう。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Zero() = None
    
        // modify
        member b.Delay(f:unit -> option<'a>) = f
        member b.TryWith (c:unit -> option<'a>, hander:exn -> option<'a>) = try c() with ex -> hander ex
        member b.Combine(x:option<'a>, y:unit -> option<'a>) = if Option.isSome x then x else y()
    
      let maybe = new MaybeBuilder()
    


    TryWithメソッドだけではなく、Combineメソッドにも変更が必要ということに注目です。
    Delayメソッドの中で処理の遅延をそのままにするかわりに、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分で、unit を適用して評価するように実装します。TryWith では、式が上から下(左から右)へ流れます。その式が Combineメソッドでも遅延されているので、このとき評価するように実装します。


      let a : unit -> option<int> = maybe { 
        let! x = None
        try 
          return x / 0
        with
        | e -> return 0
      }
      a |> printfn "%A" 
      // <fun:a@16>
      a () |> printfn "%A" 
      // <null>None
    
      let b : unit -> option<int> = maybe { 
        let! x = Some 10
        try 
          return x / 0
        with
        | ex -> printf "%s" ex.Message
                return 0
      }
      b |> printfn "%A" 
      // <fun:b@26>
      b () |> printfn "%A" 
      // 0 で除算しようとしました。Some 0
    


    となります。try...with式が思った通りの挙動をするようになりました。
    しかし、Delay によって unit を引数にとる関数になったままだと、どうも具合がよくありません。これは、Step7 で解決します。



    Step7 : 最終的なコンピュテーション式の型を決定する Runメソッド

    Step5 で、評価が遅延されるべき計算は、Delayメソッドによって包まれている(unitを引数にとる関数として)ことがわかりました。
    しかし、遅延されたままだと具合がよくありません。そこで、Runメソッドの出番です。



    6.3.10Computation Expressions - The F# 3.0 Language Specification
    http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835

    let b = builder-expr in b.Run (<@ {| cexpr |}C >@)


    F#3.0 の言語仕様にありますように、Runメソッドは、ビルダークラスが評価されるときに呼びだされます。
    つまり、「member b.Delay(f:unit-> option<'a>) = f 」に対して必要なRunメソッドの実装は「member b.Run(f:unit-> 'c) : 'c = f()」という感じになります。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = if Option.isSome x then x else y()
        member b.Zero() = None
        member b.Delay(f) = f
        member b.TryWith (c, hander) = try c() with ex -> hander ex
    
        // add
        member b.Run(f) = f() 
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        let! x = None
        try 
          return x / 0
        with
        | e -> return 0
      }
      |> printfn "%A" 
      // <null>
    
      maybe { 
        let! x = Some 10
        try 
          x / 0 |> printf "%d"
        with
        | ex -> printf "%s" ex.Message
        return "ここまで"
      }
      |> printfn "%A" 
      // 0 で除算しようとしました。Some "ここまで"
    

    ということで、このツイートで言いたかったのはこういうことでした。
    大した話でもないのにダラダラと書いてしまいました。でもまだ続きもあるのでよろしければ。



    Step8 : 「try...finally式を使いたい!」TryFinallyメソッド

    さて、try...finally式も利用できるように実装してみましょう。Step6 と Step7を踏まえれば難しくないですね。
    また、DelayメソッドとRunメソッドがどのように呼ばれているのかを確認するために、printf を入れてみました。

      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = if Option.isSome x then x else y()
        member b.Zero() = None
        member b.TryWith (c, hander) = try c() with ex -> hander ex
    
        // add
        member b.TryFinally (c, f) = try c() finally f()
    
        // modify
        member b.Delay(f) = 
          #if DEBUG
          printf "%s" "delay;"
          #endif
          f
        member b.Run(f) = f() |> fun x ->  
          #if DEBUG
            printf "%s" "run;" 
          #endif
            x 
    
      let maybe = new MaybeBuilder()
    
      maybe { 
        try 
          try
            printf "%s" "try"
          finally
            printf "%s" "finally1;"
        finally
          printf "%s" "finally2;"
        return "ここまで"
      }
      |> printfn "%A" 
      // Debug : delay;delay;delay;tryfinally1;finally2;delay;run;Some "ここまで"
      // Relese: tryfinally1;finally2;Some "ここまで"
    

    Debugモードで実行すると、「delay;delay;delay;tryfinally1;finally2;delay;run;Some "ここまで"」と出力されます。try...finally式を2回使っています。ここでDelayが2回呼ばれます。外側のtry...finally式で unit が返されますので、その後で既定値となる Zeroメソッドが呼ばれます。そのZeroメソッドで返された「None」と、 「return "ここまで"」がCombineメソッドで結合されるので、ここでも Delayが呼ばれます。Delayが呼ばれるのは、合計3回ですか? でも、実際には合計4回呼ばれています。どういうことだってばよ?




    このあたりのことは、言語仕様を読めばちゃんと書いてありますね。

    6.3.10Computation Expressions - The F# 3.0 Language Specification
    http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835

    let b = builder-expr in b.Run (<@ b.Delay(fun () -> {| cexpr |}C) >@)

    ビルダークラスに Delayメソッドが実装されているときに限り、上記のように評価されるのです。いつDelayメソッドが呼ばれるかを把握しておくことは大事です。暗黙的にDelayメソッドが呼ばれるタイミングは、Whileメソッド、TryWithメソッド、TryFinallyメソッド、Combineメソッド、Runメソッド、の5つです。これ以外にDelayメソッドが呼ばれるタイミングは、自分で明示的に呼ぶように実装した場合のみになります。ここテストに出ます!ちなみに、Runメソッドについてもビルダークラスに実装されている場合に限り呼び出しが行われます。


    寄り道 : F#3.0のコンパイラソースコードを読もう

    Step9へ行く、その前にちょっと寄り道。F#3.0のコンパイラソースコードを読もうのコーナー。



    F#3.0のコンパイラのコンピュテーション式の分析をしているところのソースコードについて、いくつかピックアップして見てみる。
    http://fsharppowerpack.codeplex.com/SourceControl/changeset/view/71313#1230866



    まずはここらへん。ふむふむなるほど。コンピュテーション式の呼び出しを生成している関数のようです。

    /// Make a builder.Method(...) call
    let mkSynCall nm (m:range) args = 
        let m = m.MakeSynthetic() // Mark as synthetic so the language service won't pick it up.
        let args = 
            match args with 
            | [] -> SynExpr.Const(SynConst.Unit,m)
            | [arg] -> SynExpr.Paren(SynExpr.Paren(arg,range0,None,m),range0,None,m)
            | args -> SynExpr.Paren(SynExpr.Tuple(args,[],m),range0,None,m)
                
        let builderVal = mkSynIdGet m builderValName
        mkSynApp1 (SynExpr.DotGet(builderVal,range0,LongIdentWithDots([mkSynId m nm],[]), m)) args m
    


    Whileメソッドを生成するとき、mkSynCall関数の引数に"Delay"が渡されているのか確認できます。ここで自動的に Delayメソッドが呼び出される構成が作られているんですね。

            | SynExpr.While (spWhile,guardExpr,innerComp,_) -> 
                let mGuard = guardExpr.Range
                let mWhile = match spWhile with SequencePointAtWhileLoop(m) -> m | _ -> mGuard
                if isQuery then error(Error(FSComp.SR.tcNoWhileInQuery(),mWhile))
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mWhile ad "While" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("While"),mWhile))
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mWhile ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mWhile))
                Some(trans true q varSpace innerComp (fun holeFill -> translatedCtxt (mkSynCall "While" mWhile [mkSynDelay2 guardExpr; mkSynCall "Delay" mWhile [mkSynDelay innerComp.Range holeFill]])) )
    

    同様に、TryWithメソッド

            | SynExpr.TryWith (innerComp,_mTryToWith,clauses,_mWithToLast,mTryToLast,spTry,_spWith) ->
                let mTry = match spTry with SequencePointAtTry(m) -> m | _ -> mTryToLast
                
                if isQuery then error(Error(FSComp.SR.tcTryWithMayNotBeUsedInQueries(),mTry))
                if q then error(Error(FSComp.SR.tcTryWithMayNotBeUsedWithCustomOperators(),mTry))
                let clauses = clauses |> List.map (fun (Clause(pat,cond,clauseComp,patm,sp)) -> Clause(pat,cond,transNoQueryOps clauseComp,patm,sp))
                let consumeExpr = SynExpr.MatchLambda(true,mTryToLast,clauses,NoSequencePointAtStickyBinding,mTryToLast)
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env  mTry ad "TryWith" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("TryWith"),mTry))
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mTry))
                Some(translatedCtxt (mkSynCall "TryWith" mTry [mkSynCall "Delay" mTry [mkSynDelay2 (transNoQueryOps innerComp)]; consumeExpr]))
    

    同様に、TryFinallyメソッド

            | SynExpr.TryFinally (innerComp,unwindExpr,mTryToLast,spTry,_spFinally) ->
    
                let mTry = match spTry with SequencePointAtTry(m) -> m | _ -> mTryToLast
                if isQuery then error(Error(FSComp.SR.tcNoTryFinallyInQuery(),mTry))
                if q then error(Error(FSComp.SR.tcTryFinallyMayNotBeUsedWithCustomOperators(),mTry))
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "TryFinally" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("TryFinally"),mTry))
                if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mTry))
                Some (translatedCtxt (mkSynCall "TryFinally" mTry [mkSynCall "Delay" mTry [mkSynDelay innerComp.Range (transNoQueryOps innerComp)]; mkSynDelay2 unwindExpr]))
    


    同様に、Combineメソッド

            | SynExpr.Sequential(sp,true,innerComp1,innerComp2,m) -> 
    
                // Check for 'where x > y' and other mis-applications of infix operators. If detected, give a good error message, and just ignore innerComp1
              if isQuery && checkForBinaryApp innerComp1 then 
                Some (trans true q varSpace innerComp2 translatedCtxt) 
    
              else
                
                if isQuery && not(innerComp1.IsArbExprAndThusAlreadyReportedError) then 
                    match innerComp1 with 
                    | SynExpr.JoinIn _ ->  () // an error will be reported later when we process innerComp1 as a sequential
                    | _ -> errorR(Error(FSComp.SR.tcUnrecognizedQueryOperator(),innerComp1.RangeOfFirstPortion))
    
                match tryTrans true false varSpace innerComp1 id with 
                | Some c -> 
                    // "cexpr; cexpr" is treated as builder.Combine(cexpr1,cexpr1)
                    // This is not pretty - we have to decide which range markers we use for the calls to Combine and Delay
                    // NOTE: we should probably suppress these sequence points altogether
                    let m1 = 
                        match innerComp1 with 
                        | SynExpr.IfThenElse (_,_,_,_,_,mIfToThen,_m) -> mIfToThen
                        | SynExpr.Match (SequencePointAtBinding mMatch,_,_,_,_) -> mMatch
                        | SynExpr.TryWith (_,_,_,_,_,SequencePointAtTry mTry,_) -> mTry
                        | SynExpr.TryFinally (_,_,_,SequencePointAtTry mTry,_)  -> mTry
                        | SynExpr.For (SequencePointAtForLoop mBind,_,_,_,_,_,_) -> mBind
                        | SynExpr.ForEach (SequencePointAtForLoop mBind,_,_,_,_,_,_) -> mBind
                        | SynExpr.While (SequencePointAtWhileLoop mWhile,_,_,_) -> mWhile
                        | _ -> innerComp1.Range
                    if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env m ad "Combine" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Combine"),m))
                    if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env m ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),m))
                    Some (translatedCtxt (mkSynCall "Combine" m1 [c; mkSynCall "Delay" m1 [mkSynDelay innerComp2.Range (transNoQueryOps innerComp2)]]))
    


    Step5 にあったとおりコンピュテーション式の中で、try...with式を利用するためには、TryWithメソッドに加えて、Delayメソッドも実装しなければなりませんでした。例えば TryWithメソッドがビルダークラスに実装されている場合、TryWithメソッドの呼び出しの中で Delayメソッドの呼び出しがコンパイラによって暗黙的に挟み込まれるということがわかります。「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分にちょうど挿入されます。これは「このタイミングで挿入される Delayメソッドの呼び出しを利用して処理を遅延してね!」といわんばかりです。つまり、MSDNで示されている「通常のシグネチャ」はガン無視していただいて構わないということです。


    ところで、ビルダークラスが評価されるタイミングについても、同じような挙動をするのでした。Runメソッドが実装されている場合のみ Runメソッドが呼び出されるようコンパイラさんがよろしくやってくれて、そのRunメソッドの中で、Delayメソッド呼ばれるかどうかについても、Delayメソッドが実装されているか否かによって判断されると、言語仕様に書いてありました。コンパイラさんのことは、コンパイラさんが一番よく知っているって、じっちゃんが言ってた。



    関係ありそうなところを以下にピックアップ。

    let mkSynDelay2 (e: SynExpr) =  mkSynDelay (e.Range.MakeSynthetic()) e
    
    let delayedExpr = 
            match TryFindIntrinsicOrExtensionMethInfo cenv env mBuilderVal ad "Delay" builderTy with 
            | [] -> basicSynExpr
            | _ -> mkSynCall "Delay" mBuilderVal [(mkSynDelay2 basicSynExpr)]
    
    let quotedSynExpr = 
        if isAutoQuote then 
            SynExpr.Quote(mkSynIdGet (mBuilderVal.MakeSynthetic()) (CompileOpName "<@ @>"), (*isRaw=*)false, delayedExpr, (*isFromQueryExpression=*)true, mWhole) 
        else delayedExpr
    
    let runExpr = 
        match TryFindIntrinsicOrExtensionMethInfo cenv env mBuilderVal ad "Run" builderTy with 
        | [] -> quotedSynExpr
        | _ -> mkSynCall "Run" mBuilderVal [quotedSynExpr]
    


    runExprを見ると、Runメソッドがビルダークラスに実装されている場合のみ Runの呼び出しが生成され、同じく delayedExprを見ると、Delayメソッドが実装されている場合のみ 内部にDelay の呼び出しが生成される、と。


    実際そのようになっているようです。コンパイラのソースを見れば...、いろいろとわかる(こともある)。オープンソースな F# いいね!


    Step9 : 「use 束縛を使いたい!」Usingメソッド


    少しばかり寄り道をしてしまいましたが、気を取り直して最後まで一気に駆け抜けましょう。


    「use 束縛を使いたい!」そんなあなたは、Usingメソッドを実装しましょう。use 束縛は、C#VBの using ステートメントにあたる働きをします。.NET開発者であれば、ご存知のとおり、IDisposableインターフェイスを実装しているオブジェクトについてリソースを解放をする働きがあるものです。

      open System
      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = if Option.isSome x then x else y()
        member b.Zero() = None
        member b.Delay(f) = f 
        member b.TryWith (c, hander) = try c() with ex -> hander ex
        member b.Run(f) = f()
        member b.TryFinally (c, f) = try c() finally f()
    
        // add
        member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= 
          b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose())
    
      let maybe = new MaybeBuilder()
    


    リソースの解放は、use キーワードで束縛した値のスコープが外れた場合。つまり式が最後まで評価された、あるいは例外が発生してスコープを外れた場合が考えられます。この実装には、先ほど実装した TryFinallyメソッドをそのまま利用することができます。TryFinallyメソッド使って簡単に実装しましょう。もちろん、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分には、コンパイラによって Delayメソッドの呼び出しが暗黙的に挿入されることになるので処理の呼び出しが遅延されます。


      let createDisposable f = { new IDisposable with member x.Dispose() = f() }
      maybe { 
        use res = createDisposable (fun () -> printf "%A" "Disposeされたよ;") 
        return "おわり"
      }
      |> printfn "%A" 
      // "Disposeされたよ";Some "おわり"
    
      maybe { 
        try
          use outp = IO.File.CreateText(@"C:\test\playlist.txt")
          outp.WriteLine("口がすべって")
          outp.WriteLine("君が好き")
          outp.WriteLine("言わせてみてぇもんだ")
        with ex -> printf "%A" ex
      } 
      |> printfn "%A" 
      // <null>
    


    use 束縛は、let 束縛と同じ機能が提供されているだけでなく、束縛した値がスコープの外に出ると対象のオブジェクトの Dispose が呼び出されます。
    また、コンパイラによって値の null チェックが挿入されますので、値が null の場合には Dispose の呼び出しは行われません。


    Step10 : 「while...do式を使いたい!」Whileメソッド

    コンピュテーション式で、「while...do式を使いたい!」という場合は、Whileメソッドを実装します。

      open System
      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = if Option.isSome x then x else y()
        member b.Zero() = None
        member b.Delay(f) = f 
        member b.TryWith (c, hander) = try c() with ex -> hander ex
        member b.Run(f) = f()
        member b.TryFinally (c, f) = try c() finally f()
        member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= 
          b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose())
    
        // add
        member b.While(guard:unit -> bool, f:unit -> option<'a>) =
          if not (guard()) then b.Zero() 
          else b.Bind(f(), fun _ -> b.While(guard, f))
    
      let maybe = new MaybeBuilder()
    


    実装をご覧いただくとわかるように、Zeroメソッド、Bindメソッド、Whileメソッド(自身)を呼び出しています。指定した条件が falseとなったときループを抜けて、上から下(左から右)へ計算式が流れていきます。ここで Zeroメソッドを呼び出すことで既定値の計算式を下(右)へ流しています。条件が true の場合は、上(左)から流れてきた計算式の計算結果と、以後に継続されるループを表す式を 、Bindメソッドによって束縛と関数適用を繰り返すことで計算を再帰的につなぎ合わせています。ループの実装は、対象とする文脈によって特に実装内容が大きく異なってきますが、基本的にはこのような流れで実装することになるでしょう。

      maybe { 
        let i = ref 0
        while !i < 10 do
          printf "%d;" !i 
          incr i
      }
      |> printfn "%A" 
      // 0;<null>
    
      maybe { 
        let i = ref 0
        while !i < 10 do
          printf "%d" !i 
          incr i
          return! Some (printf "%s" "個;" )
        return "ここまで"
      }
      |> printfn "%A" 
      // 0;1;2;3;4;5;6;7;8;9;Some "ここまで"
    
      maybe { 
        let i = ref 10
        while !i >= 0 do
          try
            printf "%d;" <| 10 / !i 
            decr i
            return ()
          with
          | ex -> printf "%s" ex.Message 
        return 999
      }
      |> printfn "%A" 
      // 1;1;1;1;1;2;2;3;5;10;0 で除算しようとしました。Some 999
    


    Step11 : 「for...do式を使いたい!」Forメソッド


    コンピュテーション式で、「for...do式を使いたい!」というニーズは結構多いのではないかと思います。
    これまでのStepの中で実装してきたメソッドを活かしながら Forメソッドを実装してみましょう。

      open System
      type MaybeBuilder() =
        member b.Bind(m, f) = Option.bind f m
        member b.Return(a) = Some a
        member b.ReturnFrom(m) = m
        member b.Combine(x, y) = if Option.isSome x then x else y()
        member b.Zero() = None
        member b.Delay(f) = f 
        member b.TryWith (c, hander) = try c() with ex -> hander ex
        member b.Run(f) = f()
        member b.TryFinally (c, f) = try c() finally f()
        member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= 
          b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose())
        member b.While(guard:unit -> bool, f:unit -> option<'a>) =
          if not (guard()) then b.Zero() else
          b.Bind(f(), fun _ -> b.While(guard, f))
    
        // add
        member this.For(sequence:#seq<_>, body) =
          this.Using(sequence.GetEnumerator(),
                    fun enum -> this.While(enum.MoveNext, 
                                           (fun () -> body enum.Current)))
    
      let maybe = new MaybeBuilder()
    

    シーケンスのリソースは解放する必要があるので、実装済みのUsingメソッドを利用します。ループの表現には先程実装したWhileメソッドを利用します。Usingメソッドと、Whileメソッドを利用して、Forを実装することができました。

      maybe { 
        for i in [1..5] do
          printf "%d;" i
          return "Combineで式が結合されてコレは捨てられます"
        return "おしまい"
      }
      |> printfn "%A" 
      // 1;2;3;4;5;Some "おしまい"
    
      maybe { 
        for i in [1..5] do
          return printf "%d;" i
        return "おしまい"
      }
      |> printfn "%A" 
      // 1;2;3;4;5;Some "おしまい"
    

    残るは、Yieldメソッド と YieldFromメソッドの実装。と言いたいところですが、Maybeモナドの文脈では、Yieldメソッドおよび YieldFromメソッドを実装する意味はありませんので省略します*2。ということで、すべてのステップが終了しました。これでおしまいです。以上、「ステップアップでわかるコンピュテーション式」でした。何かの参考になれば幸いです。



    おまけ:FsControlを拡張してお遊び

    Gustavo Leon氏(@gmpl)のGJ(グッジョブ)であるところの、fsharp-typeclasses
    https://code.google.com/p/fsharp-typeclasses/


    を、新しく構成しなおしたプロジェクト FsControl が面白くてニヤニヤしながらたまーに見ていたりします。
    https://github.com/gmpl/FsControl




    そのキモとなるのが、InlinHelperモジュール。

    [<AutoOpen>]
    module InlineHelper
    
    module Overloads =
        let inline instance_1 (a:^a                         ) = 
            ( ^a                                : (static member instance: ^a                     -> _) (a          ))
        let inline instance_2 (a:^a,b:^b                    ) =                                                      
            ((^a or ^b                        ) : (static member instance: ^a* ^b                 -> _) (a,b        ))
        let inline instance_3 (a:^a,b:^b,c:^c               ) =                                                          
            ((^a or ^b or ^c                  ) : (static member instance: ^a* ^b* ^c             -> _) (a,b,c      ))
        let inline instance_4 (a:^a,b:^b,c:^c,d:^d          ) =                                                          
            ((^a or ^b or ^c or ^d            ) : (static member instance: ^a* ^b* ^c* ^d         -> _) (a,b,c,d    ))
        let inline instance_5 (a:^a,b:^b,c:^c,d:^d,e:^e     ) =                                                          
            ((^a or ^b or ^c or ^d or ^e      ) : (static member instance: ^a* ^b* ^c* ^d* ^e     -> _) (a,b,c,d,e  ))
        let inline instance_6 (a:^a,b:^b,c:^c,d:^d,e:^e,f:^f) =                                   
            ((^a or ^b or ^c or ^d or ^e or ^f) : (static member instance: ^a* ^b* ^c* ^d* ^e* ^f -> _) (a,b,c,d,e,f))
    
    open Overloads
    
    type Inline = Inline with
        static member inline instance (                            ) = fun (x:'x) -> instance_1(          Unchecked.defaultof<'r>) x :'r
        static member inline instance (a:'a                        ) = fun (x:'x) -> instance_2(a        ,Unchecked.defaultof<'r>) x :'r
        static member inline instance (a:'a, b:'b                  ) = fun (x:'x) -> instance_3(a,b      ,Unchecked.defaultof<'r>) x :'r
        static member inline instance (a:'a, b:'b, c:'c            ) = fun (x:'x) -> instance_4(a,b,c    ,Unchecked.defaultof<'r>) x :'r
        static member inline instance (a:'a, b:'b, c:'c, d:'d      ) = fun (x:'x) -> instance_5(a,b,c,d  ,Unchecked.defaultof<'r>) x :'r
        static member inline instance (a:'a, b:'b, c:'c, d:'d, e:'e) = fun (x:'x) -> instance_6(a,b,c,d,e,Unchecked.defaultof<'r>) x :'r
    


    これがなんとも黒魔術的であり、メシウマ状態であり。その一方で"今の F#"の限界を感じたり。



    で、FsControl では、fsharp-typeclasses にあった、DoNotationBuilderを提供する do' が internalで宣言されていたり、DoPlusNotationBuilderとそのインスタンスを提供する doPlus がなくなっていたり、いろいろ変更が加えられている。また、FsControlというプロジェクトからは、なみなみならぬ Haskell愛 を感じるわけだけど、F# 愛の成分が不足しているように感じる。というのも、この記事で取り上げた コンピュテーション式内で利用できる for式 や try...with 式などの Haskell にはない機能についての利用は考えられていないからだ。FsControl はあくまで Haskell 的な関数型プログラミングのエミュレートを提供するという思想で作られているのかもしれない、とかなんとか。


    ということで、お遊び程度でちょっとゴニョゴニョしてみました。なんか表示が崩れてしまうので埋め込みはしません。
    https://gist.github.com/zecl/5280535


    Whileメソッド以下については、ちょっと工夫しないと厳しそうな気がします。なので宿題とします(キリッ

    *1:圏論で自然変換と呼ばれるやつ

    *2:仮に実装したとしても、Return, ReturnFromと同じになる。

    すべての F# ユーザーが今すぐ導入すべき拡張機能 F# snippet を導入しよう



    twitterfacebook こういった SNS があなたの仕事の邪魔をしている。いつ仕事をするか?その合間でしょ?(逆でしょ!)
    そんな今日この頃ですがみなさんいかがお過ごしでしょうか。 F# HACKATHON in sapporo [2013/03/23(土)] は、リモートでの参加も大歓迎らしいので、ドシドシ参加するといいと思います!



    最近、「VS 魂 100 連発」とゆー企画をマイクロソフトのエバさんたちがやっていますね。かなりゆるーい感じで超入門的な内容を中心にさまざまな観点から Visual Studio の使い方を解説してくれています。たぶんおそらくめいびー F# に関する話題はご紹介いただけないであろうことは想像に難しくなく。気まぐれではありますが VS魂単発@F# 的な何かです。すべての F# ユーザーが今すぐ導入すべき拡張機能であるところの、F# snippet を紹介したいと思います*1



    導入方法や詳細な使い方については、作者である Tao Liu氏(@ttliu2000)のブログ記事を参照されたい。と丸投げするのもアレなので駄文を書いてみます。


    F# code snippet + snippet management for Visual Studio 2012 Addon - Apollo 13 - Tao Liu's blog
    http://apollo13cn.blogspot.jp/2012/06/f-code-snippet-visual-studio-2012-addon.html



    F# snippetを使うモチベーション

    ソフトウェアを設計していく中で、定型的なコードというのは可能であれば抽象化してフレームワークやライブラリ内で解決できるのが望ましい。しかし、それができないような場合、あるいはできたとしてもそれが望ましくはない場合がある。同じパターンのコード片の記述を何度も強いられてしまうことがある。たとえば、ライブラリレベルでは実装できない IDisposableパターン。あるいは、INotifyPropertyChanged実装パターンなどはその典型。


    解決方法はいくつか考えられます。たとえば、T4テンプレートのようなコード生成のアプローチを検討する場合もあるでしょう。しかしそこまで大げさではない場合。より小さくより手軽な解決方法として、Visual Studioではコードスニペット機能が利用できます。C# あるいは VB.NETコードスニペット機能を使用した経験があるならば、その使い勝手の良さついては説明不要でしょう。定型的な構文や一般的によく利用されるコードパターンについて覚えるのが大変な場合。あるいは、テンプレート化された記述が面倒くさい場合、それらの記述コストを削減してくれます。この機能はとても役に立ちます。苦痛からの開放、そして生産性の向上に一役買ってくれます。


    しかし、F# ではこの機能を標準的に利用することができません。F# snippet を導入すればそれができます。これを使わない手はありません。じゃあいつ導入するか?今でしょ!(今出しょう太)




    ところで最近、F#ユーザーグループ@ロンドンの登録メンバーが、500名を超えたそうです。流石 F# の聖地。


    しかしながら、F# snippet の総ダウンロード数はというと...、たったの500強。あれ?
    (つд⊂)ゴシゴシ ( ゚д゚) (つд⊂)ゴシゴシ (;゚д゚)…?! …これは一体どういうことだってばよ!?




    F# snippet 導入手順

    1. Visual Studio 2012を開きます。

    2. Visual Studio の [ツール] -> [拡張機能マネージャー]を選択します。

    3. オンラインから「F# snippet」を検索して、インストールします。


    なんということでしょう。導入が簡単すぎる!


    もちろん、Visual Studio Gallery からも入手することができます。


    F# snippet - Visual Studio Gallery
    http://visualstudiogallery.msdn.microsoft.com/d19080ad-d44c-46ae-b65c-55cede5f708b



    クラウド上のパブリックなストレージにある、F#のコードスニペットたち

    Visual Studioより、[表示] -> [その他のウィンドウ] -> F# Snippetを選択すると次の画面が表示されます。



    でも、何処にどうやってログインすれば?


    クラウド上のパブリックなストレージ(Windows Azureかな?)にログインするには、 fsbugs@microsoft.com へメールをして許可を得る必要があるようです。おいおい...いきなりハードル高いってばよ!ごもっとも。こちらについては華麗にスルーしていただいて結構。とりあえずプライベートで利用できればよいので、ローカル(スタンドアローン)で利用する方法と、データーベース(SQL Server)でコードスニペットを管理して、複数人で F# のコードスニペットを共有する方法について見ていきましょう。



    F# のコードスニペットをローカル(スタンドアローン)で利用する

    F# snippet でコードスニペットをすぐに利用できるように、F# チームの方たちが作ったコードスニペットCodePlex で配布されています。


    CodePlex - F# Code Snippet
    http://fsharpcodesnippet.codeplex.com/


    F# の構文をサポートするもの。基本的なデータ構造の記述、クエリおよび TypeProvider、Windows Azure など広範囲をカバーする80以上ものスニペットを提供してくれています。Visual Studio 2012のインストールパスを見つけて、次のようなフォルダ階層を作成し、ダウンロードしたスニペットを配置しましょう。


    "C:\Program Files\Microsoft Visual Studio 11.0\FSharp\Snippets\1041\"


    1041 というのは、言語IDで 日本語版 を意味しています。英語版の場合は、1033 となります。
    これでF# のコードスニペット機能を利用できるようになりました。やったね!


    プライベートなデーターベースでコードスニペットを管理。複数人で F# のコードスニペットを共有する

    コードスニペットをチーム内で共有したいというニーズはどこにでもあります。ネットワークで共有をする?バージョン管理システムを使う?まぁいろいろとあると思いますが。F# snippet ではデータベース(SQL Server)を介して手軽にコードスニペットを共有する機能が提供されています。今回は、データベースに SQL Server 2012 Expressを使うことにします。



    SQL Server 2012 Express には、5つのエディションがありますが、今回は SQL Server Management Studio Express を含む Express with Tools を選びます。ダウンロードやインストール方法、およびリモート接続などについては、SQL Server 2012 Express のインストールを参照されたい。


    インストールが完了したら。F# snippet用のデータベースを作成して、こちらのスクリプトを実行して環境を構築しましょう。




    必要な構成が作成されました。これで準備完了です。先ほどのログイン画面からログインしてみましょう。




    接続成功です。やったね!



    自作のF#スニペットを記述する

    F# のコードスニペットを作成する方法は、基本的に C#VB.NETコードスニペットを作成する方法と同じです。C#コードスニペットと同じ要領で作成し、コードスニペットスキーマの Code要素 の Language属性 に「fsharp」と設定すればF# のコードスニペットとして機能します。例えば、printf関数を記述するための簡単なコードスニペットは、以下のように記述できます。


    printf.snippet

    <?xml version="1.0" encoding="utf-8"?>
    <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
        <CodeSnippet Format="1.0.0">
            <Header>
                <Title>printf</Title>
                <Shortcut>printf</Shortcut>
                <Description>printf に対するコード スニペット</Description>
                <Author>zecl</Author>
                <SnippetTypes>
                    <SnippetType>Expansion</SnippetType>
                </SnippetTypes>
            </Header>
            <Snippet>
              <Declarations>
                <Literal Editable="true">
                  <ID>param1</ID>
                  <ToolTip>format</ToolTip>
                  <Default>%A</Default>
                  <Function></Function>
                </Literal>
                <Literal Editable="true">
                  <ID>param2</ID>
                  <ToolTip>value</ToolTip>
                  <Default>value</Default>
                  <Function></Function>
                </Literal>
              </Declarations>
              <Code Language="fsharp"><![CDATA[printf "$param1$" $param2$ $end$]]>
                </Code>
            </Snippet>
        </CodeSnippet>
    </CodeSnippets>
    


    とても簡単ですね。早速データベースに登録してみましょう。




    登録したコードスニペットは、Snippetsタブから確認できます。



    データベースから、登録したコードスニペットをローカルにダウンロードしてみましょう。



    コードスニペットマネージャーを開きます。



    "C:\Users\\Documents\Visual Studio 2012\Code Snippets\Visual F#\My Code Snippets" に、データベースで共有しているコードスニペットがダウンロードされていることが確認できます。



    自作した printf関数のコードスニペットを共有して使えるようになりましたね。


    CodePlex では提供されていないもので独自に用意しておくと便利そうなものとしては、単純にAllowNullLiteralAttribute、AutoOpenAttribute、AutoSerializableAttributeなどを一発で入力できるものを作ってみたり。アクティブパターンや測定単位に関するスニペットを作ったりと、いろいろと考えられますね。それこそGitHubやクラウドなんかでみんなで共有できればそれはそれは素晴らしいことですね。


    好みのショートカットを割り当てる



    CodePlex で配布されているコードスニペットには、ショートカットが設定されていません。なので、初期設定のままでは「Ctrl + K, Ctrl + X」のシュートカットによる [スニペットの挿入] からしかコードスニペットが利用できません。独自にお好みのショートカットを設定しておくことでより使い易くなります。


    コードスニペットファイルを直接編集して スキーマの Shortcut要素にお好みのショートカットを設定するとよいでしょう。例えば、自動実装プロパティのコードスニペットであれば下記のように設定してみましょう。


    prop.snippet

    <?xml version="1.0" encoding="utf-8"?>
    <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
      <CodeSnippet Format="1.0.0">
        <Header>
          <Title>auto implementation property</Title>
          <Shortcut>prop</Shortcut>
          ...
    


    お好みにカスタマイズすることで非常に便利になります。
    F# はコード補完が弱いとかなんとか言われております。が、これで勝つる!



    最後に
    なお、F# snippetのバグを発見したら、twitterで直接Tao Liu氏(@ttliu2000)にコンタクトを取ると対応してくれるそうです(あるいは fsbugs@microsoft.com へメール)。


    C# 開発者のための F# 本にも期待しちゃいますね。


    F# for C# Developers



    余談ですが、「Snippet Designer」 が F# に対応してくれたらかなり強力なんですが。それには F# コードスニペット機能の公式化が不可欠なような気がしないこともなく。公式化はよ。


    Snippet Designer
    http://snippetdesigner.codeplex.com/



    それでは、快適でよりよい F# ライフをお楽しみください。

    *1:いまさら感あり

    すごいH本の素朴な確率モナド


    年末年始の連休から中五日あっての三連休で、正月ボケをぶり返してしまいそうな今日この頃ですが、いかがお過ごしでしょうか。


    すごいH本こと、書籍「すごいHaskellたのしく学ぼう!」の最後のほう、第14章「もうちょっとだけモナド」の 14.8 (P356)にて、素朴な確率モナドが紹介されています。



    すごいHaskellたのしく学ぼう!

    すごいHaskellたのしく学ぼう!

    普通、モナドは作りたいと思って作るものではありません。むしろ、とある問題のある側面をモデル化した型を作り、後からその型が文脈付きの値を表現していてモナドのように振る舞うと分かった場合に、Monadインスタンスを与える場合が多いです。


    というのが印象的で。ふむふむ確かになるほどなあという感じです。



    ぼけーっと、ただ連休をだらだらと過ごすだけなのもなんなので、正月ボケのリハビリを兼ねて何か書いておこうかなということで、これを F# で書いてみようと思います。



    確率を表現するための有理数を表す型

    数学では通常、確率はパーセント(%)ではなく、0 から 1 までの実数で表します。確率が 0 ということは絶対にありえないということであり、確率が 1 というのは確実に起こるということを意味します。確率を浮動小数点で表すのも間違いではないのですが、どうしても精度が落ちてしまう。そこで Haskell では、Rationalという分数を表すために最適な有理数を表す型があり、例えば 4分の1は、1%4 のように、分子と分母は % で区切って表現することができる。


    では、F# はどうでしょう。標準には用意されていませんが、F# では、F# PowerPack という追加ライブラリにて数学に関する様々な機能が提供されています。これを導入することで分数の表現に対応することができます(NuGetで簡単に導入することもできます)。有理数を表すことができる BigRational という型が定義されているので、それを使えます。BigRational は、Nリテラルを用いて表現することができ、4分の1は、1N/4N というように表せます。





    F# で素朴な確率モナド

    Haskellでの実装例は書籍や(Learn You a Haskell for Great Good! - For a Few Monads More)に出ている通りなので、そちらを参照されたい。



    BigRational型と FSharpx を使って、F# で素朴な確率モナドをとりえず実装してみる。

    namespace FSharpx.Monad
    
    // 素朴な確率モナド
    module Probability =
      let probMap f m = List.map (fun (x,p) -> (f x, p)) m
    
      type ProbBuilder() =
        member this.ReturnFrom(x) = x
        member this.Return(x) = [x,1N/1N]
        member this.Bind(m, f) = 
          let flatten xs = 
            let concatMap f m = List.concat( List.map (fun x -> f x) m )
            let multAll (innerxs,p) = List.map (fun (x,r) -> (x, p*r)) innerxs
            concatMap multAll xs
          flatten (probMap f m) 
            
        member this.Zero () = []
    
      let prob = new ProbBuilder()
    
      open FSharpx
      open Operators 
      let inline returnM x = returnM prob x 
      let inline (>>=) m f = bindM prob m f
      let inline (=<<) f m = bindM prob m f
      let inline (<*>) f m = applyM prob prob f m
      let inline ap m f = f <*> m
      let inline map f m = liftM prob f m
      let inline (<!>) f m = map f m
      let inline lift2 f a b = returnM f <*> a <*> b
      let inline lift3 f a b c = returnM f <*> a <*> b <*> c
      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 prob m (fun _ -> f)
      let inline (>=>) f g = fun x -> f x >>= g
      let inline (<=<) x = flip (>=>) x
    

    使ってみる。3枚のコイン(イカサマコインが1つ混入している)がすべて裏が出る確率を出す。

    module Program =
      open System
      open FSharpx.Monad.Probability
    
      type Coin = Heads | Tails 
      
      let coin = [(Heads,1N/2N); (Tails,1N/2N)]
      let loadedCoin = [(Heads,1N/10N); (Tails,9N/10N)]
    
      let flipThree = prob {
        let! a = coin
        let! b = coin
        let! c = loadedCoin
        return List.forall (function |Tails->true |_->false) [a;b;c]
      }
    
      flipThree |> printfn "%A"
    


    実行結果

    [(false, 1/40N); (false, 9/40N); (false, 1/40N); (false, 9/40N); (false, 1/40N); (false, 9/40N); (false, 1/40N); (true, 9/40N)]
    


    確率モナドによって、3枚とも裏が出る確率は、40分の9であると導きだすことができた。すごいH本と同じ結果になりましたね。めでたしめでたし。



    続いて、6面のサイコロを2回振ったとき、その出目の合計値ごとの確率を出してみる。

      let d sides = [for i in [1 .. sides] -> (i, 1N/ BigRational.FromInt(sides))]
      let dice = d 6
    
      let diceTwoSum = prob {
        let! a = dice
        let! b = dice
        return a+b
      }
      diceTwoSum |> printfn "%A"
    

    実行結果

    [(2, 1/36N); (3, 1/36N); (4, 1/36N); (5, 1/36N); (6, 1/36N); (7, 1/36N); (3, 1/36N); (4, 1/36N); (5, 1/36N); (6, 1/36N); (7, 1/36N); (8, 1/36N); (4, 1/36N); (5, 1/36N); (6, 1/36N); (7, 1/36N); (8, 1/36N); (9, 1/36N); (5, 1/36N); (6, 1/36N); (7, 1/36N); (8, 1/36N); (9, 1/36N); (10, 1/36N); (6, 1/36N); (7, 1/36N); (8, 1/36N); (9, 1/36N); (10, 1/36N); (11, 1/36N); (7, 1/36N); (8, 1/36N); (9, 1/36N); (10, 1/36N); (11, 1/36N); (12, 1/36N)]
    

    ここまでがすごいH本で書かれている範囲でできること。これから先については、読者への演習問題としている。



    上記の実行結果を見てわかるように、確率の結果がまとまっておらず、バラバラに出力されていて結果の内容がわかりにくい。これはひどい。




    できれば、

    [(false, 31/40N); (true, 9/40N)]
    


    とか

    [(2, 1/36N); (3, 1/18N); (4, 1/12N); (5, 1/9N); (6, 5/36N); (7, 1/6N); (8, 5/36N); (9, 1/9N); (10, 1/12N); (11, 1/18N); (12, 1/36N)]
    


    というように、結果が一致する事象の確率については1つにまとめて出力してくれるのが分かり易くて理想だよね、と。せっかくなので、この演習問題をやってみましょう。




    とりあえず、結果が一致する事象の確率を1つにまとめてみる

    あまり何も考えずに、とりあえず実装してみた版。

      let rec merge (k,p) xs = xs |> function
        | []  -> []
        | (k,p)::kps -> kps |> function
          | [] -> [(k,p)]
          | (k',p')::kps' ->
            if k = k' then (k,p+p')::(merge (k,p) kps')
            else (k,p)::(merge (k',p') kps)
    
      let agglomerate f pd = 
        let xs : ('b * BigRational) list = (probMap f pd) |> List.sort 
        List.foldBack merge pd xs
    
      let agg pd = agglomerate id pd 
    

    使ってみる。

      let flipThree = prob {
        let! a = coin
        let! b = coin
        let! c = loadedCoin
        return List.forall (function |Tails->true |_->false) [a;b;c]
      }
    
      flipThree |> agg |> printfn "%A"
    
      //let flipThree2 = agg <| lift3 (fun a b c -> List.forall (function |Tails->true |_->false) [a;b;c]) coin coin loadedCoin
      //flipThree2 |> printfn "%A"
    

    実行結果

    [(false, 31/40N); (true, 9/40N)]
    


    使ってみる。

      let d sides = [for i in [1 .. sides] -> (i, 1N/ BigRational.FromInt(sides))]
      let dice = d 6
    
      let diceTwoSum = prob {
        let! a = dice
        let! b = dice
        return a+b
      }
    
      diceTwoSum |> agg |> printfn "%A"
    
      //let diceTwoSum2 = agg <| lift2 (+) dice dice
      //diceTwoSum2 |> printfn "%A"
    

    実行結果

    [(2, 1/36N); (3, 1/18N); (4, 1/12N); (5, 1/9N); (6, 5/36N); (7, 1/6N); (8, 5/36N); (9, 1/9N); (10, 1/12N); (11, 1/18N); (12, 1/36N)]
    

    うん。とりあえず動いているね。これで一応目的は達成できているのだけど、なんだか冗長な感じがするしカッコ悪い。俺が欲しいのコレジャナイ感がぱない。もっとシンプルに行きたい。



    結果が一致する事象の確率を1つにまとめる(改訂版)


    どのあたりがコレジャナイ感を出しているのか。落ち着いて先ほどの実装をよく見てみてみよう。

      let rec merge (k,p) xs = xs |> function
        | []  -> []
        | (k,p)::kps -> kps |> function
          | [] -> [(k,p)]
          | (k',p')::kps' ->
            if k = k' then (k,p+p')::(merge (k,p) kps')
            else (k,p)::(merge (k',p') kps)
    
      let agglomerate f pd = 
        let xs : ('b * BigRational) list = (probMap f pd) |> List.sort 
        List.foldBack merge pd xs
    
      let agg pd = agglomerate id pd 
    


    List.sort でソートしたリストと、ソートする前のリストとを比較して、再帰でマージしながら結果をまとめあげる実装となっている。



    そもそもここでやりたいことは、集合として確率の結果をまとめ上げたいということ。集合を扱いたい場合、F# では set が使える。また、コレジャナイ実装では、List.foldBack で入力要素を順々に受け取りながら marge 関数で確率の和を求めながら結果の状態を順次更新していっているが、set を使って集合化することができれば、集合の要素ごとの確率の和をそれぞれ算出してゆくだけでよいことになる。あ、それって List.reduce 使えばいんじゃね? となる。



    ところで、List.reduce とはなんだったのか。例えば、List.foldを用いてintのリストの和を求める場合を思い出してみよう。

    List.fold (+) 0 [1;2;3] 
    


    のように書けるのでした。育てる種となる初期値の 0 を与えて、次々にリストを畳み込むことにより、結果 6 が得られる。



    ここで、育てる種となる初期値の 0 を与えずにリストの和を求めるには、育てる種の初期値としてリストの最初の要素を採用すればよい。最初の要素と次の要素によって演算を開始するという処理を行えばよいことなる。



    こう書く事ができる。

    List.reduce (+) [1;2;3]
    

    そう、List.fold が簡易化されたものが List.reduce ということだった。



    ということで集合を扱える set と 育てる種を必要としない List.reduce を用いて実装すると次のように書ける。

      let merge f (a,x) (b,y) : 'a * BigRational = f a b, x + y
    
      let agglomerate f pd =
        let d = pd |> List.map(fun (x,_) -> x) |> set |> Set.toList 
        List.map (fun x -> List.filter(fun (y,b) -> x = y) pd |> List.reduce (merge f)) d
    
      let agg pd = agglomerate (fun _ x -> x) pd
    


    ほむ。だいぶシンプルになりました。これはリハビリをして正解でしたね。

    Mが如くF#ive 夢、叶えしMono

    この記事は、F# Advent Calendar 2012 の12日目です。ひとつ前の記事は@katayama_kさんの「スキャナが出力したPDFを、出力デバイスに合わせて変換しよう-F#マスターへの道」です。ありがたいことにソースコードを公開してくださっているので、iTextSharpを使ってPDFを扱いたい場合に参考になりそうです。








    龍が如く5、NewスーパーマリオブラザーズU、などプレイしたいゲームが盛りだくさんだけど、ぜんぜんやる時間が取れない今日この頃ですが、みなさんいかがお過ごしでしょうか。
    この記事では、F# と MonoGame でゲームを作ろう(Mac編)という話題を扱います。F#をゲーム制作に実用してみようという話です。関数型楽しい。ゲーム作るの楽しい。ならば組み合わせない手はない! いわゆるひとつの「カツカレー理論」を採用しましたが、あなたを満足させられるような 実用 や F# の 成分は少ないかもしれません。




    私がはじめて関数型言語に触れたのは5年近く前で、言語は Haskell でした。まだ関数型について右も左もわからなかった頃、Webで偶然見つけた Haskell で書かれた テトリスクローンに心が躍り、Haskellで書かれたグラディウスクローン「Monadius」 に衝撃を受けた。「いつか俺も関数型言語でなんかゲーム作る」と胸が熱くなったのを今でもよく覚えています。関数型言語は、Webアプリケーションを開発できる程度には実用です。ゲームを作れる程度には実用です。正味な話だいたいなんでも作れます。ジョジョに人気も高まってきているせいか、Scala や F# などで実際にゲームを作って楽しんでいる人も少しずつ増えてきているようです。歩みは遅くとも流れは来ている。そう感じます。ひとむかし前と比べて、スマホやタブレットPCが一般層にまで浸透してきた今、個人で作ったゲームでも手軽に大きなマーケットに公開できるインフラも整ってきました。もしあなたがお気に入りのプログラミング言語でゲームを作れたら。それってとっても実用だなって。



    MonoGameとは

    MonoGame は一言で言うと XNA4.0 のオープンソース実装です。XNAは、Windows、Xbox 360、Windows Phone向けのゲームを作ることができるマルチプラットフォーム(ただしマイクロソフト系に限る)に対応のイケているフレームワークです。実際に利用してみるとすぐにわかりますが、初心者でも手軽にゲームを作ることができるように考えられて設計されています。もちろん、初心者のみをターゲットにしているものではなく、上級者なら高度な技術力をいかんなく発揮することもできます。C#VB、F#など、いずれかの .NETの言語さえ知っていれば、すぐにゲームプログラミングに取り組める環境が整っています。実際、ゲームプログラミングもXNAも素人な私ですが、特にハマることもなくゲームを作ることができました。「とことんF#よぷよ!」は、スーパーファミコンぷよぷよ通のとことんぷよぷよモード相当のもので、F# で書いた私のXNA処女作です。


    そんなかわいい XNA ですが、 Windows8 では XNA はサポート対象外となってしまいました。この決定に、多くの XNA ゲーム開発者が涙したことは言うまでもありません。そこで救いの手を差し伸べるのが、オープンソースのゲーム開発フレームワーク MonoGame です。


    MonoGameは、内部的にはOpenGLベースでの総書き換えなので、Windowsに限らずともさまざまなOS上で動作します。前身は XNA で開発されたゲームを iOS 上で動作させる目的で開発されていた XNATouch でした。@smallgeek さんが紹介してくださった、Mono for Android のお仲間です(Mono for Android と F# - FsNinja)。現在では AndroidiOSMac OS XLinux 。そして Windows 8 ストア向けゲームの開発も可能になりました。また、今後 PlayStation Mobile をサポートする予定があり、鋭意開発中とのことです。「PSVita で F# が動くぞ...!!」が来る日も近い? 胸が熱くなる。


    MonoGame のここがいいね!

    MonoGame の素晴らしい点は、なにより XNA4.0 で使用するのと全く同じ名前空間を使用していることです。これにより、XNAAPI が完全に保たれていてます。2Dのみでなく、部分的ではあるものの3Dのサポートもされているので、理論的には XNAで書かれたプログラムを参照設定の変更や各プラットフォームに依存するゲーム起動部分(Gameクラスの起動)などに関するわずかな変更を加えるだけで、異なるプラットフォームに簡単に移植することができます。WOPE (Write Once, Play Everywhere) 「一度書けば、どこでも動く」 の御旗のもとに開発されている MonoGame を利用することで、初心者でも手軽にクロスプラットフォームに対応したゲームを作ることができます。.NET FrameworkXNA の知識や経験をそのまま使えるという恩恵を受けながら、あらゆるプラットフォーム(市場)に対してゲームを発信することができます。MonoGame には、 iOSAndroid 向けのハイクオリティなゲームが開発(あるいはXNAからの移植)されているという実績があり、十分にその有用性が確認できます。 また、MonoDevelop はもちろん、 Visual Studio での開発もサポートされています。XNA あるいは .NET開発者にとって、最も注目すべきゲーム開発フレームワークのひとつと言えるでしょう。



    この記事を書いたきっかけ

    MonoGame の F# テンプレートの作者である@7sharp9氏のツイートおよびブログの記事を見たのがきっかけです。素敵な記事を書いてくださっています。感謝。


    F# and MonoGame on the Mac
    http://7sharpnine.com/posts/Fsharp-and-MonoGame-on-the-Mac/
    現在は、簡単に導入ができるアドインが提供されているので、この記事の内容は古いものになってしまっていますが、
    MonoGameのF#テンプレートの導入方法について書かれています。初めて利用するときに参考にさせていただきました。


    MonoGame 3D Basics
    http://7sharpnine.com/posts/monogame-3d-basics/
    正四面体の描画方法について解説されています。



    とりあえず MonoGame を使ってみる (Windows8編)



    Windows環境では、MonoGameインストーラをダウンロードして、インストールします。すると、Visual Studio 向けの MonoGameのプロジェクトテンプレート(C#用)が追加されますので、それで開発することができます。「えっ?F#のテンプレートはないのかい?」まだありません。XNA4.0同様にF#向けのテンプレートは用意されていませんが、デスクトップゲームであれば、 XNA4.0 と同じ要領で 各MonoGame FrameworkのDLLを参照設定してWindows アプリケーションとして作成することで F# でMonoGameを利用したゲームを作れます。動画は以前私が F# + XNA4.0で作ったものを F# + MonoGameへ移植したものです。ソースコードの修正を必要とせず動作しました。MonoGameは Windows8 ストア向けのゲーム開発もサポートしています。しかし、 F# で開発することはできません(2012/12/12現在)。



    例の非常に残念な画像



    F# で作れる部分はあくまで Portable Class Library(F#では Portable Library)のみに限られるので、MonoGame に依存する部分は F#で書く事はできません(くやしいです!)。MonoGame のAPI に合わせたインターフェイスを Portable Library に定義して1つ被せてラップすることで、依存部分のみ C# に委譲して F# で書ける部分を増やすように作ることも可能ですが、非常に面倒くさい作業になりますし、正直やってられないので現実的な方法ではないでしょう。Portable Library で書けない部分は、素直に C# に頼りましょう。ちなみに「とことんF#よぷよ!」の例で言うと、MonoGame(XNA)に依存しない namespace PuyoPuyoLibrary(PuyoPuyo.fs) (ソースコードがある記事)の部分を Portable Libraryとして利用することができます。





    なお、VMWare (5.0.1)上のWindows8でも動作しましたが、VMWareではGPUやドライバーに大きく影響されてしまいますので、環境によっては SpriteFontがうまく描画されないようです。




    XNA4.0 から MonoGameへ 「とことんF#よぷよ!」の移植


    非常に退屈な動画




    この動画は、F#とXNAで動作していたゲームを MonoGame を使って Mac OS X上で動くように移植する様子を録画したものです。(一部、端折ってます)。開発環境を整えるところが大半で移植作業というほどの手間はなく、ソースコードの変更はごくごくわずかです。 F#とXNAで作ったゲームがこうも簡単に Mac で動いてしまったのには、感動しました。「Mac でも、Monoがあるからできるよ!」と聞いて 「それ知ってます!」とは答えることはできたものの、実際やってみるまであまりピンときませんでした。今回こうして 自分で書いたものが 実際に Mac で動くのを目の当たりにすると、なんだかとっても気分が良いものです。自分は特に何もしていないんですが、なぜか悦に浸れますw



    Mono も MonoDevelop も MonoGameもすばらしいね!



    F# と MonoGame のゲーム開発環境構築について補足

    非常に退屈な動画をご覧いただけたのなら、ほぼ説明は必要ないかもしれませんが、Macでの F# + MonoGame の開発環境の構築について補足情報を書いておきます。


    インストール前の下準備
    なにかしらの Mac を用意します(仮想環境でもよい)。


    Xcode をダウンロードします(無料)
    https://developer.apple.com/jp/technologies/tools/
    最新のものを取得するとよいでしょう。



    せっかくなので、Macの開発者登録もしておくとよいかも(無料)



    MonoDevelopをダウンロードします(無料)


    http://monodevelop.com/Download


    MonoDevelop 3.0.5(安定版) MonoDevelop 3.0.5 installer (Mac OS X) をダウンロードします
    MonoDevelop 3.0.6(Beta)でもよいです。



    ■ Mono Framework をダウンロードします(無料)


    http://www.go-mono.com/mono-downloads/download.html


    Mono + GTK# Beta Version: 3.0.2 (Mac OS X) 
    Mono MRE installer および Mono MDK installer をダウンロードします



    ■MonoGameのソースコード を GitHub から取得します(無料)
    https://github.com/mono/MonoGame
    この工程はもしかするとなくすことができるのかもしれません。



    インストールの手順

    Xcodeをインストールします
    動画では省略した部分です。
    ただダブルクリックしてもマシンにはインストールされませんので、アプリケーションフォルダにコピーしましょう。
    Windowsと勝手が違うので、Mac 初心者のわたしは最初よくわかりませんでしたよ。



    ■Mono Frameworkをインストールします。
    Mono MRE 3.0.2 installer と Mono MDK 3.0.2 installer をそれぞれインストールします。
    動画のとおりです。



    つい最近 バージョン3.0.2が出たので、ワンステップでF#をインストールできるようになりました(いいね!)。


    バージョン 3.0.1までは、F#がバンドルされていなかったので、
    GitHub から fsharpのソースコードを取得してきて、home brewを入れたりターミナルからのコマンド操作でビルドをするという、ちょっとした工程が必要でしたが、
    3.0.2からは、F#3.0 がMonoに標準装備されたので、本当にお手軽に MonoのF#の環境を作れるようになりました。ま、おかげで動画作り直しだったわけですが。



    MonoDevelop を インストールします。
    動画のとおりです。ドラッグアンドドロップしましょう。


    ■F# アドインを追加します。
    動画のとおりです。ワンクリックでバインディングできます。


    ■MonoGame テンプレートアドインを追加します。
    動画のとおりです。こちらのURLをコピーして(http://www.infinitespace-studios.co.uk/monodevelop/main.mrep)アドインを追加しましょう。
    これで F#の MonoGameプロジェクトテンプレートが利用できます。

    こちらも、わたしがMonoGameを触り始めた11月中ごろにはまだなくて、F#のMonoGameテンプレートを利用するにはちょっとした工程が必要でした。先ほどもご紹介しましたが、テンプレートの作者である @7sharp9 氏の記事で解説されてる内容を実行する必要がありました(F# and MonoGame on the Mac)。動画を見ていただけるとわかりますが、実際にテンプレートを開いてみると、バインディングがうまくいっていないように見えます。わたしの環境の問題でしょうか。直接的な解決策ではありませんが、GitHubから取得した MonoGameのMacOS向けのソリューションを、xbuildでビルドしたものを、直接参照設定することで、MonoGameを利用することができるようになります。


    これでF#でMonoGameを使ってゲームプログラミングができます。




    準備は整った!さぁShow Timeだ


    「F# と MonoGame でゲームが作れるって言われてもさ。 そもそもゲームなんて作ったことないし、何から始めたらよいかわからん。」



    ですよねー。最初はだれでもそうです。でもよく考えてみてください。そもそも仕事などで作る業務アプリケーションやWebアプリケーション。基幹システムや組み込み開発など、どんなプログラムだって初めは「作ったことのないもの」ばかりのはずです。作ったことのないものを作る楽しさ。みなさんなら知っていますよね? わたしもゲームを作るのは素人で趣味の域を脱していませんので偉そうなことは言えませんが、やってみると楽しいのでぜひ手を動かしてみてください。



    はじめてゲームを作るときは、まずは真似から始めることをおすすめします。アマチュアバンドが、好きなバンドの曲をコピーするのと同じように、それはとてもよい練習です。まずは真似から始めて、慣れてきたら自分たちでちょっとアレンジする、ゆくゆくはオリジナルもやってみたいね!みたいなね。プログラミングを楽しく上達することができます。何事もまずは真似からじゃないでしょうか。「落ちものパズルゲーム」は比較的簡単に作れるので、テトリスクローンは、しばしば練習題材に使われます。「いや俺が作りたいのはね、よくあるオーソドックスな横スクロールのアクションゲームなんだけど。その手のやつのサンプルないの?」ないこともないですが、言語を限定しないとしても、参考にできそうなものは少ないでしょう。「C#で欲しい」と言われると更に減りますし、F# でとなると非常に限定されます。




    MonoGameゲームプログラミングの基本(というかXNAと同じ!)

    MonoGame はすべてのAPI が XNA4.0 と一致するので、XNA と全く同じ感覚でゲームプログラミングができます。基本は、Gameクラスを継承したゲームを起動するためのクラスを作成し、必要な処理について、メソッドを適宜オーバーライドして実装します。各コンテンツを読み込んだり(LoadContent)、ゲームの状態を更新したり(Update)、描画(Draw)をする処理を書く、それがはじめの一歩です。



    namespace MgaGotoku
    open MonoMac.AppKit
    open MonoMac.Foundation
    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
    
    /// Mが如く
    type MgaGotokuGame () as this = 
      inherit Game()
      /// ゲームタイトル、グラフィックデバイスマネージャ、スプライトバッチ
      let gametitle, gmanager, sprite = "Mが如く", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice)
      
      /// サンドを鳴らす
      let soundPlay (se:Lazy<SoundEffectInstance>) = se.Force().Play()
      
      /// デフォルトコンストラクタ
      do 
        // Windowの横幅
        gmanager.PreferredBackBufferWidth <- 800
        // Windowの縦幅
        gmanager.PreferredBackBufferHeight <- 480
        ()
    
      /// ゲームが実行開始する前に必要な初期化処理をここに書きます。
      /// ここで必要なサービスを照会して、関連するグラフィック以外のコンテンツを読む込めます。
      override this.Initialize() =
        base.Window.Title <-gametitle;
        base.Initialize()
    
      /// LoadContentはゲームごとに1回呼び出されます。
      /// 基本的には、ここですべてのコンテンツを読み込みます。
      override this.LoadContent() =
        base.LoadContent()
    
      /// UnloadContentはゲームごとに1回呼び出されます。
      /// ここですべてのコンテンツをアンロードします。
      override this.UnloadContent () = 
        base.UnloadContent()
    
      /// ゲームの状態の更新処理をここに書きます。
      /// プレイヤーの入力判定、キャラクタの衝突判定、オーディオの再生などなど
      override this.Update(gameTime) = 
        base.Update gameTime
    
      /// ゲームの描画処理をここに書きます
      /// gameTimeは、ゲームの瞬間的なタイミング情報です。
      override this.Draw(gameTime) = base.Draw gameTime |> fun _ ->
        gmanager.GraphicsDevice.Clear(Color.CornflowerBlue)
        sprite.Force().Begin ()
        // Texture2Dであれば、ここでスプライトバッチを描画します。
        sprite.Force().End ()
    
      /// 終了時の処理をここに書きます。
      override this.EndRun () = base.EndRun(); 
    
    type AppDelegate() = 
        inherit NSApplicationDelegate()
        
        override x.FinishedLaunching(notification) =
            let game = new MgaGotokuGame()
            game.Run()
        
        override x.ApplicationShouldTerminateAfterLastWindowClosed(sender) =
            true
     
    module main =         
        [<EntryPoint>]
        let main args =
            NSApplication.Init ()
            using (new NSAutoreleasePool()) (fun n -> 
                NSApplication.SharedApplication.Delegate <- new AppDelegate()
                NSApplication.Main(args) )
            0
    


    ここからジョジョに肉付けをしていきます。プレイヤーの入力(キーボード、ゲームパッド、タッチ操作など)も簡単に取得できますので、プレイヤーの操作に合わせて2Dの画像を上下左右に動かせるようにしてみましょう。そして、SoungクラスやSoundEffectクラスを使うと*.wavや*.mp3などの音を簡単に鳴らせるので、お気に入りの曲を鳴らしてみましょう。なんだかゲームが作れそうな気になってきます!





    Mが如くF#ive 夢、叶えしMono

    ここからは、おまけです。


    さて、そろそろ寒〜い記事のタイトルについて触れなければならない。
    「また、くだらないこと考えて...」ご名答である。でも思いついちゃったんだからしょうがない。
    これを書くきっかけを与えてくれた(?)のは @gab_km さんである。

    Mが如くF#ive 夢、叶えしMono





    うん。龍が如くは無理。知ってます! 無理だとわかっていて言っていますね?いじわるだねッ☆ 無理なものは無理なんだけど、できることもあるんじゃないかと思って考えた。古き良き王道横スクロールアクション的なアレなら今からでも間に合うもしれない。上記のやり取りで何か悪い電波を受信してしまいました。人に強制されるのは嫌いだけど、自分で思いつちゃったらこれはもうしょうがない。やるしかない。



    WiiU でも新しいやつがでたばかりですが、全世界で人気の王道横スクロールアクションゲームは多くの人がそのクローンを作ろうと試みています。YouTube等の動画サイトで 「Mario C#」とか「Mario XNA」あたりで検索をすると、XNAで作ろうとしたクローンの動画がたくさんでてくる。JavaScriptで実装してみた話とかが割と有名でしょうか。私もパズルゲームやシューティングゲームは趣味でいくつか作った経験があるのだけど、アクションゲームは作ったことがなかった。時間もなかったのでそれっぽいものができるかどうか微妙なところでしたが、なんとなくそれっぽい動き(Mが如く)になりました。ぜんぜん全くF#である必要性もないし、どちらかというと「それC#で」が求められている感がある。だが、F#で書いた。この誰にも求められていない感が良いね。「龍が如く5」と、「New スーパーマリオブラザーズ U」の誘惑に負けそうになりながらも、我慢して書きました。全然時間がなくてコードをきれいに書いている余裕がなかったので質は非常にアレですが、コメントは多めに書きましたので、どうか生温かい目で見てください。加えて、「〇〇をF#で書いてみた」的なネタ記事はもっと増えてもいいと思っている。別に屁理屈を言ったり言い訳をするつもりは毛頭ないけど、そういうのも逆説的に"実用"なのではないのかなって。



    マリオの動きを真似る

    実際にやってみて改めてわかったんですが、マリオの動きや操作感はむかしから非常によく練られていたということ。操作していて楽しいようによく考えて作られている。勉強になるなあ。





    Mario.fs

    namespace MarioLibrary
    open System
    open System.Collections.Generic
    open System.Text
    open Microsoft.Xna.Framework.Content
    open Microsoft.Xna.Framework.Graphics
    open Microsoft.Xna.Framework
    open Microsoft.Xna.Framework.Audio
    
    [<AutoOpen>]
    module MarioLibrary =
      type Direction = Left | Right         // キャラの向き
      type Motion = Stop | Run | Break      // 停止、移動、ブレーキ
      type State = Stand | Squat            // 立ち、しゃがみ
      type CollisionType = Head | Foot | No // Y軸の当たり判定タイプ:頭、足、なし
    
      // 幅、立ち状態の高さ、しゃがみ状態の高さ
      let characterWidth, standHeight, squatHeight  = 32, 52, 36
       
      /// ヒゲの配管工
      [<AllowNullLiteral>]
      type Mario internal () = 
        member val Game : Game = null with get,set  // ゲームメイン
        member val X = 0. with get,set              // キャラクタのX座標を取得または設定します。
        member val Y = 0. with get,set              // キャラクタのY座標を取得または設定します。
        member val YPrev = 0. with get,set          // キャラの前回のY座標を保持
        member val Direction = Right with get,set   // キャラの方向
        member val HeadBlock = false with get,set   // 頭にぶつかったか否かを取得または設定します。
        member val Condition = Stand with get,set   // 立ち、しゃがみの状態を取得または設定します。
        member val State = Stop with get,set        // キャラの動作状態
        member val Jumping = false with get,set     // ジャンプ中か否かを取得または設定します。
        member val Air = false with get,set         // キャラクタが空中にいるか否かを取得または設定します。
        member val Jumpval = 0. with get,set        // Verlet法の加算値
        member val Dash = false with get,set        // ダッシュ状態か否か
        member val JumpUp = false with get,set      // 上昇か落下かどうか
        member val LeverVX = 0. with get,set        // 方向キーによる加速度
        member val DashVX = 0. with get,set         // ダッシュによる加速度
        member val AddjumpCnt = 0 with get,set      // Verlet法の加算カウンタ
        member val Jumpadded = 0 with get,set       // Verlet法上昇下降カウンタ
        member val Timer = 0. with get,set          // アニメーション用のタイマー
        member val Blocks = Seq.empty<Rectangle> with get,set // 当たり判定対象のブロックを取得または設定します。
    
      /// マリオをつくる
      let createMario x y game = new Mario(X = x, Y =y, YPrev = y, Game = game, Direction = Right, State = Stop, Condition = Stand)
    
      // ダッシュによる最大速度、ダッシュによる加速度、歩行による最大速度、歩行による加速度
      let dashMaxSpeed, dashAcceleration, leverMaxSpeed, leverAcceleration = 6.5, 0.08, 5.5, 0.08
    
      /// Verlet法の加算値。値を変更でジャンプの放物線を調整
      let verletArr = [ 1.0; 0.5; 0.5; 0.5; 0.5; 0.5; 0.25; 0.25; 0.25; 0.25; 0.25; 0.25; 0.15;]
    
      // 歩行アニメーション用のベースフレーム、FPS
      let baseFreamRight, baseFreamLeft, fps = 55., -55., 60.
    
      /// スペースキーを押し続けた場合の加算の割合
      let jumpAddSep = Math.Max(Math.Round((fps |> decimal) / 5m, 1), 1m)
    
      /// 1メートルを何ピクセルとするか
      let mppx = 34.
    
      /// 重力加速度
      let gravity = 9.8 * mppx
    
      /// 重力加速度 × フレーム表示間隔()2let gh2 = Convert.ToSingle(gravity * Math.Pow((1. / fps), 2.))
    
      /// キャラクタテクスチャ幅を取得します
      let width = characterWidth
    
      /// 移動アニメフレーム
      let mutable moveFrame = 0
    
      /// キャラクタテクスチャ高さを取得します
      let height (m:Mario) = m.Condition |> function 
        | Squat -> squatHeight 
        | Stand -> standHeight
    
      /// 現在のキャラクタのX軸における加速度を取得します。
      let getAcceleration (m:Mario) = m.DashVX + m.LeverVX      
    
      /// 初速度を取得します
      let getV (m:Mario) = 
        let peakY = m.Y - float (standHeight * 3)
        -1. * Math.Sqrt(2. * gravity * (m.Y - peakY))
    
      /// キャラの座標を表すRectangleを返します
      let posRect (m:Mario) px py = m.Condition |> function
        | Stand -> Rectangle (px, py, characterWidth, standHeight)
        | Squat -> Rectangle (px, py + 16, characterWidth, squatHeight)
    
      /// ジャンプ音、頭ぶつけた時の音の遅延ロード用
      let se (m:Mario)  = 
        let jumpSuper, bump = ["jump-super";"bump";] |> List.map (fun name -> m.Game.Content.Load<SoundEffect>(@"Content\sound\" + name).CreateInstance() |> fun x -> x.Volume <- 1.0f; lazy x) |> function 
          | a::b::[] -> a,b | _ -> invalidArg "tlist" "リストの長さが違うよ。"
        jumpSuper, bump
    
      /// Y軸 キャラクタと障害物の当たり判定
      let collisionY (m:Mario) (nextY:float) bumpPlay =
        let px, py = m.X |> int, (Math.Ceiling(nextY)) |> int
        let prec = posRect m px py
    
        let normalAction () = m.Air <- true
        let normal = normalAction () ; false, float py
        let footAction () = m.HeadBlock <- false; m.Air <- false
        let headAction () = 
          if bumpPlay then
            se m |> snd |> fun bump -> bump.Force().Play()
          m.HeadBlock <- true
    
        let f (brec:Rectangle) =  
          if prec.Intersects(brec) then
            // 空中
            m.Air <- true
            // 頭の衝突
            if ((prec.Top < brec.Bottom || prec.Top < brec.Bottom) && prec.Bottom > brec.Bottom) then
              true, brec.Bottom |> float, Head 
            // 足の衝突
            elif ((prec.Bottom > brec.Top || prec.Bottom > brec.Top) && prec.Top < brec.Top) then
              true, (brec.Top - standHeight) |> float, Foot
            else
              false, float py, No
          else
            false, float py, No
    
        if m.Blocks = null || m.Blocks |> Seq.length = 0 then
          false, nextY
        else
          m.Blocks |> Seq.map f
          |> Seq.tryFind(fun (x,_,_) -> x) 
          |> function
          | None -> normal
          | Some (r,y,c) -> c |> function
          | Head -> headAction(); r,y
          | Foot -> footAction(); r,y
          | No   -> normalAction(); r,y
    
      /// ジャンプ
      let jump (m:Mario) = 
        let px, py = m.X |> int, m.Y - 17. |> int
        let prec = Rectangle(px, py, width, m |> height)
        let result,_ = collisionY m (prec.Y |> float) true
        let jumpSuper, bump = se m
        if result then
          bump.Force().Play()
        else
          jumpSuper.Force().Play()
          m.JumpUp <- true
          m.Jumping <- true
    
      /// Verlet法で次のY座標を取得する
      let calcVerlet (m:Mario) = 
        let f () = 
          // キャラクターの次のY座標をVerlet法で算出
          let possibleY = 
            if m.Jumping && m.Y = m.YPrev then
              // 初速度から算出
              gravity / (Convert.ToDouble(2. * Math.Pow(fps, 2.))) + getV m  / fps + m.Y 
            else
              // 現在の加速値から算出
              m.Y + m.Y - m.YPrev + m.Jumpval
    
          // Y座標の当たり判定を行う
          let result, backY = collisionY m possibleY true
          m.YPrev <- m.Y //tempY
          if result |> not then
            possibleY
          else
            m.JumpUp <- false
            m.AddjumpCnt <- 0
            m.Jumpadded <- 0
            m.Jumping <- false
            backY
    
        if m.JumpUp then
          // 上昇中
          verletArr |> List.map (fun x -> x * -1.) |> fun varray ->
          if m.AddjumpCnt > 0 then
            m.Jumpadded <- m.Jumpadded + 1
            if (m.Jumpadded >= Seq.length varray) then
              // 最後の加算。これ以上キーを押し続けても加算しない
              m.Jumpval <- varray.[Seq.length varray - 1]
              m.AddjumpCnt <- 0
              m.JumpUp <- false
            else
              m.Jumpval <- varray.[m.Jumpadded - 1]
          m.AddjumpCnt <- m.AddjumpCnt + 1
          f ()
        else
          // 落下中
          m.JumpUp <- false
          m.AddjumpCnt <- 0
          m.Jumpadded <- m.Jumpadded + 1
          verletArr |> List.rev |> fun varray -> 
          if m.Jumpadded >= Seq.length varray then
            m.Jumpval <- varray.[Seq.length varray - 1]
          else
            m.Jumpval <- varray.[m.Jumpadded - 1]
          f()
    
      /// X軸 キャラクタと障害物との当たり判定
      let collisionX (m:Mario) nextX = 
        let normal = false, m.X
        let px = nextX |> int
        let py = m.Y |> int
        let prec = posRect m px py
    
        let f (brec:Rectangle) =  
          let slowdown () =
            m.DashVX <- m.DashVX / 1.3
            m.LeverVX <- m.LeverVX / 1.3
    
          if prec.Intersects(brec) then
            if m |> getAcceleration > 0. then
              if brec.Left < prec.Right then
                slowdown ()
                true, (brec.Left - characterWidth) |> float
              elif brec.Right < prec.Left then
                slowdown ()
                true, brec.Right |> float
              else normal
            elif m |> getAcceleration < 0. then
              if brec.Left < prec.Right then
                slowdown ()
                true, brec.Right|> float
              elif brec.Right < prec.Left then
                slowdown ()
                true, (brec.Right + characterWidth)|> float
              else normal
            else normal
          else normal
    
        if m.Blocks = null || m.Blocks |> Seq.length = 0 then
          false, nextX
        else
          m.Blocks |> Seq.map f
          |> Seq.tryFind(fun (x,y) -> x) 
          |> function
          | None -> normal
          | Some x -> x
    
      /// キャラクタの描画
      let drawMario (m:Mario) (spriteBatch : SpriteBatch) (gameTime : GameTime) = 
        /// 状態に応じたSpriteEffectを返します
        let getSpriteEffect = fun () -> m.Direction |> function
          | Left  -> SpriteEffects.FlipHorizontally // 反転
          | Right -> SpriteEffects.None             // そのまま描画
    
        /// 現在のキャラクタテクスチャの短形を取得する
        let textureRectangle () =
          let height = m |> height
          if m.Condition = Squat then 
            Nullable (Rectangle(192, 16, characterWidth, height)) // しゃがみ
          elif m.Air && m.HeadBlock then
            Nullable (Rectangle(160, 0, width, height)) // 頭ぶつけて空中
          elif m.Air && m.Jumping |> not then
            Nullable (Rectangle(64, 0, width, height)) 
          elif m.Jumping then
            Nullable (Rectangle(160, 0, width, height))
          elif m |> getAcceleration = 0. then
            Nullable (Rectangle(96, 0, characterWidth, height))
          elif m.State = Break then
            Nullable (Rectangle(128, 0, characterWidth, height)) // ブレーキしてたら
          // 加速してたら
          elif m.State = Run then
            m.Timer <- m.Timer +  getAcceleration m * 4.
            let nextFrame = ref 0.
            m.Direction |> function
            | Left  -> nextFrame := baseFreamLeft -  getAcceleration m
            | Right -> nextFrame := baseFreamRight - getAcceleration m
    
            if m.Direction = Right && m.Timer > !nextFrame || m.Direction = Left && m.Timer < !nextFrame then
              m.Timer <- 0.
              let rect = Rectangle(moveFrame * characterWidth, 0, characterWidth, height)
              moveFrame <- moveFrame + 1
              if moveFrame > 2 then
                moveFrame <- 0
              Nullable rect
            else
              Nullable (Rectangle(moveFrame * characterWidth, 0, characterWidth, height))
          else
            Nullable (Rectangle(96, 0, characterWidth, height))
    
        /// 右向きのマリオのテクスチャ遅延ロード
        let marioTexture = lazy m.Game.Content.Load<Texture2D>(@"Content\image\right") 
        let px, py = m.X |> int, m.Y |> int
        spriteBatch.Draw(marioTexture.Force(), posRect m px py, textureRectangle (), Color.White, 0.f, Vector2.Zero, getSpriteEffect(), 0.f)
    
      /// 加速度から割り出される次のフレームのX座標
      let nextX (m:Mario) = Math.Ceiling(m.X + getAcceleration m)
    
      /// Verlet法から割り出される次のフレームのY座標
      let nextY (m:Mario) = m |> calcVerlet 
    
      /// X軸移動
      let moveX (m:Mario) =
        let nextX = nextX m
        let result,x = collisionX m nextX
        if (m.Air |> not && m.Condition = Squat) then
          if result |> not then
            m.X <- nextX |> float
          else
            m.X <- x 
        elif result |> not then
            m.X <- nextX |> float
    
      /// Y軸移動
      let moveY (m:Mario) = m.Y <- nextY m |> float
    
      /// 立つ
      let standUp (m:Mario) = 
        let py = m.Y - 17.
        if m |> getAcceleration <> 0. then
          let result,_ = collisionY m py false
          if not result then
            m.Condition <- Stand
        else
          let result,_ = collisionY m py false
          if not result then
            m.Condition <- Stand
          moveX m
    
      // 緩やかに減速
      let slowdown (m:Mario) = 
        if m.LeverVX > 0. then
          if m.LeverVX - leverAcceleration < 0. then
            m.LeverVX <-  0.
          else
            m.LeverVX <- m.LeverVX - leverAcceleration
        elif m.LeverVX < 0. then
          if m.LeverVX + leverAcceleration > 0. then
            m.LeverVX <-  0.
          else 
            m.LeverVX <- m.LeverVX + leverAcceleration
        if m.DashVX > 0. then
          if m.DashVX - leverAcceleration < 0. then
            m.DashVX <- 0.
          else
            m.DashVX <- m.DashVX - leverAcceleration
        elif m.DashVX < 0. then
          if m.DashVX + leverAcceleration > 0. then
            m.DashVX <- 0.
          else
            m.DashVX <- m.DashVX + leverAcceleration
    
      /// ブレーキ
      let break' (m:Mario) =  
        m.State <- Break
        m |> slowdown 
    
      /// 最大加速度を制御します
      let getMaxAcceleration (m:Mario) = 
        if m.LeverVX > 0. && m.LeverVX > leverMaxSpeed then
            m.LeverVX <- leverMaxSpeed
        elif (m.LeverVX < leverMaxSpeed * -1.) then
            m.LeverVX <- leverMaxSpeed * -1.
        if m.DashVX > 0. && m.DashVX > dashMaxSpeed then
            m.DashVX <- dashMaxSpeed
        elif (m.DashVX < dashMaxSpeed * -1.) then
            m.DashVX <- dashMaxSpeed * -1.
    
      /// デフォルトは右に加速
      let accel (m:Mario) f d c = 
        if m.Direction = Left then
          m.State <- Break
        if not m.Jumping then
          // 空中ではない場合キャラの向きを変える
          m.Direction <- c
        if m.LeverVX < 0. |> d then
          m.DashVX <- m.DashVX + f (dashAcceleration * 1.5)
          if m.DashVX >= 0. |> d then
            if m.DashVX - f (leverAcceleration * 0.8) < 0. then
              m.DashVX<- 0. 
            else
              m.DashVX <- m.DashVX - f (leverAcceleration * 0.8)
          m.LeverVX <- m.LeverVX + f (dashAcceleration * 2.5)
          if m.LeverVX >= 0. |> d then
            m.DashVX <- 0.
        else
          if m |> getAcceleration > 0. |> d then
            m.State <- Run
          if m.Dash then
            m.DashVX <- m.DashVX + f dashAcceleration
          else
            if m.DashVX > 0. |> d then
              if m.DashVX - f (leverAcceleration * 0.8) < 0. then 
                //m.DashVX <- 0. 
                m.DashVX <- m.DashVX - f (leverAcceleration * 0.8)
          m.LeverVX <- m.LeverVX + f leverAcceleration
        m |> getMaxAcceleration
    

    Main.fs

    namespace MgaGotoku
    open MonoMac.AppKit
    open MonoMac.Foundation
    open System
    open System.Linq 
    open System.Collections.Generic
    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 Microsoft.Xna.Framework.Media
    open MarioLibrary
    
    type GameState = Playing | Die | Gameover | End
    
    /// Mが如く
    type MgaGotokuGame () as this = 
      inherit Game()
    
      /// ゲームタイトル、グラフィックデバイスマネージャ、スプライトバッチ
      let gametitle, gmanager, sprite = "Mが如くF#ive 夢、叶えしMono", new GraphicsDeviceManager(this), lazy new SpriteBatch(this.GraphicsDevice)
      /// プレイヤー
      let mutable player : Mario = Unchecked.defaultof<_>
      /// ゲームの状態
      let mutable gameState = Playing
    
      /// コイン、フロアタイル、ソリッドタイル、土管のテクスチャ遅延ロード用
      let coinTexture,floorTileTexture,solidTileTexture,greenPipeTexture, gameoverTexture = 
        ["coin";"brownFloorTile";"brownSolidTile";"greenPipe";"gameover"] 
        |> List.map (fun name -> lazy this.Content.Load<Texture2D>(@"Content\image\" + name)) |> function 
        | a::b::c::d::e::[] -> a,b,c,d,e | _ -> invalidArg "tlist" "長さが違う"
    
      /// coin取得音
      let getCoin = lazy (this.Content.Load<SoundEffect>(@"Content\sound\getcoin"))
      /// die
      let die = lazy (this.Content.Load<SoundEffect>(@"Content\sound\smb_mariodie").CreateInstance())
      let gameover = lazy (this.Content.Load<SoundEffect>(@"Content\sound\smb_gameover").CreateInstance())
    
      /// ゲームのBGM遅延ロード用
      let bgm = lazy (this.Content.Load<SoundEffect>(@"Content\sound\overworld").CreateInstance()) 
    
      /// サンドを鳴らす
      let soundPlay (se:Lazy<SoundEffectInstance>) = se.Force().Play()
    
      // 配置するフロアタイル
      let floors = [-5..15]@[17..30] |> List.map (fun x -> new Rectangle(33 * x, 452, 32, 32))
                   
      // フロアタイルの描画  
      let drawFloors () =
        // すべてのフロアタイルを描画
        floors |> List.iter (fun rect -> sprite.Force().Draw(floorTileTexture.Force(), rect,  Color.White))
    
      // 配置するブロック
      let blocks = [2..4] |> List.map (fun x -> new Rectangle(100 + 32 * x, 416 - (33 * (x - 1)), 32, 32))
      // ブロックの描画  
      let drawBlocks () =
        // すべてのブロックを描画
        blocks |> List.iter (fun rect -> sprite.Force().Draw(solidTileTexture.Force(), rect,  Color.White))
    
      // アニメーション用テクスチャRectangleを取得するクロージャー
      let animationTextureRectangle width hight max  = 
        let flame = 10
        let animeWait, counter = ref 0, ref 0
        (fun () -> 
          animeWait := !animeWait + 1
          let rect = new Rectangle(!counter * width, 0, width, hight)
          if !animeWait > flame then 
            animeWait := 0;
            incr counter
            if !counter > max then counter := 0
            Nullable rect
          else Nullable rect)
    
      // 配置するコイン
      let coins = new List<Rectangle>()  
      // コインの点滅を表現
      let coinTextureRect = animationTextureRectangle 20 28 3 
      // コインの描画
      let drawCoins () =
        // クロージャからコインのカレントテクスチャRectangleを取得
        let coinRect = coinTextureRect ()
        // すべての配置するコインを描画
        coins.ToList() |> Seq.iter (fun rect -> sprite.Force().Draw(coinTexture.Force(), new Vector2(rect.X |> float32, rect.Y |> float32), coinRect,  Color.White))
    
      // 配置する土管
      let greenPipes = [0..0] |> List.map (fun x -> new Rectangle(32 * 15, 452-77 , 48, 77))
      // 土管の描画
      let drawGreenPipes () =
        // すべての配置する土管を描画
        greenPipes |> List.iter (fun rect -> sprite.Force().Draw(greenPipeTexture.Force(), rect,  Color.White))
    
      /// コインとの当たり判定
      let checkTouchCoin =
        let wait =ref 0.
        (fun () -> 
          async { let terget = coins.ToList()
                  wait := !wait + 1.
                  if !wait > 2. then
                    let prect = new Rectangle(player.X |> int, player.Y |> int, 32, 52)
                    terget |> Seq.filter (fun coin -> prect.Intersects(coin)) 
                            |> Seq.iter (fun coin -> ignore <| getCoin.Force().Play(); ignore <| coins.Remove(coin))
                    wait := 0. } |> Async.Start)
    
      /// X座標における画面からはみ出した場合、反対側から出てくるように
      let checkWindowOut () =
        if player.X > 800. && player |> getAcceleration > 0. then
          player.X <- -32.// 画面右隅へ
        if player.X < -32. && player |> getAcceleration < 0. then
          player.X <- 800.// 画面左隅へ
    
      /// しんでしまったか判定
      let checkDie () =
        if gameState = Playing && player.Y > 510. then
          ignore <| bgm.Force().Stop()
          ignore <| die.Force().Play()
          gameState <- Die
    
      /// プレイヤーの情報を更新
      let updatePlayer () = 
        let f () = 
          player |> moveX ; player |> moveY
          checkTouchCoin >> checkWindowOut >> checkDie <| ()
        // ゲームの終了条件をチェックします。
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back = ButtonState.Pressed) then
            this.Exit()
        // ジャンプ処理
        if player.Jumping |> not then
          if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Z) && (player.HeadBlock |> not)) then
            if (player.Air |> not) then
              player |> jump
        elif player.Jumping then
          if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Z) |> not) then
            player.JumpUp <- false
    
        // 左右両方入力されていたら
        if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right) &&
            Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Left)) then
          // ブレーキ
          player |> break'; f ()
        else
          // 左右未入力なら緩やかに減速
          if ((Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right) |> not) && (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Left) |> not)) then
            if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Down)) then
              player.Condition <- Squat
              // 緩やかに減速
              player |> slowdown; f ()
            else
              player |> standUp 
              player |> slowdown; f ()
          else
            player |> standUp
            if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right)) then
              if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.X)) then
                player.Dash <- true
              else
                player.Dash <- false
              accel player id id Right
            if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Left)) then
              if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.X)) then
                player.Dash <- true
              else
                player.Dash <- false
              accel player (fun x -> -1. * x) not Left
            f ()
    
      /// デフォルトコンストラクタ
      do 
        gmanager.PreferredBackBufferWidth  <- 800 // Windowの横幅
        gmanager.PreferredBackBufferHeight <- 480 // Windowの縦幅
        this.TargetElapsedTime <- TimeSpan.FromSeconds(1./60.)
    
      /// ゲームが実行開始する前に必要な初期化処理をここに書きます。
      override this.Initialize() =
        // ウィンドウタイトルを設定
        base.Window.Title <-gametitle;
    
        // コインを配置
        let d, m, width, height = 30, 25, 20, 28
        let space = 
          let f source = [0..31] |> Seq.filter(fun x -> source |> Seq.exists (fun y -> x = y) |> not )
          seq { yield [] |> f 
                yield [10..14]@18::[20] |> f 
                yield [10;18;20;] |> f        
                yield 10::[17..21] |> f
                yield [10..13]@18::[20] |> f 
                yield 10::[17..21] |> f            
                yield [10;18;20;] |> f
                yield [10;18;20] |> f
                yield [] |> f 
                yield [] |> f } 
          |> Seq.mapi (fun i x -> x,i)
        space |> Seq.iter (fun (h,i) -> h |> Seq.iter (fun x -> coins.Add(new Rectangle(m * x, d * i, width, height))))
    
        // BGMを鳴らす
        ignore <| bgm.Force().Play() 
        base.Initialize()
    
      /// LoadContentはゲームごとに1回呼び出されます。
      /// 基本的には、ここですべてのコンテンツを読み込みます。
      override this.LoadContent() =
        player <- createMario 0. 400. this
        player.Blocks <- floors.Concat(blocks).Concat(greenPipes)
        base.LoadContent()
    
      /// UnloadContentはゲームごとに1回呼び出されます。
      /// ここですべてのコンテンツをアンロードします。
      override this.UnloadContent () = 
        base.UnloadContent()
    
      /// ゲームの状態の更新処理をここに書きます。
      /// プレイヤーの入力判定、キャラクタの衝突判定、オーディオの再生などなど
      override this.Update(gameTime) = 
        gameState |> function
        | Playing  -> 
          updatePlayer ()
        | Die -> 
          if die.Force().State = SoundState.Stopped then
            gameState <- Gameover
        | Gameover ->
            gameover.Force().Play()
            gameState <- End
        | End -> ()
        base.Update gameTime
    
      /// ゲームの描画処理をここに書きます
      /// gameTimeは、ゲームの瞬間的なタイミング情報です。
      override this.Draw(gameTime) = base.Draw gameTime |> fun _ ->
        gameState |> function
       | Playing | Die ->
          gmanager.GraphicsDevice.Clear(Color.CornflowerBlue)
          // スプライトバッチ処理開始
          sprite.Force().Begin ()
          // 各描画処理を実行
          [drawFloors; drawCoins;drawBlocks;drawGreenPipes;] |> List.iter (fun f -> f())
          // プレイヤーを描画
          drawMario player (sprite.Force()) gameTime
          // スプライトバッチ処理終了
          sprite.Force().End ()
        | Gameover | End ->
          gmanager.GraphicsDevice.Clear(Color.Black)
          sprite.Force().Begin ()
          sprite.Force().Draw(gameoverTexture.Force(), Vector2(200.f,150.f), Color.White)
          sprite.Force().End ()
    
      /// 終了時の処理をここに書きます。
      override this.EndRun () = base.EndRun()
    
    type AppDelegate() = 
        inherit NSApplicationDelegate()
        
        override x.FinishedLaunching(notification) =
            let game = new MgaGotokuGame()
            game.Run()
        
        override x.ApplicationShouldTerminateAfterLastWindowClosed(sender) =
            true
     
    module main =         
        [<EntryPoint>]
        let main args =
            NSApplication.Init ()
            using (new NSAutoreleasePool()) (fun n -> 
                NSApplication.SharedApplication.Delegate <- new AppDelegate()
                NSApplication.Main(args) )
            0
    


    マリオの歩行とダッシュ
    マリオは左右の方向キーが押下されている間、歩行状態となり特定のスピードまで加速します。左右の方向キーに加えてダッシュ用のボタンを押下していると、さらに最大スピードに達するまで加速します。というのが、横移動の基本となります。


    減速
    これは歩行とダッシュに密接に関係があるところ。加速があれば減速もある、そりゃそうだ。運動には慣性が働くもの。それを無視してしまうと非常に違和感のあるものになります。方向キーが離さてすぐにキャラクタがその場に急停止してしまうと非常に不自然です。なのでボタンが離されると緩やかに減速してから停止するように速度を調整してやる必要があります。物理エンジンでも使っていれば、この辺のことは勝手にやってくれるんでしょうが。


    他にも移動している方向と逆に方向キーを入力したとき、急ブレーキをする表現があったり、移動中にしゃがむことで緩やかに減速するなど、細かな仕様を満たす必要がある。


    マリオのジャンプの実装
    マリオのジャンプは、これまでも様々なアクションゲームで模倣されてきたと思う。ボタンの押下時間に応じてジャンプ力が変化するところなんかは、アクション性に大きな影響を与えている。あの心地よい放物線を描くジャンプは、どんな風につくられているのか? 以前、読んだマリオのジャンプ実装法とVerlet積分 - Gemmaの日記を思い出しまして、再度拝見。なるほどーシンプルな原理でつくられているんですね。とりあえずそのままやってみると、当然ジャンプはする。だが、記事のまんま実装してもマリオ的な放物線にはならない。まぁそりゃそうですよね。続編として、マリオのジャンプ実装法とVerlet積分(実践編) - Gemmaの日記もありますが、ソースコードへのはリンクは切れていた。Verlet法というのを使うといい感じにできるということがわかったので、ググってみる。すると、どうやら分子動力学に関する話題のようである。なんとなく理屈はわかったけどどう実装すれば?なるほどわからん。



    すると、そのままズバリのわかりやすい良記事を発見。ありがたい。→ ジャンプしたときの座標をVerlet法で求める - 無職のプログラミング



    上記記事では、既定値とする地面(Y座標)に操作対象のオブジェクトが接地しているときは、キャラクタに重力が働かないように実装されてる。それだと、ジャンプして上にあるブロックに乗っかることもできないし、乗っかったブロックから落下することもできない。なので、記事を参考にしつつジャンプをしていないときも常にキャラクタに重力がかかるように実装する。するとマリオ的なジャンプのソレがうまいこといく。誤解を恐れずにいうと、使う側からすると、Verlet法というのは「現在の位置と前回の位置と経過する時間から次の位置を割り出せば楽じゃん」というもの。キャラクタの位置状況を把握しやすいし便利。よくよく考えてみると、X軸方向の移動に関しても結局は時間経過による移動なので、同様の方法で実装することもできる。ただ、マリオを表現する場合、移動している方向と逆に方向キーを入力したとき、急ブレーキをする表現が必要なので、キー入力時間による加速度を制御したほうが、実装は楽かもしれません。結果オーライだった。


    コインの取得(というか、物体との当たり判定)
    MonoGame(XNA)には、矩形(四角形の位置とサイズ)を表すのに便利な Rectangle構造体 があります。Rectangle構造体 には、当たり判定(ある矩形が別の矩形に重なるかを判定)を手軽にできるIntersectメソッドが提供されています。なので、それを使えばあたり判定できる。ただし、これだけではどの方向からどの面に対してぶつかったのかの判定材料にはならないので、実際に必要な当たり判定を得るには、もう少し考慮が必要となる。判定をミスるとキャラが地面やブロックにめり込んで残念なことに。




    よくわかりませんが、だいぶMが如く感(マリオっぽさ)を出せたんじゃないだろうか。これくらい実装できてくると、だんだんと面白くなってきて次第に欲がでてくる。ステージ1−1を丸ごと実装しようかななんて脳裏に浮かんだけど、冷静に考えると非常に面倒くさそうなので1発ネタはこれにて終了。ただ、今回書いたコードはあまりにもやっつけ仕事すぎるので、余裕ができたらキレイに書き直してみたいかも。



    まとめ:つまり結局、どういうことだってばよ!?

    ドライバー オン


    モノドゥビタッチヘーンシーン! 
    モノドゥビタッチヘーンシーン! 
    モノドゥビタッチヘーンシーン! 


    モノゲーム プリーズ エー、エー! xna!


    モノデベロップ ヘーンシーン! 
    モノデベロップ ヘーンシーン! 
    モノデベロップ ヘーンシーン! 


    アバンチュ・マック・タッチゴー!


    キャモナ エフシャープ シェイクハーンズ チョーイーネ!!
    任意のOSで実行!! サイコー!!


    「さぁ、Show Timeだ」




    次は、@otfさんですね。楽しみです。