デシリアライザとスキーマ

盛大に遅れました…

qiita.com

最近思いついたネタで実用性の高そうなものを紹介。

binarycerealのようなライブラリはデータを密にシリアライズするが、その際にフィールド名や型などの情報は失われてしまう。かといってそれらを一つ一つすべて含めるとひどく効率の悪いフォーマットになってしまう。そこで、スキーマを分離できるような仕組みを作れないかと考えて作ったのがこのクラスだ。

{-# LANGUAGE TypeFamilies, ScopedTypeVariables, FlexibleContexts, UndecidableInstances #-}
import Data.Binary

class HasSchema a where
    type Schema a :: *
    toSerializer :: a -> Put
    toDeserializer :: Schema a -> Get a

HasSchemaクラスは、普通にシリアライズするメソッドtoSerializerと、スキーマを使ってデシリアライズするtoDeserializerを持っている。Schemaは型族なのでどんなものでも使える(一般にどうあるべきかよく考えていない)。

instance HasSchema a => HasSchema [a] where
    type Schema [a] = [Schema a]
    toSerializer = mapM toSerializer
    toDeserializer = mapM toDeserializer

同じスキーマの値が連続している場合、以下のTableという型を使うと無駄がない。

newtype Table a = Table (V.Vector a)

instance HasSchema a => HasSchema (Table a) where
    type Schema (Table a) = Schema a
    toSerializer (Table v) = put (V.length v) >> mapM toSerializer v
    toDeserializer sch = get
        >>= \n -> Table <$> V.fromListN n <$> replicateM n (toDeserializer sch)

このSchematicというデータ型はスキーマとデータの対にすぎないが、これで包んでおくことで簡単にHasSchemaの力を引き出せる。

data Schematic a = Schematic !(Schema a) !a

instance (HasSchema a, Binary (Schema a)) => Binary (Schematic a) where
    get = get >>= \sch -> Schematic sch <$> toDeserializer sch
    put (Schematic sch a) = put sch >> toSerializer a

今のところ、社内で使っているレコード型を簡単にシリアライズするために実験的に使っているが、より一般的で面白い使い方があるかもしれない。この記事では、実体のあるHasSchemaインスタンス(instance HasSchema Intなど)は一切定義されていない。どう作っていくかは、これからの課題だ。

正格フラグ、バンパターン、正格版関数・データ構造

Haskellスペースリーク Advent Calendar 2015 9日目

Haskellerとて、時には厳しくならなければいけないこともある―― @fumieval, 2015

Haskellは遅延評価を基本としているため、場合によっては未評価の式が積もり非効率な状況に陥ることがある。これを防ぐため、部分的に正格評価にするための仕組みが用意されている。もちろんこれらは闇雲に使えばよいというものではない。使うべきポイントを把握し、これらを見逃さないようにしよう。

この記事では、それらの機能の正しい使い方、間違った使い方を紹介していこう。

カウンター・カウンターズ・サンクス

条件を満たす要素の個数とそうでない要素をそれぞれカウントするプログラムについて考える。アキュムレータ(ループの中で積み上げていく変数)は正格にしないといけないらしいので、BangPatterns拡張を使ってみた。どんなパターンでも、感嘆符をつけることによってあらかじめ評価を強制できるすごい拡張機能だ。

{-# LANGUAGE BangPatterns #-}

data Counter = Counter Int Int

count :: (a -> Bool) -> [a] -> Counter -> Counter
count p (x:xs) !(Counter m n)
   | p x = count p xs (Counter (m + 1) n)
   | otherwise = count p xs (Counter m (n + 1))

残念ながらこれは意味がないどころか、スペースリークを解決できていない。というのも、countに渡しているのはCounterコンストラクタ、つまり評価する必要のない値である一方、Counterの中身にはサンクが溜まってしまっているからだ。コンストラクタではなく、中身にバンパターンをつけよう。

{-# LANGUAGE BangPatterns #-}

data Counter = Counter Int Int

count :: (a -> Bool) -> [a] -> Counter -> Counter
count p (x:xs) (Counter !m !n)
   | p x = count p xs (Counter (m + 1) n)
   | otherwise = count p xs (Counter m (n + 1))

データ型のフィールドのほうに感嘆符をつけると、そのフィールド自体が正格になる。こちらは拡張いらずで、関数定義にいちいちバンパターンをつけなくてもよいので便利だ。

data Counter = Counter !Int !Int

count :: (a -> Bool) -> [a] -> Counter -> Counter
count p (x:xs) (Counter m n)
   | p x = count p xs (Counter (m + 1) n)
   | otherwise = count p xs (Counter m (n + 1))

遅延評価を含めた意味論にこだらわないのなら、すべてのデータ型のフィールドを正格にするのが無難な選択だろう。

data Maybe' a = Nothing' | Just' !a

instance Functor Maybe' a where
    fmap f (Just' a) = Just' (f a)
    fmap _ Nothing' = Nothing'

この正格版Maybeは、fmapしても中身にサンクが溜まらないのでいいことずくめに見えるが、fmapが厳密には則を満たさなくなってしまうという欠点がある。fmap id (Just undefined)Just undefinedと等しいが、fmap id (Just' undefined)undefinedになってしまう。

もっとも、日常ではここまで気にしなければいけない場面は少ないので、ほとんどの場合は気にせず感嘆符をつけて大丈夫だろう。GHC 8.0からは、全フィールドをデフォルトで正格にするStrictDataという拡張が入るため、こちらを使おう。

Strictに早合点

Lazyだとトラブルを起こしやすいということで、「正格版」の関数やデータ構造を提供している場合もある。Data.Map.Strictはその一例だ。

import Data.Map.Strict

data AppState = AppState {
   ...
   buzz :: Map (Int, Int) Int
   ...
   }

しかし、Strictがついているからといって油断してはいけない。このAppStateにおけるbuzzを繰り返し更新するとスペースリークが発生する。実はモジュール名のStrictが意味するのは、挿入時などに要素(Int)が評価されるというだけで、Map全体は正格にはなってくれないのだ。やはりフィールドは正格にする必要がある。

import Data.Map.Strict

data AppState = AppState {
   ...
   buzz :: !(Map (Int, Int) Int)
   ...
   }

関数適用を伴って更新する値は正格にしておこう。これさえ守っていればまずスペースリークは発生しない。

動物、とくにヒトと性について

我々脊椎動物は有性生殖をする。したがって、繁殖に寄与する性質が必然的に残り、そうでないものは消えてゆく。オスは精子、メスは卵を作り出すという非対称性があり、体つきや行動もそれに合わせるように決まるのは自然だ。一夫多妻制の種においては、オスはメスを取り合うためにより強靭で攻撃的な性質が要求される。日本では法律上一夫一妻制を取るが、その影響は強く残っているだろう。実際、オスとして生を受けた私も、コミュニティの中でそれを体感している。

 生まれつきの体質か、育った環境のせいかは知らないが、幼いころは体が弱かった(3才のころ、両足飛びができなかったそうだ)。そのため、幼いころの私の趣味は読書、ままごとやお絵かきなど、他の男性とは大きく異なるものだった。「男のくせに」と罵られ、暴力を振るわれたりすることもあった。残念ながらこれは戦略として正しくて、後述する私の性質を考えても、私をこの時点で消しておくことは充分に合理的だ。周りに助けを求めることもあったが実際に改善された例は少ない。なぜなら私を攻撃するのは理に適っているからだ。一方で、女性からは好意を持たれることが多かった。乱暴で幼い男たちに比べ私はいくらか紳士的かつ女性的で、友達としてよい役割を果たせたのだと思う。破られたり壊されたりした私の持ち物を直そうとする彼女たちの優しさにはひたすら感心した。私は、純粋に害を及ぼすオスが嫌いで、利益をもたらすメスが好きだった。性をしっかり認識することは身を守るために必要だった。

 第二次性徴を迎えるとやや事情は変わってくる。男性、女性はお互いを恋愛対象(繁殖対象という品のない言いかえもできる)として見るようになる。女性たちとは比較的友好的な関係を持っていたが、「モテない」「モテる」、つまり異性として魅力を感じるかどうかという評価軸ができたことも分かった。禁欲的というかそもそも欲がない私にはあまり関わりのないことだった。一方で、論理的な思考力と自信は強くなっていったため、教員と衝突することもあった。

 高専(高等専門学校)に入学してからは性の意識はいくぶん変わっていった。高専は女性の人数が男性の1/5程度となぜか比較的少ないため、一夫多妻的な戦略に起因するような争いは少ないように見えた。別に男女間の仲が悪いわけではないが、同性同士で固まる傾向が強いと感じた。私が高専で経験した揉め事のほとんどは体制に起因するもので、クラスにおける学生同士の衝突はほとんど認識しなかった。おおよそ平和だったように思う。

 就職してからは私的に女性と接することはほとんどなくなった。短時間だけ出勤する事務の方を除けば全員が男性で、たまに開催される勉強会でも男性の比率は高い(98%くらいだろうか)。これはプログラミングにおけるコミュニティは男性の比率が大きく、理由はわからないが私が主に使っているHaskellという言語の場合では特に高いためだ。プログラミングに要求される能力とオスメスの機能に関係があるようには見えないので、とても奇妙な話だ。個人的に女性が少ないことにそこまで不満を抱いてはいないが、男女が生まれる割合により近いほうが、我々にとって望ましいのではないかと思う。このような状況で、マイノリティの存在を非難したり、悪い方法で異常に取り沙汰するのはとても不道徳なことだ。

 この話はおまけである。プログラミングは、主に人手を使わずに目的を達成するために行われている。より少ない人手(人数と記述量)で、より効率的に目的を達成するのがプログラミングの目標だ。また、プログラミングの生産性はコミュニケーションのコストのため人数に比例しないことが知られている。だとすれば繁殖は必須ではなく、むしろコストの増大要因となってしまう。もし繁殖が必要ならば自然とバランスがとれるはずだが、プログラミングの目的は繁殖とはむしろ逆なため、何らかの要因でバランスが崩れてもさほど非合理的ではないと考えている。

 閑話休題、私は快楽主義者である。快楽を生み出す者を助け、邪魔する者には戦う姿勢を見せたい。より多くの快楽を、より効率的に生み出すのが私の究極的な目的である。

 性愛の話はいつも人々の話の種だが、私は興味がない。社会通念にとらわれず、私の好きな方法を使ってひたすら楽しく生きていきたい。ただ、恋愛が発生しやすい環境にいないだけで、恋愛のようなものがあっても悪くはないのかもしれない。

就職しました

本日、Tsuru Capitalのポジションを得ました。

Tsuru Capitalはデリバティブの取引を行っている企業で、自動株取引の会社ではありません。取引に関わっている10人のメンバーのうち、創始者であるSimonを除く全員がHaskellerで、取引状況の分析や一部の取引の自動化など、あらゆるところにHaskellを使っているのが大きな特徴です。日本では数少ない、Haskellをメインに使っている企業の一つでもあります。

東京、シンガポールバンクーバーにオフィスがあり、東京には私を含む5人の開発者と事務担当、Simonと愛犬テトがいます。f:id:fumiexcel:20151006095421j:plain

オフィスはオランダヒルズ森タワーRoPにあります。設備が非常に充実しており、東京に引っ越すまではオフィスに週数度の頻度で泊まっていました。風呂上がりにジンジャエールをラッパ飲みしながらサーバールームの熱風で体を乾かすと、すごく気持ちが良いです。 職場の雰囲気は非常にカジュアルで、テトと遊んだり時々昼寝したりすることもあります。エスプレッソと炭酸水が好きなだけ飲めるのもよいところです。

仕事

「Tsuru Capitalのインターンに応募してみよう」と思い応募した二日後に面接を行い、次の週から3カ月間インターンとして働くことになりました。面接ではマルチメディア関連のスキルについて話していたこともあり、最初のほうは主に取引状況を見るためのGUIの開発をしていましたが、最近は実際のトレードに関わるものも含めいろいろな部分に手を加えています。タスクは多種多様で、プログラムを書くのが楽しいです。私は抽象的なコードを扱うのが得意なので、そこをさらに活かせるよう頑張っています。

ソースコードについて

Tsuru Capitalの社内のコードはかなり高品質だ思います。lenslinearなどの比較的先進的なライブラリも活用しています。尖ったリファクタリングにも寛容で、いつでも新鮮さを保っています。取引をする上ではパフォーマンスが損益に直接関わってくるため、チューニングは丹念になされていますが、もちろんHaskellerらしく抽象化は怠っていません。

とはいえ、今までストリームの処理にはレトロ感あふれるiterateeと社内ライブラリを組み合わせて使っており、お世辞にも保守しやすいとは言えないものでした。そこで、私は新たなライブラリを開発し、古いコードを徐々に置き換えています。一筋縄ではいきませんでしたが、コード量が削減されただけでなく、パフォーマンスの向上も見られました。具体的な話は来月の関数型ストリーム処理勉強会のために残しておきましょう。


私はHaskellが好きです。一番好きなプログラミング言語であるHaskellで仕事ができるのは非常に嬉しいことでもあります。開発を通じて得たノウハウをただ仕事に生かすだけではなく、これからも記事やオープンソースソフトウェアとして世界に発信していきます。

カリー化

鍋にオリーブオイルを入れる。

にんにくを細切りにし、入れる。しょうがを少しすりおろす。いつもの流れである。

玉ねぎの半分をみじん切りにし、鍋に入れ、しばらく炒める。

キャベツ、にんじん、ヒラタケ、残りの玉ねぎ、じゃがいも(皮ごと)を大き目に切り、蓋をしつつ少し間隔を置いて順に入れる。

しばらくしたあと、鶏肉を入れる。少量のクレイジーソルトとバターも入れた。

水は少しだけ加え、他は素材の水分に頼る。Vita Craftの性能に期待を寄せる。

www.vitacraft.co.jp

市販のカレールウをある程度分割し、まぶすように入れる。6分程度待ち、途中で中身をひっくり返す。

火を止め、しばらく余熱を加えて完成だ。炊き立てのご飯と一緒に皿に盛り、チーズをトッピングして出来上がり。

作る前から分かっていたことだが、汁は少ない。日本で一般的なカレーとは異なる。

しかし、うまい。調理時間は短いが、肉や野菜にしっかりと火が通っており、それぞれのうまみが伝わってくる。芯ごと煮込んだ(蒸した?)キャベツは柔らかく、甘い。

男爵薯の舌触りもよい。外側についた濃い目の汁とのバランスは絶妙だ。

今夜はカレーのようなものを作った。料理は勢いでなんとかなってしまうものだ。

最近作った料理(簡単さ順)

面倒なので写真はなし。

ミニマリスティック卵スープ

  • 鍋でを沸かす。
  • 創味シャンタンを1人あたり小さじ半分ほど入れる。で味を補う。
  • 溶き卵を乱暴に投入する。

賞味期限の近い具材を消費するためのチャーハン

  • ごま油とサラダ油を強火で熱したフライパンに入れる。
  • 溶き卵を乱暴に投入する。
  • 数秒後にご飯を投入する。
  • ねぎと薄く切ったにんにくを入れる。
  • 創味シャンタンを小さじ半分入れる。
  • 適当な具材を入れる。賞味期限が切れそうだったソーセージとキムチを入れた。
  • 醤油と黒胡椒で味を調える。

牛丼

  • ごま油とサラダ油をフライパンに入れる。
  • みじん切りにしたにんにく、少量のおろししょうがを加える。
  • ここで七味唐辛子を投入する。
  • 揚げるがごとく炒め、香りがいい感じになるのを待つ。
  • 玉ねぎと、和風だしの素をごく少量入れ、炒める。
  • 水、適量の醤油、三温糖を入れる。
  • 牛肉を入れ、火が通るまで加熱する。

備考: 生の唐辛子を使おうと思っていた部分を七味唐辛子で代用したが、案外こちらのほうがよいかもしれない。酒も入れたかったが、筆者が未成年ゆえ入手できなかったため省いた。

lensパッケージのオプティクス(弱い順)

lensではオプティクスと呼ばれる様々な構造が定義されている。これらの関係を把握していれば、ドキュメントから欲しいものを見つけるのが楽になる。この記事では弱い順にオプティックの数々を紹介していく。

Fold

type Fold s a = forall f. (Applicative f, Contravariant f) => (a -> f a) -> s -> f s

Contravariantがついているのでわかりにくいが、これは本質的に以下の型と等価だ。mappend*>memptyfmap absurd $ contramap absurd $ pure ()に相当する。

type Fold s a = forall r. (Monoid r) => (a -> r) -> s -> r

で、これは何かと言えば、foldMap :: Foldable f => (a -> r) -> f a -> rの一般化そのものだ。foldMapf aの要素aを畳み込むが、Fold s asの中のaを畳み込む。当然、FoldableでできることはFoldでもできる。そのあたりはモノイドと継続渡しの道具箱で触れている。

FoldはそのままではfoldMapの一般化としては使いにくいため、Control.Lens.Foldモジュールでユーティリティが定義されている。Foldableメソッドとしてfooがあったとき、Foldableの代わりにFoldを受け取るfooOfという関数が提供されている。

Getter

Getterはより強く、Functorしか要求しない。

type Getter s a = forall f. (Functor f, Contravariant f) => (a -> f a) -> s -> f s

こちらは、Applicativeに由来するモノイドの力を必要としないため、forall r. (a -> r) -> s -> rという関数と等価になる。これはs -> aという関数を継続渡しスタイルにしたものだ。したがって、Getter s as -> aと同じものとして見てよい。

view :: ((a -> Const a a) -> s -> Const a s) -> s -> a
view l = getConst . l Const

Setter

FoldやGetterと独立した概念としてSetterがある。ASetterSetter型の特殊な場合であるが、本質的な差はない。

type ASetter s t a b = (a -> Identity b) -> s -> Identity t

over :: ASetter s t a b -> (a -> b) -> s -> t
over l f = runIdentity . l (Identity . f)

こちらは(a -> b) -> (s -> t)と等価だ。セッターとして使えるが、セットした際の型の変化を許すようになっている。

Traversal

FoldがfoldMapの一般化だったのに対し、TraversalはTraversableクラスのメソッドtraverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)の一般化になっている。

type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t

そのため、traversemapMの代わりを作る感覚でTraversalを定義できる。例えば、リストの偶数番目の要素に対するtraverseEvenは以下のようになる。

traverseEven :: Traversal [a] [a] a a
traverseEven f (x:y:xs) = (:) <$> f x <*> fmap (y:) (elementsEven f xs)
traverseEven _ xs = pure xs

TraversalFoldであり、Setterでもあるのが特に重要である。例えばover elementsEven (*3)は偶数番目の要素を3倍にするし、sumOf elementsEvenは偶数番目の要素の合計を求める関数になる。

Lens

Traversal複数の要素を扱うのに対し、Lensと呼ばれる構造はただ1つの対象しか扱わない。

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

LensTraversalでもあり、SetterかつGetterである。レコードのフィールドの表現に使えるだけあって人気も高く、パッケージ名になっているほどである。

Lenstraverseと似たような感覚で作れる。Traversalは0個や複数の要素を扱うのにpure<*>を使うが、要素が1つならばfmapだけでよいというのはわかりやすい。

Review

ReviewはGetterを逆転させたものだ。回りくどいが、Review t bb -> tという関数と等価である。

type AReview t b = Tagged b (Identity b) -> Tagged t (Identity t)

(#) :: AReview t b -> b -> t
(#) l = runIdentity . unTagged . l . Tagged . Identity

Prism

type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)

PrismTraversalかつReviewである。これは作りにくいので、自分で定義する際はprism :: (b -> t) -> (s -> Either t a) -> Prism s t a bという関数を使おう。

_Just :: Prism (Maybe a) (Maybe b) a b

_JustMaybeに対するtraverseとしての機能を持つが、Reviewでもあるため、_Just # 42からJust 42を得るということができる。代数的データ型のコンストラクタと似たような雰囲気で、値を埋め込んだり取り出したりできる。

Iso

type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)

IsoLensかつPrismであり、こちらは要素が必ず1つあることが保証されている。したがって同型な二つのデータ型にならIsoを定義できる。こちらはヘルパー関数なしでも作りやすい。

enum :: Enum a => Iso Int Int a a
enum = dimap toEnum (fmap fromEnum)

Equality

type Equality s t a b = forall p f. p a (f b) -> p s (f t)

EqualityIsoよりさらに強い。Equalityになる値はidただ一つしかないため、型の等価性を表現する。等価性を採取して他の場所に運びたいときに使えるが、その目的ならbaseパッケージのData.Type.Equalityがあるため、使う場面はかなり少ない。

まとめ

構造を「使いたい」と思ったときは弱いほうから、「作りたい」と思ったら強いほうから、対応するモジュールを調べてみよう。この力の関係を知っていれば、よりよくlensを活用できるに違いない。