これから Haskell を書くにあたって
-
Upload
tsuyoshi-matsudate -
Category
Technology
-
view
1.437 -
download
0
Transcript of これから Haskell を書くにあたって
目次
3頁 … はじめに9頁 … GHC 7.8 からの変更点
69頁 … Haskell が遅いと言われるワケとか106頁 … 知らないと損する言語拡張たち150頁 … FFI の話161頁 … おまけ
GHC 7.8 からの変更点
10頁 … Monad Proposal22頁 … Foldable43頁 … Traversable57頁 … Prelude の関数の変化63頁 … 変更後の標準の型クラス全体図67頁 … OpenGL から見る実装例
Monad Proposal
正確には Functor-Applicative-Monad ProposalMonad のリファクタリングの提案
Monad は正式に Applicative の子型クラスに
話題自体は2014年よりも前から始まっていた。最初期の Haskell では繋がっていたらしい。しかし…
親型クラスのメソッドをデフォルトで実装する機能がなかった。
Monad Proposal
instance 宣言のおさらい記法
* instance C1 T1 where ...* instance C1 a => T1 a where ...* instance C1 a => Cn (T1 a) where ...
型クラスの実装をデータ型に対して与える
Monad Proposal
型変数のあるデータ型への実装
instance C1 a => T1 a where f x = ...instance (C1 a, C2 a, ...) => T1 a where f x = ...
インスタンスを与える型変数を明示する
Monad Proposal
特定のインスタンスを持つデータ型への実装
instance C1 a => Cn (T1 a) where f x = ...instance (C1 a, C2 a,...) => Cn (T1 a) where f x = ...
特定する型クラスも明示する
Monad Proposal
自動で実装できる型クラス➜ deriving 句で導出できる型クラス
Eq, Ord, Enum, Bounded, Read, Show
コンストラクタの並び等から自明に導ける。(もちろん自前で書いてもよい)
Monad Proposal
Monad と Applicative の実装は?
❌自明では導くことができない
これら 2 つはコンテナの中身をどう扱うかの話
❌Applicative は Monad の親クラス
Monad のインスタンスを持つ型が直接依存しているわけではない。
Monad Proposal
公式の対応
return = pure
Monad の return の実装を Applicative の pure にリファクタリング
<*> = ap
コンテナから値を取り出し関数を適用するという点で同義
Monad Proposal
公式の対応
* pure :: a -> f a* return :: a -> m a* <*> :: f (a -> b) -> f a -> f b* ap :: m (a -> b) -> m a -> m b
コンテキストが変わるだけ
Foldable
期待される性質
✔ foldr f z t = appEndo (foldMap (Endo . f) t) z✔ foldl f z t = appEndo (getDual (Dual . Endo . flip f) t)) z✔ fold = foldMap id
Foldable
期待される性質
newtype Endo a = Endo {appEndo :: a -> a}
➜ instance Monoid (Endo a) where mempty = Endo id Endo f `mappend` Endo g = Endo (f . g)
Foldable
期待される性質
newtype Dual a = Dual {getDual :: a}
➜ instance Monoid a => Monoid (Dual a) where mempty = Dual mempty Dual x `mappend` Dual y = Dual (x `mappned` y)
Foldable
Monoid に期待される性質
✔ mappend mempty x = x✔ mappend x mempty = x✔ mappend x (mappend y z) = mappend (mappend x y) z✔ mconcat = foldr mappend mempty
Foldable
話を戻して Foldable の話
foldr f z t = appEndo (foldMap (Endo . f) t) zfoldl f z t = appEndo (getDual (Dual . Endo . flip f) t)) z
要素の畳み方を定義する必要がある
Foldable
話を戻して Foldable の話
foldMap :: Monoid m => (a -> m) -> t a -> mfoldMap f = foldr (mappend . f) mempty
Foldable のインスタンスを持つコンテナの要素をMonoid に型変換させる
Foldable
話を戻して Foldable の話
foldr :: (a -> b -> b) -> b -> t a -> bfoldr f z t = appEndo (foldMap (Endo #. f) t) z
コンテナの中身をモノイドのコンテキストに乗せながら畳んでいく
Foldable
話を戻して Foldable の話
foldr :: (a -> b -> b) -> b -> t a -> bfoldr f z t = appEndo (foldMap (Endo #. f) t) z
(´・_・`).oO((#.) ってなんだ?)
Foldable
話を戻して Foldable の話
(#.) :: (b -> c) -> (a -> b) -> (a -> c)(#.) _f = coercecoerce :: Coercible * a b => a -> bcoerce = let x = x in x
安全かつ強制的に型変換させるための関数
Foldable
Foldable まとめ
✔ 要素が Monoid のインスタンスであれば どんな値も畳むことができる✔ Endo は関数合成用のコンテナ Dual は値合成用のコンテナ✔ Monoid 用のデータ型は他にもあるよ!
Traversable
最低限実装が必要なメソッド
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)traverse f = sequenceA . fmap f
コンテナの要素を走査しながら関数を適用していく
Traversable
最低限実装が必要なメソッド
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)traverse f = sequenceA . fmap f
コンテナが入れ替わることに注意
Traversable
最低限実装が必要なメソッド
sequenceA :: Applicative f => t (f a) -> f (t a)sequenceA = traverse id
走査するだけ(中の値自体には変更を加えない)
Traversable
最低限実装が必要なメソッド
sequenceA :: Applicative f => t (f a) -> f (t a)sequenceA = traverse id
コンテナが入れ替わることに注意(大事なことなのでry)
Traversable
期待される性質
✔ t . traverse f = traverse (t . f)✔ traverse Identity = Identity✔ traverse (Compose . fmap g . f) = Compose . fmap (traverse g) . traverse f
Traversable
期待される性質
✔ t . sequenceA = sequenceA . fmap t✔ sequenceA . fmap Identity = Identity✔ sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA
Traversable
期待される性質
✔ t . traverse f = traverse (t . f)✔ t . sequenceA = sequenceA . fmap t
関数合成の自然性の保証(関数合成の効率化)
Traversable
期待される性質
✔ traverse Identity = Identity✔ sequenceA . fmap Identity = Identity
同一性の保証(id がちゃんとそのままの値を返すこと)
Traversable
期待される性質
✔ traverse (Compose . fmap g . f) = Compose . fmap (traverse g) . traverse f✔ sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA
結合則の保証
Traversable
Traversable まとめ
✔ コンテナの要素を走査し、更新する✔ その際にコンテナの構造を均して簡潔化する✔ 中の要素は Applicative の実装が必要 (更新してコンテナに再適用させるため)
Prelude の関数の変化
意外とリファクタリングされていた
Foldableに所属
null, length, elem, notElem, maximum, minimum, sum, product, and, or, any, all, concat, concatMap, mapM_(forM_), sequence_
OpenGL から見る実装例
Foldable と Traversable はどう実装されているか
instance Foldable TexCoord4 foldr f a (TexCoord4 x y z w) = x `f` (y `f` (z `f` (w `f` a))) foldl f a (TexCoord4 x y z w) = ((((a `f` x) `f` y) `f` z) `f` w)
テクスチャ座標の畳込み等(一部抜粋)
OpenGL から見る実装例
Foldable と Traversable はどう実装されているか
instance Traversable TexCoord4 where traverse f (TexCoord4 x y z w) = pure TexCoord4 <*> f x <*> f y <*> f z <*> f w
座標変換等(一部抜粋)
Haskell が遅いと言われるワケとか
70頁 … 評価しないものは溜まる79頁 … データコンストラクタの中身81頁 … 文字列の計算量とメモリ使用量85頁 … タプル88頁 … 自作データ型の高速化97頁 … 型クラスの仕組み
102頁 … 競技プログラミングでの Haskell
評価しないものは溜まる
評価予定の値もスタックに
よくある階乗の計算
fact 0 = 1fact n = n * fact n
実は未評価なまま次の計算に渡されている(評価されるのはパターンマッチの瞬間である)
評価しないものは溜まる
評価予定の値もスタックに
しかし呼び出す回数が増えるにつれて、スタックはどんどん溜まっていき、いずれは溢れてしまう➜ 渡された値も未評価のまま…
この現象をスペースリークという
評価しないものは溜まる
その場で評価させる方法
階乗の例に適用してみる
fact 0 = 1fact n = n `seq` n * fact (n - 1)
これにより、 n は seq 関数に渡され、その場で評価されるようになった
文字列の計算量とメモリ使用量
計算量多そう…
: P
"Hello"(UTF-8)
P P
C# Char#
'H'
P P
C# Char#
'e'
P
C# Char#
'l'
P P P
C# Char#
'l'
P : : : :
文字列の計算量とメモリ使用量
計算量多い(確信)
"Hello"(UTF-8)
: P P P
C# Char#
'H'
P P
C# Char#
'e'
P
C# Char#
'l'
P P P
C# Char#
'l'
P : : : :
⑲
⑱
⑰⑯
⑮⑩⑤
⑭
⑬
⑫⑪
⑨
⑧
⑦⑥
③
②① ㉒㉑ ㉓
④
⑳
1 文字だけでも 4 回以上の計算が…
文字列の計算量とメモリ使用量
メモリ食いすぎィ!
構築子: 1 word型引数: 1 word / arg文字列 1 文字分 = 5 words構築子(:) + ポインタ * 2 + Char のサイズ1 word 2 words 2 words
C# + Char# ※32bit: 20 bytes, 64bit: 40 bytes
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
Int, Integer, Float, Double, Char, Word[N], Int[N]
これらはライブラリではただの Unpack 可能な型➜ プリミティブな値は処理系が保持
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
data T a = T {-# Unpack #-} !Int
T Int#
プリミティブな値がそのまま型引数に収まる✔ 計算量とメモリ消費量が減る✔ 生の値なので遅延評価不可 (正格性フラグ必須)
ただのバイナリ
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
data T a = T {-# Unpack #-} !Int
T Int#
プリミティブな値がそのまま型引数に収まる❌ 構築子が 1 つだけの型に限る❌ 通常の型と同じようには使えない
ただのバイナリ
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
data T a = T {-# Unpack #-} !Int
T Int#
プリミティブな値がそのまま型引数に収まる❌ 複数あると処理系が判断できない❌ 通常の型はポインタであると期待される
ただのバイナリ
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
data T a = T {-# Unpack #-} !Int
T Int#
プリミティブな値がそのまま型引数に収まる❌ 複数あると処理系が判断できない❌ 許してしまうと GC の対象になり、 厄介なバグを生みやすくなる
ただのバイナリ
自作データ型の高速化
標準にあるハッシュ付きの型は Unpack 可能
data T a = T {-# Unpack #-} !Int
T Int#
プリミティブな値がそのまま型引数に収まる❌ 複数あると処理系が判断できない⚠ 不本意な再ボックス化を防ぐために 最適化オプションを付けるべき
ただのバイナリ
競技プログラミングでの Haskell
速度にシビアな問題では死ぬ
✔ 主に TLE で。➜ 標準の文字列や List 等では間に合わない 問題がある➜ それを埋め合わせるライブラリが使えない➜ そもそも型の構造的に辛い…
競技プログラミングでの Haskell
速度にシビアな問題では死ぬ
対策✔ 言語拡張を導入(詳細は後述)➜ UnboxedTuples➜ OverloadedStrings(※)➜ OverloadedLists(※)➜ BangPatterns
競技プログラミングでの Haskell
Haskell が使える競プロサービス
AtCoder✔ GHC 7.8 以降
Codeforces✔ GHC 7.8 以降他
❌ (GHC がインストールされて)ないです…
知らないと損する言語拡張たち
107頁 … リテラルを置き換える117頁 … 計算量を削減する120頁 … データ型の表現力を向上する122頁 … 型クラスの表現力を向上する148頁 … 評価戦力を切り替える
リテラルを置き換える
Overloaded Strings
✔ 文字列(文字のリスト) を IsString の インスタンスを持つ別の何かに昇格 例) ByteString, Text IsString さえ実装されていればどんな型でもよい
リテラルを置き換える
Overloaded Strings
ByteString の例
instance IsString ByteString where fromString = packChars
文字列を packChars に丸投げする
リテラルを置き換える
Overloaded Strings
Text の例
instance IsString Text where fromString = pack
文字列を pack に丸投げする
リテラルを置き換える
Overloaded Strings
Data.ByteString を import してると
"Hello" :: ByteString
Data.Text を import してると
"Hello" :: Text
リテラルを置き換える
Overloaded Lists
✔ 何らかの List を IsList のインスタンスを持つ 別の何かに昇格 例) vector IsList さえ実装されていればどんな型でもよい
リテラルを置き換える
Overloaded Lists
Vector の例
instance Ext.IsList (Vector a) where type Item (Vector a) = a fromList = fromList fromListN = fromListN toList = toList
リテラルを置き換える
Overloaded Lists
Vector の例
type Item (Vector a) = a
Item (Vector a) という型の組み合わせを型 aとして扱わせる言語拡張(ここでは触れない)See: 7.7 Type families - The User's Guide
リテラルを置き換える
Overloaded Lists
Vector の例
fromList = fromListN
List を Vector.fromListN に丸投げ(List の最初の N 要素だけを Vector に入れる)
計算量を削減する
Unboxed Tuples
Tuple からタグ(ポインタ部分)を取り除く➜ ただの限定的な連続領域になる➜ ポインタを挟まず、要素がスタックや レジスタに直接置かれる➜ Unpack されていない要素は通常通り 遅延評価される
計算量を削減する
Unboxed Tuples
Tuple からタグ(ポインタ部分)を取り除く❌ (多層的な)型や関数の引数には渡せない➜ 型ではない。➜ ポインタを持っていない➜ 引数ではない部分でなら問題ない (返り値にする時や case 式など)
計算量を削減する
Unboxed Tuples
(Int, Int)
(,) P P
I# Int# I# Int#
(# Int, Int #)
I# Int# I# Int#
連続した領域として扱われる
データ型の表現力を向上する
GADTsデータコンストラクタにも関数と同じ型注釈を使えるようにする
data T a = T1 | T2 a (T a) ➜ data T a where
T1 :: T a T2 :: a -> T a -> T a
型クラスの表現力を向上する
Multi Param Type Classes
型クラスに複数の型変数を許可する➜ メソッドが複数の異なるインスタンスを 受け取れるようになる
class C a b where f :: a -> b -> b
型クラスの表現力を向上する
Functional Dependencies
型変数の関係性を明示する➜ ある型が決まれば別の型も一意に決まる という(依存)関係
class C a b | a -> b where
型クラスの表現力を向上する
Flexible Instances
通常時のルール✔ 1 つの型クラスにつき型変数は 1 つまで✔ 型変数の重複は許されない✔ 制約は型変数にのみ与えられる✔ 実装は必ずしなければならない
型クラスの表現力を向上する
Flexible Instances
拡張導入後のルール➜ 多変数型クラスの実装を与えてもよい➜ 型変数は重複していてもよい➜ 代数的データ型を混ぜてもよい➜ 宣言のみであってもよい
型クラスの表現力を向上する
Flexible Instances
拡張導入後のルール✔ instance C a b where ...✔ instance C a a where ...✔ instance C a Int where ...✔ instance C a b (=> ...)
型クラスの表現力を向上する
Flexible Instances
拡張導入後のルール✔ 型シノニムも宣言に書ける
data T a = T1 | T2 a (T a)type TT = T Intinstance Eq TT where ...
型クラスの表現力を向上する
Flexible Instances
例1class C a binstance C a Intinstance C Int b
a と b のどちらに Int があっても特定できてしまう
型クラスの表現力を向上する
Flexible Instances
例2class C a binstance C Int binstance C Int [b]
多相型なので一番目の b にも List があり得る
型クラスの表現力を向上する
Flexible Contexts
✔ それ以外は Flexible Instances と同様に書ける➜ f :: C a b => a -> b -> b➜ f :: C a a => a -> a -> a➜ f :: C a (T b) => a -> a -> T b➜ f :: C a TT => a -> a -> TT
型クラスの表現力を向上する
Overlapping Instances
以下のインスタンスが定義されているとする
instance C a Intinstance C Int binstance C Int Int
型クラスの表現力を向上する
Overlapping Instances
この内、最も特殊性が高いのは茶色の定義
instance C Int binstance C a Intinstance C Int Int
型クラスの表現力を向上する
Overlapping Instances
よって、:: C Int Int -> Int -> Int -> aという制約に対してinstance C Int Intの定義がマッチされる
型クラスの表現力を向上する
Incoherent Instances
:: C Int Int -> Int -> Int -> aという制約に最も適合するインスタンスはinstance C Int Intだが、これがない場合は
型クラスの表現力を向上する
Incoherent Instances
⚠ どのインスタンスが選ばれるのかは、制約を 与えられた関数が呼び出されるまで わからない➜ インスタンスの選択を遅延させている➜ 同じ制約のつもりでも違うインスタンスを 選択される可能性がある
FFIの話
151頁 … 呼び出せるプログラミング言語154頁 … 呼び出し方156頁 … Haskell での型の扱い158頁 … ByteString から見る実装例160頁 … hmatrix から見る実装例
呼び出し方
構文(import)
foreign import callconv impent var :: ftypecallconv := ccall | stdcall | cplusplus | jvm | dotnetimpent := [string](呼び出すヘッダの名前と関数の名前。スペース区切り。なくてもよい)
呼び出し方
構文(export)
foreign export callconv expent var :: ftypecallconv := ccall | stdcall | cplusplus | jvm | dotnetexpent := [string](呼び出すヘッダの名前と関数の名前。スペース区切り。なくてもよい)
ByteString から見る実装例
C の型を操作するので unsafe
foreign import ccall unsafe "string.h memchr"c_memchr :: Ptr Word8 -> CInt -> CSize -> IO (Ptr Word8)
ByteString から見る実装例
C の型を操作するので unsafe
memchr :: Ptr Word8 -> Word8 -> CSize -> IO (Ptr Word8)memchr p w s = c_memchr p (fromIntegral w) s
hmatrix から見る実装例
行列演算ライブラリ
foreign import ccall unsafe "sort_indexD"c_sort_indexD :: CV Double (CV CInt (IO CInt))
C と Haskell の間を往復するのでオーバーヘッドがすごいらしい…(まだ使ったことがないので詳しくはなんとも…)
あると便利な言語拡張たち
MagicHash
リテラルや型コンストラクタに ハッシュ(#)を使うことを許可する
✔ ハッシュ(#)自体には特に意味はない✔ 慣習的に内部にプリミティブな値を 持っている型に付与されている
あると便利な言語拡張たち
MagicHash
リテラルや型コンストラクタにハッシュ(#)を使うことを許可する
'x'# Char➜ #"foo"# Addr➜ #3# Int➜ #3## Word➜ #
あると便利な言語拡張たち
ParallelListComp
お馴染みフィボナッチ数列
fibs = 0 : 1 : [a + b | a <- fibs | b <- tail fibs]
これはfibs = 0 : 1 : zipWith (+) fibs (tail fibs)
と同じ
あると便利な言語拡張たち
PatternGuards
引数のパターンマッチの柔軟性が増す
通常f :: Int -> Boolf x | n <= x && x <= m = ... | otherwise = ...
あると便利な言語拡張たち
PatternGuards
引数のパターンマッチの柔軟性が増す
複数の条件を同時に書けるf :: Int -> Boolf x | n <= x && x <= m, x >= n * 100 = ... | otherwise = ...
あると便利な言語拡張たち
PatternGuards
引数のパターンマッチの柔軟性が増す
これは(||)を適用してるのと同じf :: Int -> Boolf x | n <= x && x <= m || x >= n * 100 = ... | otherwise = ...
CPP
実は C 由来のプリプロセッサを書ける
GHC の時のみそのコードが動くようにする
{-# LANGUAGE CPP #-}#ifdef __GLASGOW_HASKELL__-- Some Codes Here#endif
CPP
実は C 由来のプリプロセッサを書ける
GHC の時のみそのコードが動くようにする
{-# LANGUAGE CPP #-}#ifdef __GLASGOW_HASKELL__-- Some Codes Here#endif
CPP
実は C 由来のプリプロセッサを書ける
GHC の特定のバージョンでのみそのコードが動くようにする
{-# LANGUAGE CPP #-}#if __GLASGOW_HASKELL__==780-- Some Codes Here#endif