Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

How wonderful to be (statically) typed 〜型が付くってスバラシイ〜

4,748 views

Published on

PFI社内セミナーで強力な型システムがもたらす様々な恩恵について発表した際の資料です。

補足的な記事はこちらをどうぞ:
http://blog.konn-san.com/article/20120412/how-wonderful-to-be-typed

また、発表の模様は以下の ustream からご覧になれます。
http://www.ustream.tv/recorded/21781769

Published in: Technology
  • Be the first to comment

How wonderful to be (statically) typed 〜型が付くってスバラシイ〜

  1. 1. How wonderfulto be (statically) typed∼(静的に)型がつくってスバラシイ∼ 石井 大海
  2. 2. 自己紹介• 石井 大海 (id:mr_konn / @mr_konn)• 早稲田大学数学科三年 • 集合論や論理学、圏論に興味• 2010 Summer Intern から PFI でバイト中• Haskell Lover • Haskell でウェブ開発などしている
  3. 3. 本日のお題:型• 型がつくと何が嬉しいのか?• 型・・・むずかしそう • ムツカシイ理論ではなく応用例の紹介 • Haskell の布教ではない • 静的型付けの布教です。
  4. 4. Table of Contents• 型とは何か?──型付け、型推論、型安全• wonderful な型たち──型の表現力と応用 • いいとこ取り多相──型クラス • 型レベル函数:型族──キャスト、Map共通化など • そっちは残像だ──幽霊型はタグ • 洒落た多相の使い方──多相型でリーク防止 • Haskell、昇進す。──Data Kinds• その型、強力につき──依存型と証明
  5. 5. Table of Contents• 型とは何か?──型付け、型推論、型安全• wonderful な型たち──型の表現力と応用 • いいとこ取り多相──型クラス • 型レベル函数:型族──キャスト、Map共通化など • そっちは残像だ──幽霊型はタグ • 洒落た多相の使い方──多相型でリーク防止 • Haskell、昇進す。──Data Kinds• その型、強力につき──依存型と証明
  6. 6. 型とは何か?──型付け、型推論、型安全──
  7. 7. 型とは何か?“データ型(でーたがた、data type)とは、コンピュータにおけるデータの扱いに関する形式のことである。データタイプとも。データ型は、プログラミングなどにおいて変数そのものや、その中に代入されるオブジェクトや値が対象となる。” ── データ型 - Wikipedia
  8. 8. 型とは何か?• つまり? • 値の種類を区別するタグのようなもの• 世の言語は大きく「型付き言語」と「型無 し言語」に大別出来る • 型付き・・・C, Ruby, Haskell, LISP, Smalltalk... • 型無し・・・Brainf*ck, Lazy-K, ...
  9. 9. 型なし……?• 実用的なプログラミング言語は全部型がついてる気が する。 • Tcl :すべては文字列。でも実際には型別の内部表現 を持っている(らしい)。• そもそもの untyped・・・Untyped Lambda Calculus• 型あり言語の分類 • 動的型付け・・・LISP, Ruby, Smalltalk, JavaScript... • 静的型付け・・・Java, C, Haskell, OCaml, ...
  10. 10. 型なし vs. 動的型
  11. 11. 動的と静的の違い• 動的型付け • 型検査は実行時に行われる • (compile-time) untyped と云える? • 以後「型なし」は「動的型付け」の alias とする • インタプリタ言語はこれが多い• 静的型付け • 型検査はコンパイル時に行われる • 型が合わないものは実行する前に弾かれる
  12. 12. 型検査• 演算の値の型が合っているか検査• 合っていなかったら? • 言語によって弾いたり、キャストした りする。→ 型安全性
  13. 13. 型安全性?• ナイーヴな定義:「型検査を通ったものが変な挙 動をしない」• ちょっと厳密な定義 • Preservation: 型が評価の順番に依らない • Progress: 型が付いたら「刺さらない」 • 「刺さらない」ってなんや(意味論による)
  14. 14. 型安全性と言語• Haskell……型安全と云ってよい • 黒魔術を使わなければ型安全 • 停止性を込めると型安全じゃない • f x = f (f x) みたいなコードも型は付くが停止し ない• C……型安全じゃない! • cast や printf(“%s”, 12); などあぶない
  15. 15. 型推論• 函数の型を自動的に推論してくれる機能• 型推論なし・・・C, Java, ...• 型推論あり・・・Haskell, Scala, OCaml, ... • 健全性とか完全性とか色々概念があるらしいです がぶっちゃけよくわからない • 色んな強さの型推論がある、と云うことで。 • コンパイラが付けられない部分は手で注釈
  16. 16. ここからは• 静的型付け、型推論あり言語の布教• かつある程度の型安全性を持つ言語 • Preserve は当然仮定 • 停止しないくらいは許す • SEGV ったりしない、程度。
  17. 17. その前に……なぜ型?• 型はドキュメント • 型を見れば(ある程度)挙動がわかる • 勿論 Int → Int とか見せられてもわか らない ^^;(でも無いよりマシ)• 型検査通ればある程度正しく動く • どの程度?→型の表現力による!
  18. 18. 型はドキュメント• SMTP 通信用の型• 複数値を取るリスト型や、値を取らないかも しれない Maybe 型などが雄弁に語る data SMTPCommand = HELO ByteString | EHLO ByteString | MAIL Path [(ByteString, ByteString)] | RCPT Path [(ByteString, ByteString)] | DATA ByteString | RSET | VRFY ByteString | EXPN ByteString | HELP (Maybe ByteString) | NOOP (Maybe ByteString) | QUIT data Path = Path [ByteString] Mailbox | Empty
  19. 19. 型はドキュメント• 各 ByteString が何を指すのかわからない…… • 型に別名を付けるとわかりやすい! • 中身としては同じ。見通しはよくなる type Domain = ByteString type Param = (ByteString, ByteString) data SMTPCommand = HELO Domain | EHLO Domain | MAIL Path [Param] | RCPT Path [Param] | DATA ByteString | RSET | VRFY ByteString | EXPN ByteString | HELP (Maybe ByteString) | NOOP (Maybe ByteString) | QUIT data Path = Path [Domain] Mailbox | Empty
  20. 20. wonderful な型たち ──型の表現力と応用──
  21. 21. 型クラス• Ad-hoc 多相を実現する機構 • 複数の型に共通な演算をオーバーロード • cf. Parametric 多相:共通の「実装」を共有する• 代替手法 • 強力なモジュールシステム(like Alloy, Agda, OCaml, ...) • プリプロセッサ(camlp4?)
  22. 22. 数値型クラス class (Eq a, Show a) => Num a where (+), (-), (*) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a x - y = x + negate y negate x = 0 - x• 数値としての特徴を持つ型のクラス• 加減乗除、反転、絶対値、数字からの変換など• Num である為には Eq, Show でもある必要がある • 型クラスは、型への「制約」「述語」として考えられる
  23. 23. ファンクタ型クラス class Functor f where fmap :: (a -> b) -> f a -> f b• 普通の函数を「持ち上げ」られるコンテナのよう なもの • []、Maybe、IO などなど• Maybe a は値を持つが、 Maybe は値を持たない • Maybe :: * → * :型を受けとって型を返す「函 数」みたいなもの
  24. 24. 多引数型クラスの例 class Monad m => MonadState s m | m -> s where get :: m s get = state (s -> (s, s)) put :: s -> m () put s = state (_ -> ((), s)) state :: (s -> (a, s)) -> m a state f = do s <- get let ~(a, s) = f s put s return a• 「m は内部状態 s を持つモナド」 • (モナド=すごいコンテナ)• m を決めると s が決まる:「m → s」関数従属性
  25. 25. 型クラス:まとめ• 型の述語のようなもの• Ad-hoc 多相を綺麗に実現する• 函数従属性で依存関係を記述出来る
  26. 26. 型族• 型レベル函数のようなもの• 型別名を付けるものと、データを新た に定義するもの• 型クラスに付随する型族
  27. 27. 例1: MonadStateの書き換え class Monad m => MonadState m where type State m :: * get :: m (State m) get = state (s -> (s, s)) put :: State m -> m () put s = state (_ -> ((), s)) state :: (State m -> (a, State m)) -> m a state f = do s <- get let ~(a, s) = f s put s return a• State m が今までの s• 型名がドキュメントの役割を果している• 複雑なFunDepsを無くし、かつ一引数クラスで書ける
  28. 28. 例2:演算キャスト class Add a b where type SumType a b add :: a -> b -> SumType a b instance Add Int Int where type SumType Int Int = Int add = (+) instance Add Int Double where type SumType Int Double = Double a `add` b = fromIntegral a + b• 二つの型の足し算を定義• SumType はキャストされた結果の型
  29. 29. 例3:マップの一般化 lass Key k where data Map k :: * -> * empty :: Map k v lookup :: k -> Map k v -> Maybe v insert :: k -> v -> Map k v -> Map k v instance Key Int where newtype Map Int a = IMap (IM.IntMap a) empty = IMap IM.empty lookup k (IMap d) = IM.lookup k d insert k v (IMap d) = IMap $ IM.insert k v d instance Key ByteString where newtype Map ByteString a = BSMap (Trie a) instance (Key a, Key b) => Key (a, b) where newtype Map (a, b) v = TMap (Map a (Map b v))• 辞書構造をキーの型によって使いわけ
  30. 30. 洒落た多相の使い方• いままでの多相型の使い方 • ad-hoc • 複数の型に共通な操作を抽象化。 • Num, Functor, Monad,... • parametric • 中身を抽象化したコンテナなど • リスト, Map, a → a, ...
  31. 31. 残像だ──幽霊型• 幽霊型(phantom types) • 実際のデータ定義に出て来ない型引数 を取る型• データにタグを付けることが出来る• GADTs と云う機構を使って制御出来る
  32. 32. 例:構文木• 型付き言語の構文木を書きたい data Expr = Zero | Succ Expr | Plus Expr Expr | Yes | No | IsZero | Apply Expr Expr eval :: Expr -> Expr eval Zero = Zero eval (Succ a) = Succ $ eval a eval (Plus a (eval -> Zero)) = eval a eval (Plus a (eval -> Succ b)) = Succ $ eval $ Plus a b eval Yes = Yes eval No = No eval (Apply IsZero (eval -> Zero)) = Yes eval (Apply IsZero _) = No eval IsZero = IsZero eval _ = error "type error"
  33. 33. 実行例 ghci> eval (Plus Zero (Succ (Succ Zero))) Succ (Succ Zero) ghci> eval (Plus (Succ Zero) (Succ (Succ Zero))) Succ (Succ (Succ Zero)) ghci> eval (Apply IsZero (Succ Zero)) No ghci> eval (Apply IsZero (Plus Zero Zero)) Yes ghci> eval (Plus Yes Zero) Yes ghci> eval (Apply IsZero (Succ Yes)) No ghci> eval (Plus Zero Yes) *** Exception: type error• 型があわないのが実行時エラーになる• 素朴にやるとエラーが……。一々チェック?
  34. 34. 型チェック通ったら落ちて欲しくない…
  35. 35. 処理系にやらせましょう。
  36. 36. 例:型付き構文木 data Expr a where Zero :: Expr Int Succ :: Expr Int -> Expr Int Plus :: Expr Int -> Expr Int -> Expr Int Yes :: Expr Bool No :: Expr Bool IsZero :: Expr (Int -> Bool) Apply :: Expr (a -> b) -> Expr a -> Expr b eval :: Expr a -> Expr a eval (Succ a) = Succ (eval a) eval (Plus a b) = eval a eval (Plus a (eval -> Succ b)) = Succ (eval $ Plus a b) eval (Apply IsZero (eval -> Zero)) = Yes eval (Apply IsZero (eval -> Succ _)) = No eval a = a• 各コンストラクタに型が付加されている • 変な構文木を弾いてくれる!
  37. 37. 型引数、更には幽霊型の威力
  38. 38. 型引数の威力• 型引数は「階層の区別」に使える • タグとしての型引数• 「ランク2多相」と組み合わせると、安 全なメモリアクセスやリソース管理に 使える
  39. 39. Rank 2 多相?• 多相函数を引数に取れる型拡張 • Haskell の総称プログラミングライブ ラリSYBなどにも使われる • ここではちょっと違った例を見ます。
  40. 40. ST Monad• State thread: メモリを用いた高速な状態 更新をするための機構• Haskell は pure なので外に inpure な操作 が漏れないようにしたい
  41. 41. ST Monaddata ST s a-- 内部状態 s を持ち、a の値を返す演算。data STRef s a-- 内部状態 s を持ち、a の値を保持する可変値。-- これも s を漏らしてはいけない。newSTRef :: a -> ST s (STRef s a)-- 新しい STRef を作る。その際 ST と STRef の内部状態は統一する。readSTRef :: STRef s a -> ST s awriteSTRef :: STRef s a -> a -> ST s ()modifySTRef :: STRef s a -> (a -> a) -> ST s ()-- STRef を読み書きするための函数。ST内部でしか使えない。runST :: (forall s. ST s a) -> a-- ST 演算を実行して、結果を返す函数。-- 内部状態は捨てられるので純粋性は保たれる。
  42. 42. 利用例 f str = do ref <- newSTRef str modifySTRef ref (++ ", that is the value") readSTRef ref main :: IO () main = do print $ runST $ f "hoge"• 簡単な例• 破壊的代入を行えるので、STArray など 高速な配列演算を行うための型もある• Bloom Filter の実装などにも使われる
  43. 43. 外部に漏れない? leaker str = do ref <- newSTRef str modifySTRef ref (++ ", that is the value") return ref main :: IO () main = do let ref = runST $ leaker "hoge" print $ runST $ anotherEvilSToperation ref• STRef を外に返すような函数を書けば漏 れるんじゃないの?
  44. 44. コンパイルしてみる /Users/hiromi/pfi/seminar-20120411/test01.lhs:121:11: Couldnt match type `a0 with `STRef s [Char] because type variable `s would escape its scope This (rigid, skolem) type variable is bound by a type expected by the context: ST s a0 In the second argument of `($), namely `leaker "hoge" In the second argument of `($), namely `runST $ leaker "hoge" In the expression: print $ runST $ leaker "hoge" Failed, modules loaded: none.• STRef s [Char] の s は ST の内部状態と同じ• s は runST :: (forall s. ST s a) → a の引数内側で束縛 • 何でもいい、だからこそ何だかわからない!
  45. 45. ちょっと理由説明• runST :: (forall s. ST s (STRef s a)) → forall s. STRef s a • と云う型じゃだめなの?• 束縛変数の記号を変えてもいいので……• (forall s’. ST s’ (STRef s’ a)) → forall s. STRef s a • と書き換えることも出来て、型があわない
  46. 46. さらに応用:リソース管理• 問題:Handle など scarce なリソースはすぐに解放した い • リソース取得と解放とが同順とは限らない • 解放したリソースを外でアクセス出来ないようにし たい• 「リージョン」を使う! • 同一リージョン中は割り当てたリソースが使用可 能、抜けると解放しアクセス不可能に
  47. 47. 実装方針• ST を真似て Rank 2 多相で leak を防ぐ • 内部状態ではなくただのタグ、幽霊型• 同順じゃなく解放するには? • リージョンをネスト • 型クラス+モナド変換子で親リージョ ンのリソースを割り当てられるように
  48. 48. 実装newtype RIO s a = RIO (ReaderT (IORef [Handle]) IO a) deriving (Functor, Monad, MonadReader (IORef [Handle]))newtype RHandle s = RHandle HandlerunRIO :: (forall s. RIO s a) -> IO arunRIO (RIO act) = bracket (newIORef []) (readIORef >=> mapM_ hClose) $ runReaderT actopenFileR :: FilePath -> IOMode -> RIO s (RHandle s)openFileR fp mode = do h <- RIO $ liftIO $ openFile fp mode RIO $ ReaderT $ ref -> liftIO $ modifyIORef ref (h:) return (RHandle h) • 開いたハンドルへの参照を持っておく • 終了時に解放するようにする • ハンドルは全てRHandle の形で扱い、閉じれない
  49. 49. 使ってみる test = do h <- openFileR "conf.txt" ReadMode str <- hGetLineR h putStrLnR str main = runRIO test• 型は省略しても推論してくれる(!!)• 上の実装だと一リージョンしか扱えな いが、型クラスによってネスト出来る
  50. 50. やりたい例 RIO s IO RIO t (RIO s IO)
  51. 51. やりたい例1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO)
  52. 52. やりたい例1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO)
  53. 53. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開くを渡す
  54. 54. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く
  55. 55. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く 4 ファイル3を(親リージョンで)開く h3 :: RHandle s
  56. 56. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く 4 ファイル3を(親リージョンで)開く h3 :: RHandle s 5 1, 2の内容を交互に3へ書き込む
  57. 57. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く 4 ファイル3を(親リージョンで)開く h3 :: RHandle s 5 1, 2の内容を交互に3へ書き込む h2, hc を解放 h3 を返す; h3は型があうので返せる
  58. 58. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く 4 ファイル3を(親リージョンで)開く h3 :: RHandle s 5 1, 2の内容を交互に3へ書き込む h2, hc を解放 h3 を返す; h3は型があうので返せる 6 1 に残りがあれば それを 3へ書き込む
  59. 59. やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle tを渡す 3 ファイル2の位置を読み、開く 4 ファイル3を(親リージョンで)開く h3 :: RHandle s 5 1, 2の内容を交互に3へ書き込む h2, hc を解放 h3 を返す; h3は型があうので返せる 6 1 に残りがあれば それを 3へ書き込む h3, h1 を解放
  60. 60. そのまま書けるtest3 = runSIO $ do h1 <- newSHandle "/tmp/SafeHandles.hs" ReadMode h3 <- newRgn (test3_internal h1) till (shIsEOF h1) (shGetLine h1 >>= shPutStrLn h3) shReport "test3 done"test3_internal h1 = do h2 <- newSHandle "/tmp/ex-file.conf" ReadMode fname <- shGetLine h2 h3 <- liftSIO (newSHandle fname WriteMode) shPutStrLn h3 fname till (liftM2 (||) (shIsEOF h2) (shIsEOF h1)) (shGetLine h2 >>= shPutStrLn h3 >> shGetLine h1 >>= shPutStrLn h3) shReport "Finished zipping h1 and h2" return h3 • ネストしても推論してくれる! • 勝手にタグがつく! • リソースの受け渡しも型が監視してくれる!
  61. 61. 幽霊型・Rank 2多相 他の応用例• セッション型 • ネットワーク/スレッド通信のチャネ ルの「使われ方」の不整合を型が検 出してくれる
  62. 62. Session 型の例typecheck1 $ c -> send c "abc" :: (Length ss l) => Session t (ss :> Send [Char] a) (ss :> a) () example from [10] • 操作が 型レベルリストで並んでいる • Session t (事前条件) (事後条件) 結果 • 上の例は「一度だけ、一つだけチャネ ルを使って文字列をを送る」と読む • t は Rank 2 多相によるリーク防止
  63. 63. 昇進──Data Kinds• Data Kinds? • データ型を型レベルへ「持ち上げ」 • GADTs や Type Family などと組み合わ せて強力な威力を発揮する
  64. 64. 例:長さ付きリスト• 幽霊型として、長さをパラメタ化• 空リストへの先頭要素、tail アクセスを 防止• 連結の際は型レベル足し算がいる • type family で足せばいい!
  65. 65. 実装例data Nat = Z | S Natdata Vector :: Nat -> * -> * where Nil :: Vector Z a Cons :: a -> Vector n a -> Vector (S n) avhead :: Vector (S n) a -> avhead (Cons a _) = avtail :: Vector (S n) a -> Vector n avtail (Cons _ as) = asvnull :: Vector n a -> Boolvnull Nil = Truevnull _ = Falsetype family (a :: Nat) :+: (b :: Nat) :: Nattype instance Z :+: a = atype instance S n :+: a = S (n :+: a)vappend :: Vector n a -> Vector m a -> Vector (n :+: m) avappend Nil v = vvappend (Cons a as) v = Cons a (as `vappend` v)
  66. 66. 以前は…。• Nat を型に持ち上げられなかったので 無意味な足し算が定義出来る可能性が あった• 同様に長さ以外のインデックスが付く 可能性があった• 持ち上げる事で範囲を制限!
  67. 67. 補足• Data Kinds が強力になり、型が引数として任 意の値を取れるようになる(!!) • 依存型と云う • Coq, Agda など• Curry-Howard 対応 • 型と論理式の対応→定理証明
  68. 68. 時間切れ orz
  69. 69. References1. “Fun with type functions”, Oleg Kiselyov, Simon Peyton Jones and Chung-chieh Shan, 20102. “Giving Haskell a Promotion”,B. A.Yorgey, S. Weirich, J. Cretin, Simon Peyton Jones and D.Vytiniotis, 20113. “Lightweight Monadic Regions”, Oleg Kiselyov, Chung-chieh Shan, 20084. “Algebra of Programming in Agda”, Shin-Cheng Mu, Hsiang-Shang Ko, Patrik Jansson, 20095. “Yesod Web Framework for Haskell”6. “データ型 - Wikipedia”, Wikipedia7. “Type safety - Wikipedia”, Wikipedia8. “Phantom type - HaskellWiki”, HaskellWiki9. “Region-based resource management”, Oleg Kiselyov, 201110. “full-sessions-0.6.1”, id:keigoi11. “プログラミングCoq”, 池渕未来
  70. 70. Any Questions?
  71. 71. 御清聴ありがとうございました

×