PureScriptでビンゴしようよ！！！！

# はじめに

こちらの[rubyでビンゴしようよ！！！！](https://qiita.com/nebakei713/items/d7f515103c9ff7e71ff4)の記事が面白かったので、これを参考に、PureScriptでビンゴゲームを作ってみました。

ビンゴゲームを作るという課題は、元々は[ブログ記事](https://blog.jnito.com/?page=1557867252)でRuby初心者向けに提案されていました。課題の内容はRubyに限定したものではなく一般的なものなので、いろいろな言語で挑戦してみると面白いのではないかと思います。また、リンク先のブログ記事では、他にも課題があげられていたので、興味のある方は腕試しにぜひ挑戦されてみると面白いと思います！

# 設計

課題自体はシンプルなので、いろいろ作り方はあると思うのですが、せっかくなので、なるべく見通しの良い作りにしたいと思いました。
Rubyの[元の記事](https://qiita.com/nebakei713/items/d7f515103c9ff7e71ff4)のコメントを読むと、MVCというか、ビンゴカードを表現するモデルのロジックと、その表示処理を分離して作ることが推奨されています。
これをPureScriptに当てはめた場合、そもそも表示処理はEffectになるので、わりと自然に分離した作りになるように思うのですが、それだけでなく、乱数の発生もEffectになるため、全体的に綺麗な作りになる気がします。

PureScript自体については、[こちらの記事](https://qiita.com/hiruberuto/items/2316b58162cfec150460)がとても参考になります。

### マスの扱い

ビンゴのマスには1〜75までの数字が入りますが、中央だけはFreeになっています。そこで、マスを表現するデータ構造として`Box`を導入しました。

```haskell
data Box = BoxFree | BoxInt Int
```

Maybeと同じ構造なのですが、Maybeとは区別してShowクラスのインスタンスにしたいため、このようにしました。

```haskell
instance showBox :: Show Box where
  show (BoxInt v) = show v
  show BoxFree = ""
```

### カードの扱い

カードは単純に、Boxを5x5に並べたものとしました。(定義では、5というサイズはなくて、単なる配列の配列ですが）

```haskell
type BingoCard = Array (Array Box)
```

### 乱数の処理

ビンゴカードを作る場合、例えば１列目を作るには、1〜15の数を作成して、そこからランダムに5つ取り出す必要があります。Rubyでは、Arrayクラスのオブジェクトにsomeという便利なメソッドがあるようなのですが、PureScriptはないようなので、次の二段階の方法で作成することにしました。

- 配列をシャッフルする関数 `shuffle`

```haskell
shuffle :: forall a. Array a -> Effect (Array a)
shuffle input = do
  pairs <- traverse (\i -> (numWithRandom (length input) i)) input
  let shuffledPairs = sortWith _.second pairs
  pure $ _.first <$> shuffledPairs
  where
  numWithRandom :: forall a. Int -> a -> Effect { first :: a, second :: Int }
  numWithRandom len i = do
    r <- randomInt 1 (len*10)
    pure $ { first: i, second: r }
```

任意の型aの配列をランダムに並び替えます。アルゴリズムとしては、各要素に対してRandomな自然数を割り当て、その値でソートしているだけです。ランダムの値が重複しづらい方がよいので、配列の長さの10倍の値で乱数を発生させています。乱数を使っているので、戻り値は`Effect (Array a)`型です。

これを使えば、1〜15の数の中から5つ取り出すのは、`take 5 $ shuffle (1..15)` のように表現出来ます。

# 実装

### メイン処理

メインとなる実装部分は、ビンゴカードを作り出す`generateCard`関数です。

```haskell
generateCard :: Effect BingoCard
generateCard = do
  card' <- sequence $ map (\x -> do
    let nums = range (x*15+1) (x*15+15)
    randNums5 <- map (map BoxInt) $ take 5 <$> shuffle nums
    pure 
      if (x == 2)
      then
        case updateAt 2 BoxFree randNums5 of
        Just randNums5' -> randNums5'
        Nothing -> randNums5
      else 
        randNums5
  ) (0..4)
  pure $ transpose card'
```

`randNum5`は2次元配列で、1行目に1〜15の中からランダムに5つ選んだ配列、２行目に16〜30の中からランダムに5つ選んだ配列、、、、。という感じに格納されます。

中心部分を`BoxFree`とするために、`updateAt`を使っています。この`updateAt`は配列の特定の要素を更新する関数ですが、結果が`Maybe`なので、case文で取り出しています。この条件では、`Nothing`は呼ばれることはないのですが、ダミーとして何も更新しない配列を返しています。

また、最後に`transpose`で行と列を入れ替えています。これも、Rubyでは配列にそういったメソッドがあるようなのですが、PureScriptではないようなので、別途実装しています。

### 表示処理

表示もちょっと悩みました。Rubyのように文字列を整形する命令があまりないようなので、`rjust`という補助関数(`rjust n`で、長さ`n`になるように空白を加える関数。コード全体に記載)を用意して、次のように実装しました。

```haskell
showCard :: BingoCard -> String
showCard card = intercalate "\n\n" $ (intercalate "|") <<< (map $ (rjust 3) <<< show ) <$> card
```

`intercalate`は他の言語でいう`join`みたいなもので、セパレータ文字列を与えて、文字列の配列をくっつけてくれます。`card`が2次元配列なので、`map`を2回使って(一つは明示的な`map`、もう一つは`<$>`)で、内部のBoxにアクセスして、合成関数`(rjust 3) <<< show`を適用しています。

### コード全体

補助関数も含めたコード全体は次のようになりました。(PureScript 0.12)

```haskell
module Bingo where

import Prelude
import Effect
import Effect.Console (log)
import Effect.Random
import Data.Maybe
import Data.Array
import Data.Traversable
import Data.Ordering
import Data.String as S

shuffle :: forall a. Array a -> Effect (Array a)
shuffle input = do
  pairs <- traverse (\i -> (numWithRandom (length input) i)) input
  let shuffledPairs = sortWith _.second pairs
  pure $ _.first <$> shuffledPairs
  where
  numWithRandom :: forall a. Int -> a -> Effect { first :: a, second :: Int }
  numWithRandom len i = do
    r <- randomInt 1 (len*10)
    pure $ { first: i, second: r }

transpose :: forall a. Array (Array a) -> Array (Array a) -- assuming all rows are same length.
transpose matrix =
  case (sequence $ map head matrix) of
    Just y -> case (sequence $ map tail matrix) of
      Just ys -> y : (transpose ys)
      Nothing -> []
    Nothing -> []

space :: Int -> String
space 0 = ""
space n = " " <> space (n-1)

rjust :: Int -> String -> String
rjust max str =
  if max < S.length str 
  then str 
  else (space (max - S.length str) ) <> str

data Box = BoxFree | BoxInt Int

instance showBox :: Show Box where
  show (BoxInt v) = show v
  show BoxFree = ""

type BingoCard = Array (Array Box)

generateCard :: Effect BingoCard
generateCard = do
  card' <- sequence $ map (\x -> do
    let nums = range (x*15+1) (x*15+15)
    randNums5 <- map (map BoxInt) $ take 5 <$> shuffle nums
    pure 
      if (x == 2)
      then
        case updateAt 2 BoxFree randNums5 of
        Just randNums5' -> randNums5'
        Nothing -> randNums5
      else 
        randNums5
  ) (0..4)
  pure $ transpose card'

showCard :: BingoCard -> String
showCard card = intercalate "\n\n" $ (intercalate "|") <<< (map $ (rjust 3) <<< show ) <$> card

main :: Effect Unit
main = do
  card <- generateCard
  log "  B|  I|  N|  G|  O\n"
  log $ showCard card
```

### 結果

実際に実行してみると、次のような結果になりました。

```
  B|  I|  N|  G|  O

 15| 21| 39| 48| 66

  8| 26| 43| 53| 65

  2| 19|   | 59| 67

  7| 27| 44| 49| 72

  6| 20| 35| 52| 69
```

# 終わりに

やはりRubyのように便利なメソッドやライブラリがたくさん用意されているのは何かと便利ですね。頑張って、PureScript用に汎用的に使えるライブラリを作りたいなと思いました。

`shuffle`を実装する中で、Haskellであればタプルを使うような処理の部分も、レコード型にしました。PureScriptにもTuple型はあるので、最初はそれを使っていたのですが、PureScriptはレコード型が便利で積極的にそれを使っていく方がPureScriptらしいので、レコード型に書き換えました。

細かいことですが、`rjust`を作るときに、第一引数を文字列にするか、長さにするか悩みました。オブジェクト指向言語のオブジェクト（Rubyでいうレシーバ)は、関数型言語でいうと第一引数に割り当てるのが良さそうな気がするのですが、それだとクロージャとして`rjust n`のように使えないので、ちょっと不便ではないかと思い、第一引数を文字列の長さにしてみました。

設計自体は、関数型言語での設計がまだよくわかっていませんが、データ構造を作り、なるべく汎用的に使える関数を作って組み合わせていく、、、ということを意識してみたのですが、なかなかまだ感覚がつかめていないです。ぜひ、アドバイスなどコメントをいただけたらと思います。