Servant と Relational Record でウェブアプリケーション開発

Servant とは

Servant は型レベルプログラミングによって、ウェブアプリとしてのインターフェースと実装との差異を防ぐことのできるウェブアプリフレームワークです。

haskell-servant.readthedocs.io

日本語記事としては lotz さんのこちらが分かりやすいので、参考にしてください。

qiita.com

Haskell Relational Record とは

Haskell Relational Record は言語内 DSL によって SQL を生成するもので、正しくない SQL に相当するものは型エラーとなります。

khibino.github.io

この2つを組み合わせることで、ユーザーからのリクエストから DB 操作を経てレスポンスの返答まで型に守られて開発ができるようになります。

この記事で解説するソースコードこのリポジトリーで公開しています。

github.com

Stack の resolver は 12.26 を使用しています。

作るもの

下記のようなインタフェースを持ったアプリを作ります。

  • /
    • HTML が返る
  • /books
    • 書籍情報のマップが JSON で返る
  • /book/<id>
    • id で指定された書籍情報が JSON で返る

/books で返る JSON は次のような形式です。

{
  "ABC of Read": {
    "name": "ABC of Read",
    "auther": "Mary"
  },
  "The Good Text": {
    "name": "The Good Text",
    "auther": "John"
  }
}

/book/1 で返る JSON は次のような形式です。

{
  "name": "The Good Text",
  "auther": "John"
}

ハンドラーの型を拡張する

DB 接続を扱うということは、ハンドラー内で接続にアクセスできないといけません。ここでは Reader モナドトランスフォーマーを Servant のハンドラーに積みます。ロガーも使うのでその上に Logging モナドトランスフォーマーも積みました。

type Handler = LoggingT (ReaderT (Pool Connection) Servant.Handler)

Api 型とそれぞれのハンドラーが定義されている場合、ハンドラー拡張前では Application 型の値は次のようになっているでしょう。この部分は積んだモナドトランスフォーマーをはがすように書きかえなければいけません。

app :: Application
app = serve api server

api :: Proxy Api
api = Proxy

server :: Pool Connection -> Server Api
server pool =
  Index.handler :<|>
  Books.handler :<|>
  Book.handler

次のように書きかえます。

makeApp :: IO Application
makeApp = do
  pool <-
    createPool
    connect
    disconnect
    1 -- stripes
    1 -- time for keeping open
    5 -- resource per stripe
  pure $ serve api $ server pool

api :: Proxy Api
api = Proxy

server :: Pool Connection -> Server Api
server pool =
  hoistServer
    api
    (flip runReaderT pool . runStdoutLoggingT)
    $ Index.handler :<|>
      Books.handler :<|>
      Book.handler

接続は再利用したいので resource-pool を用います。server 関数は hoistServer 関数を使ってモナドトランスフォーマーをはがす関数を埋め込みます。hoistServer などについてはこちらを参考にしてください。リンク先の記事は最新版の Servant 0.12 に対応させました。

qiita.com

クエリーの発行

通常は runQuery' 関数に接続を渡して使用しますが、今回作ったハンドラーは Reader モナドで接続を持っていますので接続を渡す部分を隠すことができるはずです。次のようなラッパー関数を作りました。

runQuery'
  :: (ToSql SqlValue p, FromSql SqlValue a)
  => Query p a
  -> p
  -> Handler [a]
runQuery' q p = do
  pool <- lift ask
  withResource pool $ \conn ->
    liftIO $ R.runQuery' conn q p

モジュール構成

今のところ次のようなモジュール構成にして開発しています。

  • app
    • Main.hs
      • メイン関数を持つ
  • src
    • ServantHrr.hs
      • app/Main.hs から使われるインターフェースを公開する
      • Data
        • Common.hs
          • ハンドラーを横断して使用するデータ型を定義する
        • Book.hs
          • Book ハンドラーで使用するデータ型を定義する
        • Relation
          • Book.hs
            • データベースのテーブルと 1:1 対応し、HRR で生成される定義を持つ
      • Handler
        • Common.hs
          • ハンドラーを横断して使用する関数を定義する
        • Book.hs
          • Book ハンドラーを定義する
      • Api
        • Api 型を定義する
      • DataSource.hs
        • HRR 用
        • Secret.hs
          • DB 接続用のパスワードなど
          • VCS 管理下におかない

この記事は IIJ の執務時間を使って書かれました。

Haskell Windows Ctrl-C 動作確認

コード

コードは前回記事と同じです(再掲)。

Git リポジトリーはこちら

import Control.Concurrent
import Control.Monad
import System.Exit
import System.IO
import System.Win32.Console.CtrlHandler

main :: IO ()
main = do
    tid <- myThreadId
    let
      handler event = do
        if event == cTRL_C_EVENT
          then do
            putStrLn "goodbye!"
            killThread tid
            pure True
          else
            pure False
    pHandler <- mkHandler handler
    success <- c_SetConsoleCtrlHandler pHandler True
    when (not success) $ do
      putStrLn "SetConsoleCtrlHandler failed"
      exitFailure

    let
      loop n = do
        putStr $ show n ++ ", "
        hFlush stdout
        threadDelay 1000000
        loop (n+1)
    loop 0

これを次の4通りのオプションでビルドします。

name: windows-interruption

dependencies:
- base >= 4.7 && < 5
- Win32

executables:
  app-rtsopts-threaded:
    main: Main.hs
    source-dirs: .
    ghc-options:
    - -rtsopts
    - -threaded

  app-threaded:
    main: Main.hs
    source-dirs: .
    ghc-options:
    - -threaded

  app-rtsopts:
    main: Main.hs
    source-dirs: .
    ghc-options:
    - -rtsopts

  app:
    main: Main.hs
    source-dirs: .

実行

これを実行してみます。

app

タイミングによる。

$ app
0, 1, 2, goodbye!
goodbye!
app.exe: thread killed
(応答がなくなる)
$ app
0, 1, 2, goodbye!
goodbye!
app.exe: thread killed
(終了する)
$ app
0, goodbye!
app.exe: thread killed
app.exe: warning: too many hs_exit()s

また、GHC 8.6.3 でコンパイルしたときですが、下記のようなエラーが出ることもありました。そもそも GHC 8.6.3 は Windows でのビルドにバグがあるので、他は GHC 8.4.4 を使っています。

$ app
0, 1, 2, goodbye!
goodbye!

Access violation in generated code when writing 0x20

 Attempting to reconstruct a stack trace...

app.exe: internal error: scavenge_stack: weird activation record found on stack: 5212000
    (GHC version 8.6.3 for x86_64_unknown_mingw32)
    Please report this as a GHC bug:  http://www.haskell.org/ghc/reportabug
   Frame        Code address
 * 0x484dd00    0x736b99

app-rtsopts

タイミングによる。

$ app-rtsopts
0, 1, 2, goodbye!
goodbye!
app-rtsopts.exe: thread killed
$ app-rtsopts
0, 1, 2, goodbye!
app-rtsopts.exe: thread killed
app-rtsopts.exe: warning: too many hs_exit()s

app-rtsopts +RTS --install-signal-handlers=no

--install-signal-handlers はデフォルトでは yes。

タイミングによる。

$ app-rtsopts +RTS --install-signal-handlers=no
0, 1, 2, goodbye!
$ app-rtsopts +RTS --install-signal-handlers=no
0, goodbye!
app-rtsopts.exe: thread killed
app-rtsopts.exe: warning: too many hs_exit()s
$ app-rtsopts +RTS --install-signal-handlers=no
0, 1, 2, goodbye!
goodbye!
app-rtsopts.exe: thread killed

app-threaded

想定通りの動作。

$ app-threaded
0, 1, 2, goodbye!
app-threaded.exe: thread killed

app-rtsopts-threaded

想定通りの動作。

$ app-rtsopts-threaded
0, 1, 2, 3, goodbye!
app-rtsopts-threaded.exe: thread killed

app-rtsopts-threaded +RTS --install-signal-handlers=no

想定通りの動作。

$ app-rtsopts-threaded +RTS --install-signal-handlers=no
0, 1, 2, 3, 4, 5, goodbye!
app-rtsopts-threaded.exe: thread killed

まとめ

  • 想定通りの動作
    • app-threaded
    • app-rtsopts-threaded
    • app-rtsopts-threaded +RTS --install-signal-handlers=no
  • タイミングによる
    • app
    • app-rtsopts
    • app-rtsopts +RTS --install-signal-handlers=no

タイミングによるときの動作。

  • ハンドラー → 終了する
  • ハンドラー → ハンドラー → thread killed 例外 → 応答がなくなる
  • ハンドラー → ハンドラー → thread killed 例外 → 終了する
  • ハンドラー → thread killed 例外 → too many hs_exit()s 例外 → 終了する

今後

ハンドラーが2回呼ばれるときは killThread も2回呼んでいるはずなので、その後がおかしくなることはありそう。また killThread しているが、それは自分自身ではないのかの確認もしたい。

System.Win32.Console.CtrlHandler とか GHC RTS を追うしかない気がするので追ってみます。

GHC RTS はこの辺(rts/win32/ConsoleHandler.c)からかなぁ。

Haskell で Ctrl-C を制御する(Windows)

Ctrl-C 等の割り込みの扱い方です。

tl;dr

System.Win32.Console.CtrlHandler を使います。


Ctrl-C が押されたらクロージングの処理を伴って終了するプログラムを書いてみます。

import Control.Concurrent
import Control.Monad
import System.Exit
import System.IO
import System.Win32.Console.CtrlHandler

main :: IO ()
main = do
    tid <- myThreadId
    let
      handler event = do
        if event == cTRL_C_EVENT
          then do
            putStrLn "goodbye!"
            killThread tid
            pure True
          else
            pure False
    pHandler <- mkHandler handler
    success <- c_SetConsoleCtrlHandler pHandler True
    when (not success) $ do
      putStrLn "SetConsoleCtrlHandler failed"
      exitFailure

    let
      loop n = do
        putStr $ show n ++ ", "
        hFlush stdout
        threadDelay 1000000
        loop (n+1)
    loop 0

実行してみましょう

> runhaskell Main.hs
0, 1, 2, 3, 4, 5, goodbye!
Main.hs: thread killed

ちゃんと goodbye と出力されて終了しました!killThread は例外を伴って終了するので Main.hs: thread killed というメッセージが出てしまっています。もし気になるなら例外を握りつぶすか MVar を使って終了を監視する仕組みを作るといいでしょう。

肝心の割り込みを制御する関数は c_SetConsoleCtrlHandler です。

c_SetConsoleCtrlHandler :: PHANDLER_ROUTINE -> BOOL -> IO BOOL
mkHandler :: Handler -> IO PHANDLER_ROUTINE
type BOOL = Bool
type Handler = CtrlEvent -> IO BOOL
type PHANDLER_ROUTINE = FunPtr Handler

c_SetConsoleCtrlHandler の第1引数にはコールバック関数(のポインター)を、第2引数には追加時に True を、削除時に False を指定します。

CtrlEvent の値は予め用意されていて下記があります。

cTRL_C_EVENT :: CtrlEvent
cTRL_BREAK_EVENT :: CtrlEvent

Handler の返り値は、イベントを処理して次のハンドラーにイベントを伝播させないなら True を、そうでないなら False を指定します。上の例では handler という関数にメッセージの表示とメインスレッドの停止の処理を書いて Handler として渡していました。


この記事は HaskellでCtrl-Cを制御する - Qiita のオマージュです。

Help wanted

runhaskell を使うと確かにこの動作をするのですが、ビルドして実行バイナリーを作ると Ctrl-C を2回打たないと止まりません。原因と背景を説明できる方いませんか?

> stack exec windows-interruption-exe
0, 1, 2, 3, goodbye!
goodbye!
windows-interruption-exe.EXE: thread killed

あと、Ctrl-Break だとハンドルせず終了するはずだけどしないような……

ちゃんと C のドキュメント読もう。→ 読んだ。理解は合ってそう。(追記)

議論は Haskell-jp Slack で。参加方法はこちら

Windows で Haskell iconv をビルドする

GHC 8.0 以前についてはこちらを参考に。

teratail.com

GHC 8.2 以降で stack を使う場合をここではとりあげる。

確信はないのだが、GHC 8.2 から GHC 自体が iconv に依存しなくなったのか、$(stack path --programs)\ghc-8.0.2\mingw\lib から libiconv.alibiconv.dll.a がなくなっているため GHC 8.0 以前のようにビルドができなくなっている。

なので、まず libiconv を取得する。

stack exec -- pacman -S libiconv-devel

インストールされる場所は stack がデフォルトでは見に行ってくれないので明示してやる。MSYS バージョンは将来的に変わることもあるだろう。

stack build --extra-include-dirs="$(stack path --programs)\msys2-20180531\usr\include" --extra-lib-dirs="$(stack path --programs)\msys2-20180531\usr\lib"

これでコンパイルはできるがリンクに失敗する。

`libiconv_open' に対する定義されていない参照です

iconv パッケージの cabal ファイルを編集してやる。

--- a/iconv.cabal
+++ b/iconv.cabal
@@ -26,7 +26,7 @@ library
   includes:        hsiconv.h
   include-dirs:    cbits
   c-sources:       cbits/hsiconv.c
-  if os(darwin) || os(freebsd)
+  if os(darwin) || os(freebsd) || os(windows)
     -- on many systems the iconv api is part of the standard C library
     -- but on some others we have to link to an external libiconv:
     extra-libraries: iconv

iconv を利用するプロジェクトの stack.yaml で、編集後のリポジトリーを extra-deps に指定して stack build するとビルドできる。

Leica M3

Lomography Konstruktor F・Diana Mini から始まったフィルムカメラ熱ですが一瞬で行くところまで行ってしまった感じです。

Leica M3。

現在まで続く M 型 Leica の始祖を買ってしまいました。1962年製の56歳です。

(この写真を撮ったレンズも Asahi Super Takumar 55mm F1.8 なので60年代のレンズです。)

Leica 意識前

初めはフィルムカメラの入門機としていくつかの記事に紹介されていた Asahi Pentax SP か Olympus OM-1M-1)かと考えて中古カメラ屋めぐりをしていました。

このころはひたすらサンライズカメラ*1の記事を読みあさっていました。

めぐったのは新宿・中野・秋葉原御徒町・上野の次のお店です。(抜けがあるかも。)

  • 新宿
    • マップカメラ
    • 新宿中古カメラ市場
    • 中古カメラボックス
    • ラッキーカメラ
    • アルプス堂
    • レモン社
  • 中野
  • 秋葉原御徒町・上野
    • 東京カメラ
    • 喜久屋カメラ
    • 秀光
    • 千曲カメラ

東京は中古カメラ屋さん充実してますね。

で、東京カメラで Asahi Pentax SP か Olympus OM-1 が気になるんですよという話をしていたはずなのに、店員さん(昨日カメラさん)と話していたらいつのまにかどれが至高の Leica なのかとう話になり、帰るときにはそれぞれの Leica がどう違うのか調べないといなという気持ちになっていました。(Bessa R3M 気になってるとか言った気がするからかな。)

値段でいうと Asahi Pentax SP か Olympus OM-1 が3万あればレンズ含め買えるのに対して、Leica は M 型だと少なくとも10万円で本体が買えるかどうかというレベル。

どうしてこうなった。

Leica 意識後

それから Leica が気になって、昔の雑誌などを読んで(スキャンされたのが Kindle で出版社から売ってある)、やっぱり欲しいなぁという気持ちになったので、世界の中古カメラフェアで買うことにして買いました。

露出計付きの M6 にしようかと思ったんですけど、M6 修理してくれるところがほとんどないので、修理してもらえる M3 に外付け露出計を使った方がよいと昨日カメラさんから助言をもらい、M3 にすることにしました。

東京カメラには悪いんですけど(東京カメラの買うか悩んでいたのが売れてしまっていたのもあって)、結局早田カメラで買いました。(東京カメラにはお世話になったのでまたレンズなんかを買いに行きます。)

早田カメラは自店でオーバーホールもしていて永年保証してもらえるのがすごくいいですね。

25万円以上して綺麗な見た目のものと、傷・当たりがあるけど14万円のものがあって、さすがに25万円以上は払えないなぁと14万円の方にしました。

早田さんとお話ししたところによると、5年から10年ぐらい使うとシャッター音が変わったなということがあるので、そうすると修理に持ってきてほしいとのことでした。

レンズは Elmar f = 9 cm 1:4 にしました。元々 APS-C で 55mm が好きだったのでじゃあ 90mm かなということでそうしました。Leica だと 35mm や 50mm が人気らしいですね。レンズ前玉に拭き傷と塗装剥げのせいで、無保証の B 級品の2.5万円ほどとやすいものでしたが、特に写りには問題なさそうです。

写真

フィルムは、富士フイルム 記録用 ISO 100 だったはず。

スキャン後、切り抜きだけしています。(理由は後述。)

これ以降は Lomography Color Negative 400。

使ってみて

9cm のレンズを使ったときのパララックスが結構ひどくて次の写真が分かりやすいんですが、これファインダーでは左右対称に撮ったはずなんですよね。左側に大きくずれています。それで上記の画像の何枚かは切り抜きしています。

f:id:kakkun61:20180930005458j:plain

それが分かってからは気持ち右にずらして撮っています。

あとは、レンジファインダーの二重像がちょっと上下にずれてて、それさえなければなという感じですね。これは本体を買うときにもっとお金を積むかどうかの問題ですね。

追記:上下像ずれはレンジファインダー機ならすぐ直せるようになっているらしく、数分で直してもらえました。

レンズとしてはゴーストが大きく出ますね。

なので、後で新宿中古カメラ市場でフードを買い足しました。Leica 純正フードが高いのなんの。金属の筒が1万円する。中国とかでプラスチックで作ってくれないかなという気がしてきますね。そうするとたぶん1000円もしないでしょう。量が売れないから高くなるかな?

あと、シャッター音は唯一オペラハウスでの使用が認められた静かさとのことですが、なんだか自分には腑抜けた音に思えます。別にいいんだけど。

その後

Elmar 50mm 1:2.8 が増えました。早い。

9cm 使っててやっぱり窮屈だなと思ってしまって。APS-C 55mm との差はわりとありました。これはレモン社で6.5万円ほどで買いました。(東京カメラじゃないんかい。9cmのフードを一緒に買いたくて新宿に行っちゃったので。)

50mm 使うときの問題としては、自分が眼鏡をかけているのでファインダーのブライトフレームがケラれてしまうことですね。視度補正レンズが Leica やマップカメラから出ているなんですけど -3 が限度で、どうも自分は -4 よりきついみたいなのでムリそうですね。今は眼鏡をかけてピントを合わせた後、眼鏡を上げて裸眼でぼんやり像でフレームしてます。これもうバルナックと手間変わらないな?

50mm の写真

フィルムは富士フイルム 記録用カラーフィルム ISO 400。F2.8 で ISO 400 なら夜も平気ですね。だいたい1/125秒で撮ってました。現像してみるまで本当に撮れているのか半信半疑でしたが。

夜の新宿です。

フィルムしてみて

フィルム代と現像・スキャン代が想像以上にかさむ……

やすい記録用フィルムが1本330円ほどで現像・スキャンが1本1000円。8月末にフィルムを始めてもう12本撮っていて、となると1.6万円。高え……

スキャンだけでも家でと考えてヨドバシカメラでスキャナーの話を聞いてきたんですが、スキャン時間がかなりかかる(1フレーム2分とか)みたいで断念。2分×36枚で72分しかもその間放置ではダメで作業が生じる。

デジカメでフィルムを撮るのはほこり除去が大変でおすすめしないと言われたが、実際どうなんでしょう?

しばらく撮りまくって満足したらデジカメの比率を上げることにします。

使ったフィルムは次の6種類。正直ネガは利きフィルムされてもどれがどれか分からないと思います。

使ってないけど買ってあるのは次の4種類。

さて次はどこに撮りに行きますかね?

*1:なぜ URI は sunset camera なのか?

プラモデルカメラ Lomography Konstruktor F

本来はフジヤカメラに行くことが目的だった。

カメラバカにつける薬 in デジカメ Watch」でも取り上げられてて、その前から気になってたのもあって中野に行った。

特に買うつもりもなかったので雰囲気だけ知れればいいやというようにそそくさと出たのだが、ジャンク館が中野ブロードウェイにあるということで行ってみた。

そういえば中野ブロードウェイも気になってたんだし一石二鳥だった。

するとコイデカメラがあった。Lomography Konstruktor F があった。

動くしくみを理解しながら自分で組み立てるのが好きでカメラも好きな自分が惹かれるのは当然という感じだった。

そのときは散財する予定はなかったしそのまま帰ったんだが高々4000円ぐらいだし買ってもよかったなと後から思った。

帰って Lomography を調べてその写真にやられてしまった。買うしかなくなった。

直営店 Lomography+ が末広町にあるみたいだからせっかくだしそこで買うことにしつつ、メルカリで同じく Lomography のトイカメラの Diana Mini も買った。

前書きはこの辺りにして、これが組み立てる前の Konstruktor F だ。

おしゃれな箱に入っているがちょっと雑な作りで中身の精度が心配になる。しかし、中身は削ったりお湯で曲げたりしなくてもちゃんと嵌まる精度だ。

材質はランナーパーツが ABS と POM。ランナーに付いていない部品がおそらく PS だろう。

ABS はねばりのある素材で、POM は摩擦係数の低い素材である。ABS は昔から、POM は最近のミニ四駆でも使われている。PS はプラモデルで一般的な素材で塗装がしやすい。

そう塗装だ。

ただ組み立てるだけじゃおもしろくない。せっかく自分好みにいじくれるのだから何かしたい。ということで、塗装することにした。

塗装で注意するのは、ABS と POM にはプライマーというものを事前に塗装すること。そうしないと、塗装がパラパラと剥がれてしまう。そしてそのプライマーは PS には絶対に塗装しないこと。プライマーは PS を溶かし、ひどいときには中に浸透し脆くしてしまう。

難しい場合は、PS のパーツつまりランナーに付いていない外装のパーツだけを塗装するのがいいと思う。

もう1点注意するのは、内側には塗装しないこと。これは自分が失敗してしまったんだが、塗装してしまうとギアの回りが悪くなる箇所がある。ちゃんとマスキングをしよう。自分は事前に下記の記事で注意されていることを読んだにもかかわらず雑に塗装してしまい、調子が悪い原因を特定してそこでやっと注意を思い出した。頭が悪い。

pentaxian.hatenablog.com

後は組み立てなんだが説明書が分かりやすいとはいえない。プラモデルメーカーの組み立て説明書がいかに分かりやすいのかが分かる。

しかし、説明書と現物とそのしくみを考えれば組み立て自体はすぐ終わる。

ここでも注意点があって、1つは下記の記事でも言及されているがねじを留めるところの説明が1箇所抜けている。この記事は1つ前のモデルの名前に F が付かないもので、ここで言及されている誤謬のうちそのねじ留めの箇所1点以外は改修されている。

av.watch.impress.co.jp

個人的なこのプラモデルのハイライトは巻き上げダイヤルのロック・解除機構で、こういうアイディアを実感できるところが動き物を作る醍醐味だ。とても楽しい。

しくみを言葉で説明しても、実物を見ないことには実感できないのでぜひ自分の手で作って納得の快感を得てほしい。

(B22 パーツは誰によって回されるのか?P8 パーツが P1 パーツをロックした後、それを解除するのは誰なのか?巻き戻し時はなぜロックされないのか?辺りを考えるとよいと思う。)

f:id:kakkun61:20180910022628j:plain

手順13・14でギアの位置を細かに指定されるけど、これが意味があったのかが不明。なんだったんだろう。

おそらく複雑だったんだろうミラー跳ね上げ部分は組み立て済みなんだが、説明書に組み立て方が載っているのでバラして動作の確認ができるところもいい。

そんなこんなで組み上がったのがこちら。(ところで α6300 + Vario-Tessar T* E 16-70mm F4 ZA OSS で特に考えずさくっと撮ったのにいい味が出ている。)

ファインダーを覗くとこんな感じ。よく見るアイレベルファインダーではなくウェストレベルファインダーというやつだ。プリズムがない。

意外と写ることに感動するはず。

せっかくなのでフィルム1本撮ってきた。フィルムは Fujifilm Superia X-Tra 400。(撮り終わったと思ったら7枚ぐらいまだ残っていた。しくみ上最後になるほどダイヤルが重くなるのでそれで勘違いしたのかもしれない。)

ちゃんと写ってる。すごい。

左右反転像なので慣れないと左右の向きと回転を合わせるのがこんがらがる。(少女2人の写った写真はいかにもこんがらがったもの。)

Diana Mini と比較するとびっくりするほどブレていない。あっちは目測フォーカスでこっちはちゃんと確認できる。そしてシャッター押下時のブレの大きさの違いだろう。Diana Mini はシャッターを切った後のレバーの遊びが大きすぎてすごくブレる。

巻き上げ・巻き戻しが固くて指の皮が痛くなるけど、しばらくはこれで撮影しようと思う。

キャンペーンでもらったクローズアップアダプターレンズとマクロアダプターレンズをまだ作っていないのでまだ楽しめる。

『Yesod 入門』商業誌化

同人誌で発売していた『遠回りして学ぶ Yesod 入門』がこのたびインプレス R&D より『Haskell で作る Web アプリケーション 遠回りして学ぶ Yesod 入門』として商業誌化されることになりました。

www.impressrd.jp

f:id:kakkun61:20180901081452p:plain

同人誌版からの変更点は主に、

  • 日本語が読みやすくなった
  • 対応バージョンが上がった

点です。章が増えたり減ったりはしていません。あと、表紙がかわいくなりました。かわいい。

商業誌では Amazon と honto にてオンデマンド印刷の紙の書籍も購入できるようになっています。紙はこれまで即売会でしか販売していなかったので初めて通信販売で買えるようになりました。

よろしくお願いします。