Hatena::ブログ(Diary)

あどけない話

2016-01-14

重複したフィールドラベル

Haskell 2010 では、同じファイルに重複したフィールドラベルを定義できない。たとえば、以下はエラーになる。

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float } -- これはダメ

この問題を解決する案は、OverloadedRecordFields と呼ばれ、苦難の歴史を持つ。実装があるにもかかわらず、

  • 実装が一枚岩
  • コードの複雑になる割に利益が少ない

などの理由により、GHC へはマージされずにいた。現在では、OverloadedRecordFieldsは、三つの拡張へと分割された:

  1. DuplicateRecordFields
  2. OverloadedLabels
  3. Magic type classes

この中、1. と 2. が GHC 8.0 に入る。

DuplicateRecordFields

DuplicateRecordFields は単純で、同一ファイルで重複したフィールドラベルが「定義」されることを許す。以下は、GHC 8.0 ではエラーにならない。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

しかし、フィールドラベルを使おうとするとエラーになる。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

main :: IO ()
main = do
    let x = Foo 1
    print $ same x -- これはダメ

型推論はどこで完了するか分からないので、型を取ってこれないらしい。型を明記すれば使える。

{-# LANGUAGE DuplicateRecordFields #-}

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

main :: IO ()
main = do
    let x = Foo 1
    print $ same (x :: Foo)

これはやってられない感じがする。

OverloadedLabels

この問題を解決するための第一歩が OverloadedLabels である。とっても面倒だが、以下のような記述で、# が付いたフィールドラベルが使えるようになる。

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE DataKinds, TypeFamilies #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}

import GHC.OverloadedLabels

data Foo = Foo { same :: Int }
data Bar = Bar { same :: Float }

instance (a ~ Int) => IsLabel "same" (Foo -> a) where
  fromLabel _ (Foo n) = n

instance (a ~ Float) => IsLabel "same" (Bar -> a) where
  fromLabel _ (Bar d) = d

main :: IO ()
main = do
    let x = Foo 1
    print $ #same x

将来の GHC では、このボイラープレートが自動導出されるようになるそうだ。

2015-12-25

h2load を使おう

これはhttp2 Advent Calendar 2015の25日目の記事です。

これまで web サーバのスループットを図るには weighttp が定番でしたが、これから h2load を使いましょう。

h2load は、nghttp2と一緒に配布されているベンチマークツールです。以下のような特徴があります。

対象プロトコルは以下のように切り替えます。

プロトコル平文TLS
HTTP1.1h2load --h1 http://<url>h2load --h1 https://<url>
HTTP2h2load http://<url>h2load https://<url>

主要なコマンドラインオプションは以下の通りです。

  • -n<num>: リクエスト数
  • -c<num>: コネクション数
  • -t<num>: ワーカー数 (マルチコア環境で指定する)
  • -m<num>: HTTP/2 用のリクエストの並列数

たとえば、HTTP/2 over TLS を計測するには以下のようにします。

% h2load -n1000000 -c500 -m100 -t4 https://<url>

h2loadは、辻川さんによって精力的に開発が進められているので、要望を出すと実装されるかもしれませんよ。

2015-12-17

GHC とスペースとリーク

これはHaskellスペースリーク Advent Calendar 2015の17日目の記事です。

:sprint と MonomorphismRestriction

サンクのリークを防ぐには、どの式がサンクかを理解する必要がある。そのために便利なのが、GHCiの:sprint コマンドだ。MonomorphismRestriction 拡張がデフォルトで有効だった GHCi 7.6 以前は、このコマンドは直感的に動く。

> let x = 1 + 2
> let y = (x,x)
> :sprint x
x = _
> :sprint y
y = (_,_)
> print x
3
> :sprint y
y = (3,3)

GHCi 7.8 以降では、MonomorphismRestriction拡張がデフォルトで無効になった。単に式を評価する使い方なら、こちらの方が便利だ。しかし、:sprintは挙動がおかしくなってしまう。

> let x = 1 + 2
> let y = (x,x)
> :sprint x
x = _
> :sprint y
y = _    {- あれれ? -}

MonomorphismRestriction を有効にすれば、:sprintは期待通りに動く。

> :set -XMonomorphismRestriction 
> let x = 1 + 2
> let y = (x,x)
> :sprint x
x = _
> :sprint y
y = (_,_)

問題は型が決まらないことなので、明示的に指定してもよい。

> let x = 1 + 2 :: Int
> let y = (x,x)
> :sprint x
x = _
> :sprint y
y = (_,_)

unbox-small-strict-fieldsフラグ

Haskellでは、基本的にデータは参照で指される。GHC 7.6 までは、即値を使うには、"!" と UNPACK プラグマの両方を指定する必要があった。

data Foo = Foo {
    bar :: {-# UNPACK #-} !Int
  , baz :: {-# UNPACK #-} !Int
  }

これはあまりにもダサい。GHC 7.8 以降では、unbox-small-strict-fieldsフラグデフォルトで有効になっているので、即値として扱える正格なフィールドは即値となる。

data Foo = Foo {
    bar :: !Int
  , baz :: !Int
  }

マニアックなことを言うと、たとえば Int は以下のように定義される。

data Int = Int Int#

Int が参照で、Int# が即値を表す。詳しくはUnboxed values as first class citizensを参照のこと。

Strict プラグマと StrictData プラグマ

GHC 8.0 以降では、Strict プラグマと StrictData プラグマが使えるようになる。"!" もダサいという人にはオススメ。


Weak ThreadId

スレッドリークの記事で、Weak ThreadId を使えばスレッドがリークしないと書いたが、実はリークしていることが判明した。

はっきり言ってGHCバグ。対策として、

  • Weak ThreadId の利用を止めて素直に ThreadId を使う
  • ThreadId を保持する IO アクションを適宜 return () で上書きする

のようにするよい。詳しくは、Weak ThreadId still leaks threadsを読んで欲しい。

2015-12-14

スレッドリーク

これはHaskellスペースリーク Advent Calendar 2015の14日目の記事です。

スレッドリークとは一般的に、終了させることを忘れたスレッドが残り続けることを言う。これは終了させ忘れたのが悪いという他ない。一方で、GHC では、スレッドを終了させたにもかかわらず、スレッドのメモリ領域がCGに回収されない事態が起こりうる。この記事は、この二番目のスレッドリークについて説明する。

ThreadId

ThreadIdは、名前からしても、以下のように表示させても、Ordのインスタンスであるところからも、単なる番号のように思える。

> forkIO (return ()) >>= print
ThreadId 88

しかし、ThreadIdのマニュアルを注意深く読むと、以下のようなことが書いてある。

GHCの実装では、ThreadIdは本質的にスレッドに対する参照である。
これは、ThreadIdを捨てなければ、GCスレッド自体を回収できないことを意味する。
このおかしな仕様は、将来修正されるだろう。

なんということだろう。ThreadId をどこかに保持していると、スレッドは終了しても開放されないのだ。これは、実際に Warp で問題となった。

弱い参照

これを解決するには、ThreadIdという強い参照を弱い参照に変換すればよい。このための関数である mkWeakThreadId のシグニチャは以下の通り:

mkWeakThreadId :: ThreadId -> IO (Weak ThreadId)

ThreadIdの代わりに、Weak ThreadId を保持するわけだ。Weak a は、Eq でも Ord でもないので、リストぐらいにしか格納できないことに注意。

Weak ThreadId から ThreadId を取り出すには、System.Mem.Weak の deRefWeak を使う。

deRefWeak :: Weak v -> IO (Maybe v) 

deRefWeakは、弱い参照の先に実際にデータがあれば Just を、そうでなければ Nothing を返す。ThreadId と一緒に使うコードの例を以下に示す。

do mtid <- deRefWeak wtid
   case mtid of
      Nothing  -> return ()
      Just tid -> killThread tid

まとめ

Haskellの一般的なデータ構造は、明示的には解放できない。しかし、スレッドは終了することで解放できる特殊なデータ構造である。他の言語からすれば当たり前なこのデータ構造には、他の言語が苦しめられているのと同種の危険性がある。

2015-12-08

にせ末尾再帰

これはHaskellスペースリーク Advent Calendar 2015の8日目の記事です。

IOのコードは、普通に書けば末尾呼び出しの最適化が効く形になる。たとえば、こんな感じ:

foo :: Char -> String -> IO Int
foo a b = do
    c <- bar a b
    zoo b c
    woo c

woo :: Int -> IO Int
woo c = do
  d <- goo c
  return $ d + 1

foo が woo を呼び出す時は、fooのフレームを忘れてしまってよい。IOの文脈再帰関数を書く場合も末尾再帰で十分な場合が多い。

goo :: Int -> IO ()
goo 0 = return ()
goo n = do
   getChar >>= putChar
   goo (n - 1)

残念なことに、一見末尾再帰に見えるが実はそうなっていなくて、スペースリークするパターンが存在する。たとえば、bracket と組み合わせた場合だ:

boo n = bracket alloc free $ \res -> do
  ...
  boo (n - 1)

これは一見末尾再帰に見える。しかし、bracket の実装は、

bracket before after thing =
  mask $ \restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- after a
    return r

であり、頭の中でコードを展開してみれば、boo はまったく末尾再帰の形になってないことが分かるだろう。これを「にせ末尾再帰」と呼ぶことにする。にせ末尾再帰の他の例としては、finally が挙げられる。

coo n = do {
     ...
   ; coo (n - 1)
   } `finally` finalThing

これらに対処するには、(go や loop といった)末尾再帰のローカル関数を定義すればよい。

boo n = bracket alloc free $ go n
  where
    go 0 _   = return ()
    go m res = do ...

まとめ:

  • にせ末尾再帰はスペースリークする
  • IO ではリソース管理などのせいで、にせ末尾再帰が起こりやすい
  • IO での再帰は、ローカル関数でやれ