はじめに
自分のHaskell力の低さを痛感している今日このごろ。
仕事で使う言語はHaskellではないものの、複数のプログラミング・パラダイムに触れているということはプラスに働くと考えているため、学習の機会は設けておきたいもの。
しかし自分はLazyで学習を先送りにしてしまうタイプ。怠慢、ストイックさの欠如。
これは由々しき自体です。どうすれば良いのか。
対策として、覚えたい言語に触れる機会を創出してしまうというのがあると思いますが、ちょうど以下の記事にインスパイアされて個人用のSlack BOTを作ってライフハックしてみようかと思っていたので今回の開発ターゲットとしました。
まずはゆるく始める
構成
ひとまずは、以下のように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を用いることとしました。
候補としては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 --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の中身をまるっと以下で置き換えます。
{-# 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 --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にデプロイします。
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で書くに改めてインスパイアされつつ、サブプロセスでHaskellの処理を呼ぶ形式にトライしてみました。
サブプロセス式: ローカルで動かすまで
$ stack new haskellbot
$ cd haskellbot
$ yo hubot
Bot Adapterはslack
とします。
HaskellとHubotのコードが混ざり合うのが微妙な気がしますが…いったんこれで進めます。
app/Main.hsを以下の内容にします。
module Main where
import Data.Char(toUpper)
main :: IO ()
main = interact (map toUpper)
ビルドします。
$ stack build
試しに実行してみましょう。
$ stack exec haskellbot-exe
ここで入力した半角英字が大文字になれば成功です。
続いて、scripts/haskell.coffeeを作成します。
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
内容は以下を参考に。
そしてhaskell.coffeeを次のようにしたら、ローカルでは動作確認ができました。
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力以外にも上げなければならないパラメータがあるようです。