C ドライブ依存を減らす

いろいろなソフトウェアがデフォルトで何でも C ドライブを使う。

C ドライブが手狭になってきたのでいくらかのデータを別ドライブに移行する方法をメモしておく。

Stack

Stack(Haskell)のリソースはユーザー環境変数 STACK_ROOT の指すディレクトリーに保存される。デフォルト値は C:\sr である。パス長制限の問題があるので浅い場所がよい。D:\stack に変更した。

Stack によってインストールされる GHC や MSYS2 は LOCALAPPDATA\Programs\stack にある。これは STACK_ROOT\config.yamllocal-programs-path を記載すると変更できる。非 Windows 環境では STACK_ROOT\programs らしいということもあり D:\stack\programs に変更した1

STACK_ROOT\config.yaml に記載する Non-project-specific configSTACK_ROOT\global-project\stack.yaml に記載する Project-specific config は別物なので注意する。

75 GB ぐらいだった。

Chocolatey

Chocolatey のリソースはシステム環境変数 ChocolateyInstall の指すディレクトリーに保存される。D:\Chocolatey に変更した。choco.exeChocolateyInstall\bin にあるのでシステム環境変数Path に追加しておく2

いくつかのパッケージの実行ファイルが保存される場所はユーザー環境変数 ChocolateyToolsLocation によって示される。D:\tools に変更した。


  1. LOCALAPPDATAディレクトリーの役割についてはWindowsのディレクトリ構成ガイドライン - torutkのブログが日本語で完結に知れる。

  2. 通常ユーザー環境変数は同名のシステム環境変数を上書きするが、Path は特別で実行ファイルを探すときはシステム環境変数Path から検索した後にユーザー環境変数Path から検索する。

Zenza Bronica S 系用接写リング

Zenza Bronica S 系用接写リングの説明書(をスキャンしたものをプリントしたもの)を入手したので文字データに起こした。

Zenza Bronica S 系接写リング

docs.google.com

この接写リングは4個(C-A C-B C-C C-D)で1組になっています 使用できるレンズも 40 mm~200 mm までと巾広く 標準レンズ使用の場合で等倍までの近接撮影ができます 4個の接写リングは撮影の目的に合わせ ご自由に組合せてご使用ください 別表は S2 型・C2 型カメラに 接写リングと交換レンズを組合せた場合の撮影倍率と露出倍数の関係を示します

(表)

● 被写体に近接するに従って フィルム面とレンズの距離は遠くなり 実際のレンズの明るさ(F値)は減少します 表中の露出倍数を参考にして露出時間を増加してください

● 接写には ブロニカ TTL 露光計の併用をおすすめいたします 露出倍率などに関係なく 指針を 0 に合わせるだけで 正しい露光が得られ 完全に遮光されたピントフードがついていますので映像は明瞭です

最近の自分の Haskell 開発環境(Windows)

id:syocy のブログを見たので Windows で自分がどうしているかをメモしておく。

syocy.hatenablog.com

Stack

最近は ghcup があるがシェルスクリプト製で自分は PowerShell ユーザーなので stack を使っている。(ghcup はなんで Haskell 製じゃないんだ?)

GHC 8.8 を使うには resolver は ghc-8.8 や nightly を指定する。まだ LTS にはなっていない。

エディター

エディターは Spacemacs を使っている。前は IntelliJ IDEA に HaskForce プラグインを入れて使っていたが、ソースコード量に対して線形以上に動作が遅くなっている感覚があって使わなくなってしまった。

Spacemacs の提供してる Haskell レイヤーに Happy Haskell Programming を追加して使っている。

.spacemacs.d\init.el に次のように追記している(リポジトリー)。

(defun dotspacemacs/user-config ()(add-to-list 'load-path "~/.spacemacs.d/hhp/elisp")
  (autoload 'hhp-init "hhp" nil t)
  (autoload 'hhp-debug "hhp" nil t)
  (add-hook 'haskell-mode-hook (lambda () (hhp-init))))

HHP は stack プロジェクトを考慮しないので、unveil-stack を使って、ghc コマンドが呼ばれると現在のプロジェクトの resolver が指定する ghc を呼び直すようにしている。hhpc や hhpi についても同様で stack exec -- hhpc のように呼び直している。

Spacemacs の Haskell レイヤーについてはここを、HHP の機能についてはここを参照のこと。

Makefile

Haskell プロジェクトのビルドは stack 経由だが、その他のタスク用に PowerShell 版の Make のような Psake を使っている(発音は「酒」と同じだそうだ)。

リポジトリーの全ファイルに stylish-haskell や hlint をかけたり、改行を CR/LF から LF に変換したりするのに使っている。

Task Format {
    Exec { Get-ChildItem -Filter '*.hs' -Recurse src | ForEach-Object { stack exec -- stylish-haskell -i $_.FullName } }
}

Task Lint {
    Exec { stack exec -- hlint src }
}

関数のメモ化

ブログに書いてみるとよく分からなくなってきました 🙃

Haskell-jp で回答をもらいました。


@lotz84_ さんの記事や GHC のプロファイルに出てくる CAF がよく分かってなかったのをまとめる。

qiita.com

fact のメモ化

lotz さんの記事の階乗 fact 関数を題材にする。

fact :: Int -> Integer
fact 0 = 1
fact n = fromIntegral n * fact (n-1)

lotz さんの記事よれば、次の実装だとメモ化されるとのこと。

-- | 関数をメモ化する関数
memoize :: (Int -> a) -> Int -> a
memoize f = (map f [0..] !!)

fact :: Int -> Integer
fact = memoize fact'
  where
    fact' 0 = 1
    fact' n = fromIntegral n * fact' (n-1)

な、なんで……

1つめを fact1、2つめを fact2 として GHCi で確かめてみる。

> :set +s
> fact1 50000 `seq` pure ()
(2.44 secs, 2,605,979,968 bytes)
> fact1 50000 `seq` pure ()
(2.41 secs, 2,605,979,968 bytes)
> fact2 50000 `seq` pure ()
(2.50 secs, 2,613,580,040 bytes)
> fact2 50000 `seq` pure ()
(0.00 secs, 88,432 bytes)

確かに fact2 の2回めの評価はすぐに終わっている。

簡単のために fact2 を次のように書き換える。

fact3 :: Int -> Integer
fact3 = (map fact' [0..] !!)
  where
    fact' 0 = 1
    fact' n = fromIntegral n * fact' (n-1)
> fact3 50000 `seq` pure ()
(2.47 secs, 2,611,180,024 bytes)
> fact3 50000 `seq` pure ()
(0.00 secs, 88,432 bytes)

同じようにメモ化されている。

では次のように書き換えると?ポイントフリー化されていたのを引数を明示するようにした。

fact4 :: Int -> Integer
fact4 x = map fact' [0..] !! x
  where
    fact' 0 = 1
    fact' n = fromIntegral n * fact' (n-1)
> fact4 50000 `seq` pure ()
(2.58 secs, 2,619,180,432 bytes)
> fact4 50000 `seq` pure ()
(2.59 secs, 2,619,180,432 bytes)

メモ化されない。

もう1つ、ラムダ式で定義すると?

fact5 :: Int -> Integer
fact5 = \x -> map fact' [0..] !! x
  where
    fact' 0 = 1
    fact' n = fromIntegral n * fact' (n-1)
> fact5 50000 `seq` pure ()
(2.57 secs, 2,619,180,432 bytes)
> fact5 50000 `seq` pure ()
(2.52 secs, 2,619,180,432 bytes)

メモ化されない。

何か特別な形である必要があるみたい。

Constant Applicative Form・Super Combinator

Haskell High Performance Programming によると、その特別な形は Constant Applicative Form(定作用形)というらしい。プロファイルを取ると Cost Centre に CAF と書かれてあるやつ。

www.packtpub.com

項が定作用形あるとは、項がスーパーコンビネーターでありかつラムダ式でないものである。

Constant applicative form - HaskellWiki

とりあえず、項がラムダ式であるとダメだそうなので fact5 は CAF ではない。

項がスーパーコンビネーターであるとは、項が定数もしくは、項がコンビネーターであり全ての部分項がスーパーコンビネーターであるものである。

Super combinator - HaskellWiki

これは次の定義と同値だそう。

任意の \x1 x2 … xn -> EEラムダ式でない。n ≧ 0。)の形の式で次の場合に限りそれはスーパーコンビネーターである。E における自由変数は x1, x2, … xn のみで、かつ E に出現するラムダ式は全てスーパーコンビネーターである。

fact3 が CAF であるかを確認する。

fact3 :: Int -> Integer
fact3 = (map fact' [0..] !!)
  where
    fact' 0 = 1
    fact' n = fromIntegral n * fact' (n-1)
  • map fact' [0..] !!ラムダ式でないためスーパーコンビネーターであればよい。

  • map fact' [0..] !! は定数でないため部分式が全てスーパーコンビネーターであればよい。

  • 部分項である map は、あ、あれ?定数でないしスーパーコンビネーターでもなくただの変数では……?2つめの定義においても x1, x2, … xn 以外の自由変数だし……

…………

……

いかがでしたか?関数がメモ化関数になるためには定義が CAF でないといけないようです 😎

なんで fact3 が CAF なのか教えてください 🙏


Haskell-jp で回答をもらいました。

Markdown モードで引用の中でコードブロック書くのどうやるんだ……)

@mizunashi-mana

スーパーコンビネータは,一般の文脈では確かにコンビネータで部分項がスーパーコンビネータであるものを指しますが, CAF の文脈では,

  • ラムダ式でない
  • ローカル関数が全てグローバルに出しても問題ない定義になっている

だと思うのが良いと思います.厳密には,

https://gitlab.haskell.org/ghc/ghc/wikis/commentary/rts/storage/gc/CAFs

に書いてある通り,要はサンクとなる static closure のことを指しています.

ところでメモ化の要因は,確かに fact3 が CAF であることもありますが,一番の要因として, Core では関数適用の引数は let を通して変数に束縛されることになるので,実際にはこのプログラムは

fact3 = (!!) fact3'
   where
       fact3' = map fact' l
       l = [0..]
       fact' 0 = 1
       fact' n = ...

みたいなものと等価になります (厳密にはこれも怪しいですが) .で, fact3' が CAF となるからというのが大きいですね (なお実際 GHC 8.6.5 では fact3' が floating out されていて, fact3 自体は最終的に eta expand されて fact3 = \x -> (!!) fact3' x という形になっていました)

技術書典 5 ふりかえり

え?6?いやいや 5 ですよ?

techbookfest.org

池袋

慣れ親しんだ秋葉原の地を飛び出して池袋にやってきました。

めちゃくちゃ広くてびっくりです。秋葉原通運会館からアキバスクエアにやってきたときも思いましたが、同じ感想がもう一度。

ガラス張りじゃなくなったので外の行列見てやばいやばい言えなくなったのはほんのちょっとだけ残念です。

か61

kakkun61 という名前でもろもろアカウントを取っているのですが、今回は卓番号が「か61」ということでまさに自分のための場所でした。覚えやすい!

そんな弊卓の様子です。

新刊落としました…… フィルムカメラに目覚めた結果土日をそれに使ってしまい云々

ま、まぁ、でも、商業化した『Haskell で作る Web アプリケーション』は初技術書典持参だったし云々

nextpublishing.jp

はい。楽しみにしていただいたみなさまにはすみませんでした。

数字

被チェック数は60でした。

売上部数は下記の通り。

  • 手続き Haskell
    • 紙 32部
    • 電子 ?部(メモしてなかった)
  • Haskell で作る Web アプリケーション
    • 紙 28部

『手続き Haskell』は印刷部数のおよそ半分が売れて、『Haskell で作る Web アプリケーションは完売でした。

技術書典 2・4 でも頒布した(同人版ですが)『Haskell で作る Web アプリケーション』が思った以上にサクサク売れていったのが印象的でした。技術書典への新規参加者か、表紙がよくなったことか、その両方か。

それでは。

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)からかなぁ。