FRPクライシス
FRP(Functional Reactive Programming)は、リアクティブプログラミングと関数型プログラミングの性質を持つプログラミングパラダイムである。FRPは古典的FRPと矢矧のFRPに大別される。
古典的FRP
古典的(Classical)FRPは、非連続的な値の列Eventと、常に何らかの値を取るBehaviourの二種類の構造を導入したものである。 代表的な実装としてreactive-banana、euphoria、reflexなどが挙げられる。
Haskellにおいては、EventはIOを通じて非同期的に生成できる設計が多い。Eventはマップやフィルタリングができ、モノイドとして合成することもできる。なお、GenはFRPの構造を扱うのに要求されるモナドで、実装の都合上しばしば必要となる。Behaviourは現在の値を取り出せる他、HaskellならApplicativeのインスタンスにもなる。「読み取り専用のIO」とも言えるかもしれない。CFRPの構成要素をまとめると以下のようになる。
Eventを畳み込むaccum
が肝で、Behaviorをサンプリングするapply
が心である。
矢矧のFRP
矢矧の(Arrowized)FRPは、値ではなく変換に着目し、値を変換する機構を導入する。Category
及びArrow
のインスタンスであるため、関数と共通の演算を持つ。実装はYampa、wiresなどがある。
(>>>) :: Category (~>) => (b ~> c) -> (a ~> b) -> a ~> c arr :: Arrow (~>) => (a -> b) -> a ~> b (***) :: Arrow (~>) => (a ~> b) -> (c ~> d) -> (a, c) ~> (b, d)
その実態は多くの場合ミーリマシンであり、状態を保持できる。表現する方法は色々あるが、例えば以下のような構造なら、入力に応じて次のMealy
を決めるということができる。
newtype Mealy a b = Mealy { runMealy :: a -> (b, Mealy a b) }
(>>>)
や(***)
などを直接組み合わせるのは骨が折れるが、Arrow記法を用いることで比較的簡単に記述できる。arteryというライブラリでシンセサイザーを実装した例を紹介する。
sineWave :: Artery m (Float, Float) Float -- 周波数と位相を入力とし、正弦波を出力する genADSR :: Float -> Float -> Float -> Float -> Artery m Bool Float -- なめらかな立ち上がり・立ち下がりを持つエンベロープを生成する bell :: Artery m (Float, Bool) Float bell = proc (freq, gate) -> do env <- genADSR 0.01 0.4 0.2 0.4 -< gate m <- sineWave -< (64 * freq, 0) sineWave -< (freq * 2, m * env * 0.5)
入力を明示的に表現する必要があるが、CFRPに比べるとパフォーマンスにおいては優れている傾向がある。また、矢矧のFRPは固有の概念が少ないのも長所である。
FRPの問題点
両者の共通の利点でもあり落とし穴でもあるのは、状態を隠蔽できるというところだ。状態を完全に隠蔽することで組み合わせやすくなる場合もある一方で、一度FRPで構築したものは、状態が隠れているゆえに拡張性が乏しい。古典的FRPにおいては、入力への依存性も隠蔽されているため、特に注意が必要である。隠れた状態が増えるほど、プログラムの性質は複雑で把握するのが難しくなる。同時発生するイベントの処理も注意が必要である。
注意すべきこと
- 何でもFRPで書こうとしない: FRPは、異なった内部状態を隠蔽し、合成可能な共通の型によって扱うことを可能にする。しかし、この利点が活きないような場所に適用すれば、単に再利用性とパフォーマンスが犠牲になるだけである。
- ライブラリのインターフェイスをFRPに限定しない: APIをFRPに限定することは、FRPに起因する問題を回避する手段がないことを意味し、ユーザーにとって足枷となる。FRPのAPIは色々な操作をまとめることで成り立つが、その内部の操作もエクスポートされていれば柔軟な使い方ができる。よっぽどの信念がない限り、FRPはアプリケーションのコードだけにとどめ、ライブラリの実装では使わないのが得策だろう。
- FRPによって何が得られ、何が失われるか常に意識する: FRPでプログラムを表現することは、内部状態にアクセスする、文脈に依存せず動作するという性質を犠牲にして、状態の隠蔽、簡単な合成を可能にする。本当に自信がない限り、FRPを使うべきではない。
- FRPは、プログラムの注意点を増やし、決して減らすことはない: FRPが新たな構造と演算を導入する以上、冗長性をいくらか解消できても、本質的な複雑さをなくすわけではない。安全に使いこなすには仕組みの理解が不可欠であり、多くのCFRPライブラリのように、実装が隠蔽されている場合は特に気をつけるべきである。
FRPの代わりになりうるもの
- STM TVarを中心とする、状態を保持する構造に対する操作(トランザクション)を、STMという専用のモナドで記述する。トランザクションは状態が更新された時に実行されるようにでき、リアクティブな動作を表現できる。並行処理との親和性が高いのも魅力である。
- ストリーム処理ライブラリ 入出力と状態を扱うという面ではFRPとの共通点が多い。何もファイルやネットワークなどに限定する必要はなく、アプリケーションのロジックをストリーム処理ライブラリで記述するのもアリだ。
- objective objectiveはHaskellでオブジェクト指向を実現するライブラリである。オブジェクトは自然変換とミーリマシンの性質を併せ持っており、矢矧のFRPの発展形としても見ることができる。FRPと同様、安易な使用はアンチパターンである。
総評
私は仕事で古典的FRPを使ったコードを保守しているが、(私から見て)過剰に使われているため、かなりの重荷である。少しでもFRPを減らして保守性を高めるべく、繊維強化プラスチックのごとき決意を固めた。
快速のExtensible effects
extensibleは拡張可能レコードだけでなく拡張可能作用(extensible effects)も用意している。拡張可能作用は一時期Haskell界隈で話題になったものの、今では人気も下火になってしまった。新しいバージョンをリリースした今、拡張可能作用の動機と使い方について改めて紹介しよう。
難行の一次関数
Haskellでモナドをカスタマイズする方法としては、transformersのモナド変換子がよく使われている。モナド変換子は、モナドをパラメータとして取り、新たな能力を付与したモナドにする構造だ。例えば、StateT sはモナド変換子の一つである。任意のアクションm a
はlift
を使ってStateT s m a
に変換できる。
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
他にもReaderT, WriterT, MaybeTなどの変換子があり、好きなように組み合わせてモナドを作れる。しかし、変換子を積んだ分だけliftを適用しなければいけないので、そのまま使うのは非常に不便だ。
苦行の二次関数
mtlというライブラリは、モナドの固有アクションを抽象化する型クラスを定義している。
MonadState
はその一つで、関数従属性m -> s
は、m
の型が決まればs
もわかるという制約を示している。もちろんStateTはそのインスタンスになる。
class Monad m => MonadState s m | m -> s where get :: m s put :: s -> m () state :: (s -> (a, s)) -> m a instance Monad m => MonadState s (StateT s m)
ベースのモナドがMonadStateならば、単体ではStateの能力を持たないモナドについてもインスタンスが定義できる。そのため、書き手側がliftを書く必要がなく、使いやすい。
MonadState s m => MonadState s (MaybeT m) MonadState s m => MonadState s (ListT m) (Monoid w, MonadState s m) => MonadState s (WriterT w m) (Monoid w, MonadState s m) => MonadState s (WriterT w m) MonadState s m => MonadState s (IdentityT m) MonadState s m => MonadState s (ExceptT e m) (Error e, MonadState s m) => MonadState s (ErrorT e m) MonadState s m => MonadState s (ReaderT * r m) MonadState s m => MonadState s (ContT * r m)
しかし、モナドを作る側にとっては厄介な代物だ。新しい変換子を作るたび、mtlにある分だけでMonadCont
, MonadError
, MonadReader
, MonadState
, MonadWriter
, MonadIO
の6つのインスタンスを定義しないといけない。しかもMonadCont
とMonadWriter
は単なるlift
では書けずかなり厄介だ。
クラスを作る場合、今度は自前のモナドに加え、AccumT
, ContT
, ExceptT
, IdentityT
, MaybeT
, RWST
二種、ReaderT
, WriterT
二種、StateT
二種の計12個のインスタンスも作らされる。変換子と対応するクラスを作るたびに加速度的に労力が増し、いつかは破綻する。
拡張可能作用
Extensible effectsはモナド変換子の代替として提案された*1。アクションを表すのに、Open unionsと呼ばれる特殊なデータ型を用いる。
data Union r v -- :: [* -> *] -> * -> * inj :: (Functor t, Typeable t, Member t r) => t v -> Union r v
Unionは型レベルリストをパラメータとして取る。そのリストの要素に含まれていれば、inj
関数を和型のコンストラクタのように使える。これをFreeモナドと組み合わせたモナドEff
を導入し、liftいらずにしようという試みだ。
実装はextensible-effectsパッケージとして存在する。アクションがTypeableであることを要求する、データ型が不必要に再定義されているなど、やや引っかかりはある。特に、Typeableの制約ゆえに、多相なアクションはEff
に持ち上げることができず、MonadState
のような関数従属性もない。計算量のオーダーの改善のため「慈悲なき反省」*2を起用しているが、動作は低速である。
より自由な作用
Oleg Kiselyov, Hiromi Ishiiにより、Operationalモナドをベースにした新しい実装が提案された*3。内部表現にはOkasakiの連結可能キューではなく二分木を使っており、パフォーマンスが向上している。
Hackageにはfreerがアップロードされ、freer-effectsに引き継がれた。速度は見違えるほど改善したものの、多相な型が持ち上げられない、型推論がうまくいかないと言った根本的な問題は解決されていない。
全部盛り
extensibleは拡張可能レコードのライブラリである。型に名前をつけて管理するため、多相型が推論の邪魔になることはない。この仕組みを拡張可能作用に応用すれば多相性の問題を解決できると考え、2015年の4月には基礎部分を実装した。「慈悲なき反省」を取り込んだ自前のOperationalモナドライブラリmonad-skeleton
を、拡張可能ヴァリアントと組み合わせただけである。地味に画期的なmtlとの互換性もあったものの、あまり使うあてがなく、長時間放置していた。今年の2月になって急にモチベーションが向上し、まともな拡張可能作用のライブラリとして使えるようにAPIを整えた。
mtlの代替が基本の使い方である。まずはモジュールをインポートする。
import Control.Monad.State.Class import Data.Extensible.Effect import Data.Extensible.Effect.Default
アクションはmtl
と互換性がある。lens
の演算子などももちろん利用可能だ。このような多相なパラメータをもつアクションを試してみよう。
increment :: (Num a, MonadState a m) => m () increment = modify (+1)
アクションを実行するには、Data.Extensible.Effect.Defaultモジュールのrun...Def
とleaveEff
を使う。
runStateDef :: Eff (StateDef s ': xs) a -> s -> Eff xs (a, s) leaveEff :: Eff '[] a -> a
*Main> leaveEff $ runStateDef increment 0 ((),1)
ReaderT r (WriterT w (State s))
に対応するモナドはEff '[ReaderDef r, WriterDef w, StateDef s]
で、覚えるのは難しくないだろう。実行するにはrunReaderDef
, runWriterDef
, runStateDef
で、leaveEff
で締めくくる。
好きな名前を与えることもでき、同じ型を持つ作用を複数持たせることもできる。その場合、Eff '["foo" >: WriterEff String, "bar" >: WriterEff String]
のように書く。
名前付きのアクションはEff
が名前に入っている。OverloadedLabels拡張を使うと簡単に作用名を指定できる(GHC 8.0の型推論器の制約上、Proxyが必要だった)。
test :: (Associate "foo" (WriterEff String) xs, Associate "bar" (WriterEff String) xs) => Eff xs () test = do tellEff #foo "Hello " tellEff #bar "hoge" tellEff #foo "world" tellEff #bar "fuga"
実行するときはTypeApplicationsを使う。
> leaveEff $ runWriterEff @ "foo" $ runWriterEff @ "bar" test (((),"hogefuga"),"Hello world")
自前のアクションを持ち上げるにはliftEff
、分解するにはpeelEff1
を使おう。
liftEff :: forall s t xs a. Associate s t xs => Proxy s -> t a -> Eff xs a peelEff1 :: forall k t xs a b r. (a -> b -> Eff xs r) -- ^ 結果を返す -> (forall x. t x -> (x -> b -> Eff xs r) -> b -> Eff xs r) -- ^ アクションを解釈する -> Eff (k >: t ': xs) a -> b -> Eff xs r -- ^ bは状態を表す変数
物好きならば、peelAction0
を使ってみるのも面白いだろう。Action [a, b, c] r
はa -> b -> c -> E r
を固有アクションとして持つ作用E
に相当する型で、自分でデータ型を定義することなく作用を扱える。decEffects
にGADTの定義を食わせることで、アクションをTHで自動生成できるぞ。
peelAction0 :: forall k ps q xs a. Function ps (Eff xs q) -> Eff (k >: Action ps q ': xs) a -> Eff xs a
decEffects [d| data Blah a b x where Blah :: Int -> a -> Blah a b b |]
は
type Blah a b = "Blah" >: Action '[Int, a] b blah :: forall xs a b. Associate "Blah" (Action '[Int, a] b) xs => Int -> a -> Eff xs b
に変換される。
ベンチマーク
RWS相当のモナドのベンチマークをした。 さすがに階層の浅いモナド変換子には及ばないものの、新しめの実装であるfreer-effectsの倍以上の速度で、なかなか良好と言えるだろう。
benchmarking rws/extensible time 11.63 μs (11.33 μs .. 11.95 μs) 0.995 R² (0.993 R² .. 0.997 R²) mean 11.79 μs (11.49 μs .. 12.03 μs) std dev 847.4 ns (737.1 ns .. 984.8 ns) variance introduced by outliers: 76% (severely inflated) benchmarking rws/mtl time 909.5 ns (890.9 ns .. 928.4 ns) 0.997 R² (0.995 R² .. 0.998 R²) mean 896.4 ns (882.1 ns .. 916.9 ns) std dev 56.40 ns (45.09 ns .. 79.70 ns) variance introduced by outliers: 76% (severely inflated) benchmarking rws/mtl-RWS time 721.7 ns (713.3 ns .. 729.6 ns) 0.999 R² (0.998 R² .. 0.999 R²) mean 721.5 ns (714.2 ns .. 730.4 ns) std dev 27.47 ns (22.82 ns .. 38.35 ns) variance introduced by outliers: 54% (severely inflated) benchmarking rws/exteff time 150.5 μs (145.3 μs .. 156.1 μs) 0.992 R² (0.987 R² .. 0.995 R²) mean 148.5 μs (145.3 μs .. 152.7 μs) std dev 11.89 μs (9.974 μs .. 14.74 μs) variance introduced by outliers: 72% (severely inflated) benchmarking rws/effin time 40.77 μs (39.97 μs .. 41.87 μs) 0.994 R² (0.991 R² .. 0.997 R²) mean 41.97 μs (40.98 μs .. 43.27 μs) std dev 3.509 μs (2.887 μs .. 4.246 μs) variance introduced by outliers: 78% (severely inflated) benchmarking rws/freer-effects time 25.26 μs (24.60 μs .. 25.86 μs) 0.992 R² (0.983 R² .. 0.996 R²) mean 26.28 μs (24.55 μs .. 31.05 μs) std dev 8.954 μs (2.011 μs .. 17.52 μs) variance introduced by outliers: 99% (severely inflated)
Stackageのnightly-2017-07-31 (GHC 8.2)を使用した。
まとめ
既存の拡張可能作用の実装は、型とパフォーマンスの二つの問題を抱えていた。しかし、extensibleはその両方を解決し、拡張可能レコードも付いたお得なパッケージにまとまっている。実用的な局面にも十分に通用する使い心地を保証しよう。
リンク
*1:Oleg Kiselyov et al. http://okmij.org/ftp/Haskell/extensible/exteff.pdf, 2013
*2:Atze van der Ploeg and Oleg Kiselyov, Reflection Without Remorse, http://okmij.org/ftp/Haskell/zseq.pdf, 2014
generateの罠
vectorパッケージのData.Vector
にはgenerateという関数がある。
generate :: Int -> (Int -> a) -> Vector a
型から全てを知ることはできないが、だいたい想像通りgenerate n f
は[f 0, f 1, f 2, ...f (n - 1)]
からなるVector
を生成する。しかし、これは要素を評価はしない。生成されるのはあくまでサンクのVectorだ。
Prelude > import Data.Vector as V Prelude V> V.length $ V.generate 5 (const undefined) 5
vector
は速くて正格そうなイメージがあるが、ボックス化される方に関して、基本的に正格性は最小限なので注意しよう。どう工夫してもgenerate
だけで正格なVectorは作れないので、generateM
を使おう。
Prelude V> V.length $ runIdentity $ V.generateM 5 $ const $ pure $! undefined 5
Identityではダメなようだ…だが、継続モナドCont
を使うとうまくいく。
cont :: ((a -> r) -> r) -> Cont r a runCont :: Cont r a -> (a -> r) -> r
V.length $ flip runCont id $ V.generateM 5 $ \_ -> cont $ \k -> k $! undefined *** Exception: Prelude.undefined
継続最高。
波打たせるものの正体(エクステンシブル・タングル)
Haskell Advent Calendar 11日目
リアルワールドなHaskellerは、幾十ものフィールドを持つ大きなレコードをしばしば扱う羽目になる。モナディックにレコードを構築したい場合、RecordWildCards
拡張を用いて以下のようにするのが定番だ。
import System.Random data Rec = Rec { foo :: String, bar :: Int, baz :: Double, qux :: Bool } makeRec = do foo <- getLine bar <- length <$> getLine baz <- readLn qux <- randomIO return Rec{..}
しかし、<-
の右辺が大きい、フィールドの数が多い、といったリアルワールドにありがちな事象が掛け算されれば、定義は巨大になってしまう。
そこで登場するのがextensibleの拡張可能レコードである。たとえアプリカティブに包まれていようと、一発でレコードを鋳出すことができる。
extensibleについておさらいしよう。根幹となるのは拡張可能な積だ。
(:*) :: (k -> *) -- 顕現せし型 -> [k] -- 要素のリスト -> *
拡張可能な積(:*)
は顕現せし型と要素のリストの二つの型パラメータを持つ。顕現せし型は、要素を現実、つまり種*
の型に対応させる型である。例えば、T :* '[A, B, C]
はタプル(T A, T B, T C)
と等価となる。
普通のレコードとして使う場合、顕現せし型としてField Identity
を帯びる。
type family AssocValue (kv :: Assoc k v) :: v where AssocValue (k ':> v) = v newtype Field (h :: v -> *) (kv :: Assoc k v) = Field { getField :: h (AssocValue kv) }
これにより、以下の型は前に定義したRec
と等価になる。
type Rec = Field Identity :* ["foo" :> String, "bar" :> Int, "baz" :> Double, "qux" :> Bool]
レコードを作るには、各フィールドごとに関数を定義し、hgenerateFor
にほぼ直接渡すだけでよい。
インスタンス宣言でフィールドの型を二度書かないといけないのは少々面倒だが、これで拡張性を得られた。
{-# LANGUAGE TemplateHaskell, DataKinds #-} import Control.Monad.Trans.Class import Data.Extensible import Data.Functor.Identity import Data.Proxy mkField "foo bar baz qux" type Fields = ["foo" :> String, "bar" :> Int, "baz" :> Double, "qux" :> Bool] type Rec = Record Fields class MakeRec kv where make :: proxy kv -- kvを明示しないといけない -> IO (AssocValue kv) instance MakeRec ("foo" :> String) where make _ = getLine instance MakeRec ("bar" :> Int) where make _ = length <$> getLine instance MakeRec ("baz" :> Double) where make _ = readLn instance MakeRec ("qux" :> Bool) where make _ = randomIO makeRec :: IO Rec makeRec = hgenerateFor (Proxy :: Proxy MakeRec) (\m -> Field . pure <$> make m)
しかし、アクションに依存関係があるとこの方法は使えない。do記法とRecordWildCardsの定番スタイルでも、ステートメントの順番をうまく並べ替えなければならず、別の定義に切り分けるというのもそう簡単ではない。
そこでエクステンシブル・タングルという新しいアプローチを始めた。拡張可能な積h :* xs
を構築するためのモナド変換子、TangleT h xs
を導入する。
TangleT :: (k -> *) -- 顕現せし型 -> [k] -- 要素のリスト -> (* -> *) -- 礎のモナド -> * -- 結果の型
先ほどの例をTangleT
を使うように変えると以下のようになる。lift
を入れてTangleT (Field Identity) Fields
を返している以外は特に違いはない。
class MakeRec kv where make :: proxy kv -> TangleT (Field Identity) Fields IO (AssocValue kv) nstance MakeRec ("foo" :> String) where make _ = lift getLine instance MakeRec ("bar" :> Int) where make _ = lift $ length <$> getLine instance MakeRec ("baz" :> Double) where make _ = lift readLn instance MakeRec ("qux" :> Bool) where make _ = lift randomIO
まずmake
を一か所に集める。型合わせのためにComp
が使われている点に注意。
-- newtype Comp (f :: j -> *) (g :: i -> j) (a :: i) = Comp { getComp :: f (g a) } tangles :: Comp (TangleT (Field Identity) Rec m) (Field Identity) :* Rec tangles = htabulateFor (Proxy :: Proxy MakeRec) $ \m -> Comp $ Field . pure <$> make m)
これをレコードに変換するのがrunTangles
だ。最初の引数には先ほどのtangles
、次は既知の値(ある場合)を渡す。既知の値はないのでwrench Nil
を渡す。
-- runTangles :: Monad m -- => Comp (TangleT h xs m) h :* xs -- -> Nullable h :* xs -- -> m (h :* xs) makeRec :: IO (Record Rec) makeRec = runTangles tangles (wrench Nil)
このモナドの価値を決める必殺技とも言うべき固有アクション、それはlasso
である。
lasso
にフィールド名を渡すとその値が返ってくる。二度以上呼んでも実際の計算は一回しか行われないのがポイントである。
lasso :: forall k v m h xs . (Monad m, Associate k v xs, Wrapper h) => FieldName k -> TangleT h xs m (Repr h (k ':> v))
これにより、依存関係をいくら孕んでいようとも、簡単にレコードを構築できる。foo
とbaz
が文字列として一致しているか確かめるコードはこんな感じになる。
instance MakeRec ("qux" :> Bool) where make _ = do str <- lasso foo x <- lasso baz return $ str == show x
しかし、なぜこんなものが必要になったのか――その動機は「波打たせるもの」にある。
メッセージフォーマット勉強会にて、このプログラムの存在について軽く触れた*1。監視対象はログを出力し、ネットワークを通じてオフィスのサーバーに配送される。ビューア(監視プログラム)はリアルタイムでログを読み取り、GUIとして表示するが、一つの問題が生じる。
「あるイベントが発生した回数」を表示したいとしよう。監視プログラムはそのイベントが出てくるたびに内部のカウンタを増やす。しかし、少し前の値を見ようとシークした途端、その値は無意味なものになってしまう。そういった値は監視対象がログに含めるという手もあるが、遠隔地にあり帯域も制限されているだけでなく、パフォーマンス上の要求から処理を増やしたくないため、なるべくこちら側で解決したい。そこで手を打つべく開発されたのが「波打たせるもの<コラゲーター>」である。
「波打たせるもの」は、監視対象と監視プログラムの中間に設置するプロセスであり、ログを読み取って監視プログラムのためのデータを生成する。出力はビューアに必要な情報(コラゲーションと呼ぶ)をすべて含んでおり、ストリームのどこにシークしても、イベントの回数などの累積的な値を正しく表示できる。結果として、ビューアはインターフェイスを除けば状態が不要になり、コードの簡略化にも繋がる。
波打たせるものが導入される前のログも読めなければいけないので、ビューアにもビルトイン・コラゲーターが内蔵されている。ビルトイン・コラゲーターは「ログとコラゲーションを読む」「部分的に非互換なコラゲーションを読む」「ログのみを読む」の3つのケースに対応する必要があり、ここがこのエクステンシブル・タングルの力の見せ所になる。
ログとコラゲーションが存在する場合、ビューアは何もする必要がない。そこでコラゲーションの有無で条件分岐するのではなく、runTangles
の二番目の引数にコラゲーションを渡す。こうすると、コラゲーションの一部が読み取れない場合も、必要な部分のみを計算できる。ログのみの場合は空っぽのレコードを渡せばすべて自力で賄う。
前回の記事*2でも触れたように、extensible
のレコードはデシリアライザを非常に簡潔に実装できる。また、顕現せし型を例えばField Maybe
とすれば、部分的なデシリアライズも表現できる。エクステンシブル・タングルと組み合わせることで、パフォーマンスを損なわずに拡張性と互換性を持つロジックを記述できるのだ。
エクステンシブル・タングルが必要な場面は非常に限られていると思うが、ここぞというとき有効であることは約束する。他の言語ではなかなか真似できない、Haskellらしい表現力を活かしていきたい。
割とすぐに始められるextensibleチュートリアル(レコード編)
ごきげんよう諸君。今回はextensibleについて説明しよう。
extensibleはその名の通り、拡張可能なデータ構造を提供するライブラリである。具体的には、型レベルのリストによって特徴づけられる積と和を提供する。非常に残念なことに、GHC 8.0.1ではコンパイラのバグのせいでそもそもライブラリがビルドできない*1。来たる8.0.2では修正されているので、それを待つほかない。
とにかく、ここでは積の応用技である拡張可能レコードについて紹介する。使い方は簡単だ。まず使いたいフィールド名をスペースで区切ってmkField
に渡す。
{-# LANGUAGE TemplateHaskell, DataKinds, TypeOperators, FlexibleContexts #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} import Data.Extensible import Control.Lens hiding ((:>)) mkField "name collective cry"
すると、フィールド名を表す値がTemplate Haskellによって自動生成される。仰々しい型をしているが気にせず先に進もう。
$ stack ghci --package lens --package extensible > :load tutorial.hs ... > :t name name :: forall (kind :: BOX) (f :: * -> *) (p :: * -> * -> *) (t :: (Assoc GHC.TypeLits.Symbol kind -> *) -> [Assoc GHC.TypeLits.Symbol kind] -> *) (xs :: [Assoc GHC.TypeLits.Symbol kind]) (h :: kind -> *) (v :: kind) (n :: Data.Extensible.Internal.Nat). (Labelling "name" p, Wrapper h, Data.Extensible.Internal.KnownPosition n, Extensible f p t, Elaborate "name" (FindAssoc "name" xs) ~ 'Expecting (n ':> v)) => Data.Extensible.Internal.Rig.Optic' p f (t (Field h) xs) (Repr h v)
ここで、動物の名前と、群れの呼び方、ある場合は鳴き声の擬声語を含むデータ型を定義する。
type Animal = Record [ "name" :> String , "collective" :> String , "cry" :> Maybe String ]
Record
は、フィールドの名前と型のリストをパラメータとして持つレコードの型だ。名前と型のペアは:>
で作る。
Record :: [Assoc k *] -> * (:>) :: k -> v -> Assoc k v
例としてハトとハクチョウを作ってみよう。
構築の方法はリストと同じと思ってよい。Nil
は空のレコードで、(<:)
で値を追加する。@=
でフィールド名と実際の値を指定する。
-- (@=) :: FieldName k -> v -> Field Identity (k :> v) -- infix 1 @= dove :: Animal dove = name @= "dove" <: collective @= "dule" <: cry @= Just "coo" <: Nil swan :: Animal swan = name @= "swan" <: collective @= "lamentation" <: cry @= Nothing <: Nil
作っても使えなければ意味がない、しかし心配はいらない。実は、最初に定義したフィールド名はLensとしても使えるレンズ沼仕様になっているのだ。
> swan ^. name "swan"
Lensなので当然更新もできる。地上のハクチョウはbankと呼ぶのでそれを反映させてみよう。
> swan & collective .~ "bank" name @= "swan" <: collective @= "bank" <: cry @= Nothing <: Nil
動物を取り、その集団を表す句を返す関数を定義できる。
collectiveOf :: Animal -> String collectiveOf a = unwords ["a", a ^. collective, "of", a ^. name ++ "s"]
> collectiveOf dove "a dule of doves" > collectiveOf swan "a lamentation of swans"
collectiveOf
はAnimal
型を引数として取るが、実は以下のように一般化できる。
collectiveOf :: (Associate "name" String s, Associate "collective" String s) => Record s -> String
もはや引数はAnimal
である必要はない。name
とcollective
をString
として持つなら、どんなレコードに対しても使える。これが拡張可能たる所以だ。
一見難解な仕組みに見えるが、実用するうえで必要なのはRecord
および:>
型とmkField
、@=
、<:
、Nil
だけであり、決して覚えるのが大変なものではない。同じ名前をどこでも使い回せる、lensとの相性がよいなどの性質があるので、従来のレコードの代わりに使うのも便利だ。
extensibleの拡張可能レコードは、ジェネリックな関数が書きやすいというメリットもある。以下のコードは、JSONのオブジェクトをレコードにするFromJSON
のインスタンスだ。
これは、GHCのジェネリクスを用いた実装(332行)と比べるとはるかに短い。
{-# LANGUAGE TemplateHaskell, DataKinds, TypeOperators, FlexibleContexts, UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} import Data.Extensible import Control.Lens hiding ((:>)) import Data.Aeson (FromJSON(..), withObject) import Data.Proxy import Data.String import GHC.TypeLits instance Forall (KeyValue KnownSymbol FromJSON) xs => FromJSON (Record xs) where parseJSON = withObject "Object" $ \v -> hgenerateFor (Proxy :: Proxy (KeyValue KnownSymbol FromJSON)) $ \m -> let k = symbolVal (proxyAssocKey m) in case v ^? ix (fromString k) of Just a -> Field . pure <$> parseJSON a Nothing -> fail $ "Missing key: " ++ k
Forall
とhgenerateFor
がこのインスタンスの要になっている。Forall c xs
は、「xsのすべての要素が制約cを満たす」という制約で、この場合、すべてのフィールドについて、名前は型レベル文字列、値の型はFromJSON
のインスタンスであることを示している。hgenerateFor
はその制約を用いてレコードを一気に構築する関数だ。
Forall :: (k -> Constraint) -> [k] -> Constraint hgenerateFor :: (Applicative f, Forall c xs) => proxy c -> (forall (x :: k). c x => Membership xs x -> f (h x)) -> f (h :* xs)
Membership xs x
はx
がxs
の要素であることを表す型である。ここでは、フィールド名をsymbolVal
とproxyAssocKey
でString
に変換するために使われている。Field . pure
は具体的なフィールド名を指定せずにフィールドを構築する関数である。
> decode <$> fromString <$> getLine :: IO (Maybe Animal) {"name": "zebra", "collective": "dazzle", "cry": "whoop"} Just (name @= "zebra" <: collective @= "dazzle" <: cry @= Just "whoop" <: Nil)
リポジトリにはToJSONのインスタンスの例もある。
レコードに関しては、GHCのGenericsとは比べ物にならないほど簡潔に汎用的なインスタンスを記述できる。もし自前のクラスについてジェネリックなインスタンスを定義したいが、複雑で実装が難しいという場合は、ぜひextensibleを使ってみてほしい。
ここで紹介したのはextensibleの一部に過ぎない。次回は、レコードと対をなす構造、拡張可能ヴァリアントについて紹介したい。
写真の撮り方
物体の発する光や反射した光を結像し、何らかの媒体に記録したものを写真と呼ぶ。カメラと呼ばれる道具には「撮影」という動作が必ず定義されており、撮影によって内部状態に画像を記録できる。内部状態を取り出して処理する(現像)ことで写真が得られる。大抵のカメラには以下のようなパラメータがあり、それらを最適化するのが撮影者の仕事になる。
焦点(Focus)
はっきりとした像を得るには、光学系の焦点をそれに合わせる必要がある。最近のカメラは対象物の距離を測定し、自動で焦点を合わせる(オートフォーカス)機能を持っているものもある。
焦点距離(Focal length)
焦点距離が長いほど像は拡大されて見えるようになる。デバイスの規格に依存して、ヒトの視野に近いと考えられている40度前後の画角を持つような焦点距離が決まり、その焦点距離を持つレンズを標準レンズと呼ぶ。APS-Cの規格のセンサーを持つデジタル一眼レフカメラにおける主流は35mmである。レンズによっては動的に焦点距離を変えられるものもある。
f値(f-ratio)
絞りとも呼ばれる。焦点距離を、レンズの有効口径(無限遠点にある点光源が通過できる光束の直径)をとおくと、対応するf値は以下のように表される。
多くのカメラは有効口径を動的に変える機構を備えている。f値が高いほど、像がはっきりと写る範囲(被写界深度)も大きくなる。
露出時間(Exposure time)
現在を0とする時刻tにおける世界の状態をとおく。世界の状態にカメラを適用し、それを定積分することによって画像が得られる。このときの積分する範囲が露出時間である。
露出時間が長ければ長いほど、世界の状態の変化が画像に表れる。そのため、ものを一定の位置に保つのが難しい生体がカメラを保持する場合、露出時間は10ミリ秒のスケールまで短くする。
ISO感度(Film speed)
ISO感度は理想的にはカメラ内の定数係数であり、ISO感度が2倍ならば、半分の露出時間、もしくは倍のf値で同じ明るさの画像が得られる。技術的制約により、ISO感度と画質はトレードオフの関係にある。
露出値(Exposure value)
露出はf値と露出時間によって決まる相対的な光量の尺度である。APEXと呼ばれる体系では露出値を以下のように定義しており、広く使われている。
撮影の流れ
ISO感度を簡単に変更できるカメラの場合、以下のようにするとやりやすい。
- 目的の焦点距離を決め、レンズを取り付ける。
- 目的の被写界深度を持つよう、f値を決定する。
- 対象物に焦点を合わせる。
- 露出時間を決定する。
- 適切な明るさとなるようにISO感度を決定する。もしISO感度が限界に達した場合、露出時間とf値を見直し、それでも明るすぎる場合は減光フィルターをカメラに取り付けて光量を減らす。暗すぎる場合はあきらめる。
- レリーズボタンを押して撮影する。
近年ではソフトウェアで現像できることも多い。筆者はAdobe Lightroomを使用している。
所見
近年のカメラは各パラメータを自動で計算する機能が備わっていることが多いが、目的とする表現が得られないことが多いため、咄嗟の場面でパラメータを気にせずに撮影したいという状況以外は推奨しない。
レリーズボタンにオートフォーカスを統合し、撮影する直前にオートフォーカスするようになっているカメラが多いが、予期せぬほうへ焦点を変えてしまう恐れがあるため、筆者は別のボタンにオートフォーカスをアサインし、レリーズボタンはレリーズボタンとしてのみ機能するように設定している。
まとめ
写真撮影は最適化すべきパラメータが少なく、比較的とっつきやすい。携帯電話などに内蔵された簡易なカメラしか使ったことがない人でも、本記事で触れたようなパラメータについて考えてみれば、本格的な写真撮影をする動機がきっと生まれるはずだ。
今のところ比較的簡単なモナドの作り方
準備
モナドを作るには、どんなモナドを作りたいか考える。モナドは一般に、どのようなアクションが使えるかによって特徴付けられる。その点ではオブジェクト指向におけるインターフェイスとよく似ている。
では、foo :: String -> M Bool
とbar :: M Int
という二種類のアクションを持つモナドを作るとしよう。まず、どんなアクションが使えるかを表すデータ型を定義する。
{-# LANGUAGE GADTs #-} data MBase x where Foo :: String -> MBase Bool Bar :: MBase Int
GADT(一般化代数的データ型)の各データコンストラクタがアクションに対応する。GADTsを使ったことがなくても心配してはいけない。引数の型と結果の型に着目しよう。
モナドにする
monad-skeletonをインストールする。
$ stack install monad-skeleton
モジュールをインポートし、先ほどのMBaseを使い、Mを定義する。bone :: t a -> Skeleton t a
でMBase
の値をM
の値にする。
{-# LANGUAGE GADTs #-} import Control.Monad.Skeleton data MBase x where Foo :: String -> MBase Bool Bar :: MBase Int type M = Skeleton MBase foo = bone . Foo bar = bone Bar
Mはもう、モナドになっている。拍子抜けするほど簡単だ。
:t bar >>= foo . show bar >>= foo . show :: Skeleton MBase Bool
モナドを使う
作っても使わなければ意味がない -- 誰か
アクションにdebone :: Skeleton t a -> MonadView t (Skeleton t) a
を適用すると、MonadViewというデータ型の値が得られる。
data MonadView t m x where Return :: a -> MonadView t m a (:>>=) :: t a -> (a -> m b) -> MonadView t m b
Return a
はそのアクションが実質的にreturn a
であること、t :>>= k
は最初に実行すべきアクションがt
であることを意味する。k
にt
の結果を渡すと、その次に実行すべきアクションが得られる。これらを使うと、Mを実際に解釈する関数を定義できる。ここでは、foo
を「与えられた文字列の長さがある値より短いか判定する」、bar
を「判定基準を返す」ものとして定義しているが、型さえ合えば実際はなんでもよく、一つのM
に対して複数の解釈を与えることさえ可能だ。
runM :: Int -> M a -> a runM n m = case debone m of Foo str :>>= k -> runM n $ k $ length str <= n Bar :>>= k -> runM n $ k n Return a -> a
monad-skeletonはDSL(ドメイン特化言語)を作るのに適している。何より、簡単に使えるのが売りなので、初心者にもおすすめだ。
runMのような「モナドを解釈する関数」に内部状態を持たせられたら便利なのに、と思う人もいるかもしれない。実はこれにも既に解決法があるので、次の機会に紹介しよう。