Slack BOTから始めるHaskell

# はじめに

自分のHaskell力の低さを痛感している今日このごろ。

仕事で使う言語はHaskellではないものの、複数のプログラミング・パラダイムに触れているということはプラスに働くと考えているため、学習の機会は設けておきたいもの。

しかし自分はLazyで学習を先送りにしてしまうタイプ。怠慢、ストイックさの欠如。
これは由々しき自体です。どうすれば良いのか。

対策として、覚えたい言語に触れる機会を創出してしまうというのがあると思いますが、ちょうど以下の記事にインスパイアされて個人用のSlack BOTを作ってライフハックしてみようかと思っていたので今回の開発ターゲットとしました。

[割と本気で家庭用Slack Botを作ってみた](http://blog.8arrow.org/entry/2016/01/13/183349)

# まずはゆるく始める

## 構成

ひとまずは、以下のようにSlackからHubotを介してHaskellサーバにアクセスするようにしたいと思います。

```
[Slack] <-> [Hubot Server] <-> [Haskell Server]
```

実際Hubotは不要なのですが、今回はSlackのReal Time  Messaging APIよりもHaskellを覚えたいというのが正直なところで、その部分はHubotに担ってもらおうという魂胆です。


イメージとしては、Hubot ServerがL7のルータのような感じでしょうか。
また、Hubotのプラグインに手頃なものがあれば簡単に取り入れられます(そしてHaskell学習が捗りませんね!)。

ひとまず無料にしておこうということで、Hubot、Haskellの各サーバにはHerokuを用いました。

また、Haskellのフレームワークには[Spock](https://www.spock.li/)を用いることとしました。
候補としてはYesodが主流なのでしょうか。
しかし今回はシンプルなもので始めたかったということと、ScottyのチュートリアルがStackではなく生Cabalを使っていたので、Spockにしました。


## やったこと

### Haskell側 基本のスタンバイ

#### まずはHello Worldまで

https://www.spock.li/tutorial/ を見つつ進めました。
ので、しばらく上記チュートリアルを日本語にして端折った内容が続きます。

```
$ stack new Spock-example
```

しばらく待ちます。

できあがったら、`Spock-example`ディレクトリに移動します。
そしてstack.yamlおよびSpock-example.cabalに依存関係としてSpockを追加します。

```diff
diff --git a/Spock-example.cabal b/Spock-example.cabal
index ff3741e..b93ee1a 100644
--- a/Spock-example.cabal
+++ b/Spock-example.cabal
@@ -25,6 +25,9 @@ executable Spock-example-exe
   ghc-options:         -threaded -rtsopts -with-rtsopts=-N
   build-depends:       base
                      , Spock-example
+                     , Spock >=0.11
+                     , mtl
+                     , text
   default-language:    Haskell2010

 test-suite Spock-example-test
diff --git a/stack.yaml b/stack.yaml
index c1fe192..4ffd28b 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -37,6 +37,13 @@ resolver: lts-7.14
 # will not be run. This is useful for tweaking upstream packages.
 packages:
 - '.'
+- location:
+      git: https://github.com/agrafix/Spock.git
+      commit: 77333a2de5dea0dc8eba9432ab16864e93e5d70e
+  subdirs:
+    - Spock
+    - Spock-core
+    - reroute
 # Dependency packages to be pulled from upstream that are not in the resolver
 # (e.g., acme-missiles-0.3)
 extra-deps: []

```

いったんビルドします。

```
$ stack build --fast --pedantic
```

README.mdがない、みたいな警告が出ますがスルーします。

そして、app/Main.hsの中身をまるっと以下で置き換えます。


```hs
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Web.Spock
import Web.Spock.Config

import Control.Monad.Trans
import Data.Monoid
import Data.IORef
import qualified Data.Text as T

data MySession = EmptySession
data MyAppState = DummyAppState (IORef Int)

main :: IO ()
main =
    do ref <- newIORef 0
       spockCfg <- defaultSpockCfg EmptySession PCNoDatabase (DummyAppState ref)
       runSpock 8080 (spock spockCfg app)

app :: SpockM () MySession MyAppState ()
app =
    do get root $
           text "Hello World!"
       get ("hello" <//> var) $ \name ->
           do (DummyAppState ref) <- getState
              visitorNumber <- liftIO $ atomicModifyIORef' ref $ \i -> (i+1, i+1)
              text ("Hello " <> name <> ", you are visitor number " <> T.pack (show visitorNumber))
```

そして以下のコマンドで再びビルドした上でサーバを起動します。

```
$ stack build --fast --pedantic
$ stack exec Spock-example-exe
```

動作確認してみます。

```
$ curl http://localhost:8080
Hello World!
$ curl http://localhost:8080/hello/test
Hello test, you are visitor number 1
```

というわけでHello Worldに成功しました。


### Herokuにデプロイする

とりあえず無料で稼動させたいので、とりあえずのHerokuです。

今回はbuildpackとして https://github.com/mfine/heroku-buildpack-stack を使うことにしました。

```
$ heroku create --buildpack https://github.com/mfine/heroku-buildpack-stack.git
```

続いて、Herokuで動作するようにコードを書き変えます。

```diff
diff --git a/app/Main.hs b/app/Main.hs
index cfda0b5..5d8c433 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -8,6 +8,7 @@ import Control.Monad.Trans
 import Data.Monoid
 import Data.IORef
 import qualified Data.Text as T
+import System.Environment

 data MySession = EmptySession
 data MyAppState = DummyAppState (IORef Int)
@@ -16,7 +17,8 @@ main :: IO ()
 main =
     do ref <- newIORef 0
        spockCfg <- defaultSpockCfg EmptySession PCNoDatabase (DummyAppState ref)
-       runSpock 8080 (spock spockCfg app)
+       port <- getEnv "PORT"
+       runSpock (read port::Int) (spock spockCfg app)

 app :: SpockM () MySession MyAppState ()
 app =
```

Procfileの中身はこうします。

```
web: Spock-example-exe
```

コミットしたらHerokuにpushします。

```
$ git push heroku master
```

動作確認をします。

```
$ heroku open
```

ブラウザに Hello World! と表示されれば成功です。


### Hubot側 基本のスタンバイ

Hubot、Heroku、Slack等のキーワードでググるといろいろ出てくるのでその情報で何とかします(雑)。
Slack連携まで行い、Slack上で`hubot ping`と打って`PONG`と返るところまで進めればOKです。

そして、`scripts/haskell.coffee`として以下の内容を記述し、改めてHerokuにデプロイします。

```coffeescript
module.exports = (robot) ->
  robot.respond /hs/i, (msg) ->
    robot.http(先ほどのHaskellサーバのトップページURL)
    .get() (err, res, body) ->
      if res.statusCode is 200
        msg.send body
      else
        # エラーハンドリングは上手いことやっておく
        msg.send 'haskell server has something wrong'
```

これでSlackから`hubot hs`と打ち、`Hello World!`と返れば実験は成功です。
Haskellのバックエンドを持ったHubotが生まれました。

ここまで来れば、後はどうとでもなるでしょう。


# しかし納得できなかったので

やはりHubot、Haskellのプロセス間通信がサーバをまたぐ必要性はないなと。

今回のアイディアのインスパイア元ブログの別記事である、[HubotスクリプトをCommon Lispで書く](http://blog.8arrow.org/entry/2015/12/25/080000)に改めてインスパイアされつつ、サブプロセスでHaskellの処理を呼ぶ形式にトライしてみました。


## サブプロセス式: ローカルで動かすまで

```
$ stack new haskellbot
$ cd haskellbot
$ yo hubot
```

Bot Adapterは`slack`とします。

HaskellとHubotのコードが混ざり合うのが微妙な気がしますが…いったんこれで進めます。

app/Main.hsを以下の内容にします。

```hs
module Main where

import Data.Char(toUpper)

main :: IO ()
main = interact (map toUpper)
```

ビルドします。

```
$ stack build
```

試しに実行してみましょう。

```
$ stack exec haskellbot-exe
```

ここで入力した半角英字が大文字になれば成功です。

続いて、scripts/haskell.coffeeを作成します。

```coffeescript
module.exports = (robot) ->
  robot.respond /hello/i, (res) ->
    spawn = require('child_process').spawn
    stack = spawn('stack', ['exec', 'haskellbot-exe'])
    stack.stdout.on('data', (data) ->
      res.send data
      )
    stack.stdin.write(res.message.text)
    stack.stdin.end()
```

ではBOTから実行してみましょう。

```
$ bin/hubot
```

そして`haskellbot hello`と打ち、`HASKELLBOT HELLO`と返ってくれば成功です。


## Herokuへのデプロイは悔しい結果に

結論から言うと、動くところまで到達できないままアドベントカレンダーの期日を迎えてしまいました。
Docker力とStack力の低さが足を引っ張る形に。これはリベンジが必要です。

完成していないということでここから先を公開するかは正直迷ったのですが…とりあえず記録だけ残してきます。

*これを読んだあなた。どうか真相を暴いてください。それだけが私の望みです。*

### 最初はビルドパックでやってみた

作業としては、以下のコマンドを打ってみました。

```
$ heroku create --buildpack https://github.com/mfine/heroku-buildpack-stack.git
$ heroku buildpacks:add heroku/nodejs
$ git push heroku master
```

するとこんなエラーが。

```
bin/hubot: 5: bin/hubot: npm: not found
```

少し色々やってみたんですが、`node`と`npm`にパスが通っていない印象でした。
heroku-buildpack-stackを外すとこのエラーが出なくなるので、ビルドパック内でパス周りの何かが起きているのでしょう。


### そしてContainer Registryへ

というわけでDockerで攻めてみることにしました。
HerokuのContainer Registryを使うわけです。

これならHeroku固有の技術というわけでもありませんし、他のプラットフォームへの移行も視野に入ることでしょう。

まずはこんなDockerfileを用意しまして

```
FROM node:4-onbuild

ENV LANG            C.UTF-8

RUN echo 'deb http://ppa.launchpad.net/hvr/ghc/ubuntu trusty main' > /etc/apt/sources.list.d/ghc.list && \
    echo 'deb http://download.fpcomplete.com/debian/jessie stable main'| tee /etc/apt/sources.list.d/fpco.list && \
    # hvr keys
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F6F88286 && \
    # fpco keys
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C5705533DA4F78D8664B5DC0575159689BEFB442 && \
    apt-get update && \
    apt-get install -y --no-install-recommends cabal-install-1.24 ghc-8.0.1 happy-1.19.5 alex-3.1.7 \
            stack zlib1g-dev libtinfo-dev libsqlite3-0 libsqlite3-dev ca-certificates g++ git && \
    rm -rf /var/lib/apt/lists/*

ENV PATH /root/.cabal/bin:/root/.local/bin:/opt/cabal/1.24/bin:/opt/ghc/8.0.1/bin:/opt/happy/1.19.5/bin:/opt/alex/3.1.7/bin:$PATH

CMD stack setup
CMD stack build
CMD bin/hubot -a slack
```

内容は以下を参考に。

* https://hub.docker.com/_/node/
* https://hub.docker.com/_/haskell/

そしてhaskell.coffeeを次のようにしたら、**ローカルでは**動作確認ができました。

```coffeescript
module.exports = (robot) ->
  robot.respond /hello/i, (res) ->
    spawn = require('child_process').spawn
    # ここを変更
    # before: stack = spawn('stack', ['exec', 'haskellbot-exe'])
    stack = spawn('.stack-work/install/x86_64-linux/ghc-8.0.1/8.0.1/bin/haskellbot-exe')
    stack.stdout.on('data', (data) ->
      res.send data
      )
    stack.stdin.write(res.message.text)
    stack.stdin.end()
```

`spawn`のところで、`stack exec haskellbot-exe`とせずにパスを指定しないと上手く動かなかったという。
この辺で技術的な怪しさが漂っていますね。

ではHerokuにデプロイです。

```
$ heroku plugins:install heroku-container-registry
$ heroku container:login
$ heroku container:push web
```

結果としては、`hubot ping`には反応してくれたんですが、肝心のHaskellのプロセスを立ち上げるところでエラーになりました。

そしてここでタイムアップ。
無念ですが、Haskell力以外にも上げなければならないパラメータがあるようです。