How wonderful
to be (statically) typed
∼(静的に)型がつくってスバラシイ∼
         石井 大海
自己紹介
•   石井 大海 (id:mr_konn / @mr_konn)

•   早稲田大学数学科三年

    •   集合論や論理学、圏論に興味

•   2010 Summer Intern から PFI でバイト中

•   Haskell Lover
    •   Haskell でウェブ開発などしている
本日のお題:型
• 型がつくと何が嬉しいのか?
• 型・・・むずかしそう
 • ムツカシイ理論ではなく応用例の紹介
 • Haskell の布教ではない
   • 静的型付けの布教です。
Table of Contents
•   型とは何か?──型付け、型推論、型安全

•   wonderful な型たち──型の表現力と応用

    •   いいとこ取り多相──型クラス

    •   型レベル函数:型族──キャスト、Map共通化など

    •   そっちは残像だ──幽霊型はタグ

    •   洒落た多相の使い方──多相型でリーク防止

    •   Haskell、昇進す。──Data Kinds

•   その型、強力につき──依存型と証明
Table of Contents
•   型とは何か?──型付け、型推論、型安全

•   wonderful な型たち──型の表現力と応用

    •   いいとこ取り多相──型クラス

    •   型レベル函数:型族──キャスト、Map共通化など

    •   そっちは残像だ──幽霊型はタグ

    •   洒落た多相の使い方──多相型でリーク防止

    •   Haskell、昇進す。──Data Kinds

•   その型、強力につき──依存型と証明
型とは何か?
──型付け、型推論、型安全──
型とは何か?
“データ型(でーたがた、data type)とは、コ
ンピュータにおけるデータの扱いに関する形
式のことである。データタイプとも。データ
型は、プログラミングなどにおいて変数その
ものや、その中に代入されるオブジェクトや
値が対象となる。”

              ── データ型 - Wikipedia
型とは何か?
•   つまり?

    •   値の種類を区別するタグのようなもの

•   世の言語は大きく「型付き言語」と「型無
    し言語」に大別出来る

    •   型付き・・・C, Ruby, Haskell, LISP, Smalltalk...

    •   型無し・・・Brainf*ck, Lazy-K, ...
型なし……?
•   実用的なプログラミング言語は全部型がついてる気が
    する。

    •   Tcl :すべては文字列。でも実際には型別の内部表現
        を持っている(らしい)。

•   そもそもの untyped・・・Untyped Lambda Calculus

•   型あり言語の分類

    •   動的型付け・・・LISP, Ruby, Smalltalk, JavaScript...

    •   静的型付け・・・Java, C, Haskell, OCaml, ...
型なし vs. 動的型
動的と静的の違い
•   動的型付け

    •   型検査は実行時に行われる

        •   (compile-time) untyped と云える?

        •   以後「型なし」は「動的型付け」の alias とする

    •   インタプリタ言語はこれが多い

•   静的型付け

    •   型検査はコンパイル時に行われる

    •   型が合わないものは実行する前に弾かれる
型検査
• 演算の値の型が合っているか検査
• 合っていなかったら?
 • 言語によって弾いたり、キャストした
  りする。

→ 型安全性
型安全性?
•   ナイーヴな定義:「型検査を通ったものが変な挙
    動をしない」

•   ちょっと厳密な定義

    •   Preservation: 型が評価の順番に依らない

    •   Progress: 型が付いたら「刺さらない」

        •   「刺さらない」ってなんや(意味論による)
型安全性と言語
•   Haskell……型安全と云ってよい

    •   黒魔術を使わなければ型安全

    •   停止性を込めると型安全じゃない

        •   f x = f (f x) みたいなコードも型は付くが停止し
            ない

•   C……型安全じゃない!

    •   cast や printf(“%s”, 12); などあぶない
型推論
•   函数の型を自動的に推論してくれる機能

•   型推論なし・・・C, Java, ...

•   型推論あり・・・Haskell, Scala, OCaml, ...

    •   健全性とか完全性とか色々概念があるらしいです
        がぶっちゃけよくわからない

    •   色んな強さの型推論がある、と云うことで。

    •   コンパイラが付けられない部分は手で注釈
ここからは
• 静的型付け、型推論あり言語の布教
• かつある程度の型安全性を持つ言語
 • Preserve は当然仮定
 • 停止しないくらいは許す
   • SEGV ったりしない、程度。
その前に……なぜ型?
• 型はドキュメント
 • 型を見れば(ある程度)挙動がわかる
   • 勿論 Int → Int とか見せられてもわか
    らない ^^;(でも無いよりマシ)

• 型検査通ればある程度正しく動く
 • どの程度?→型の表現力による!
型はドキュメント
•   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
型はドキュメント
•   各 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
wonderful な型たち
  ──型の表現力と応用──
型クラス
•   Ad-hoc 多相を実現する機構

    •   複数の型に共通な演算をオーバーロード

    •   cf. Parametric 多相:共通の「実装」を共有する

•   代替手法

    •   強力なモジュールシステム(like Alloy, Agda,
        OCaml, ...)

    •   プリプロセッサ(camlp4?)
数値型クラス
         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 でもある必要がある

    •   型クラスは、型への「制約」「述語」として考えられる
ファンクタ型クラス
         class Functor f where
           fmap :: (a -> b) -> f a -> f b



•   普通の函数を「持ち上げ」られるコンテナのよう
    なもの

    •   []、Maybe、IO などなど

•   Maybe a は値を持つが、 Maybe は値を持たない

    •   Maybe :: * → * :型を受けとって型を返す「函
        数」みたいなもの
多引数型クラスの例
        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」関数従属性
型クラス:まとめ

• 型の述語のようなもの
• Ad-hoc 多相を綺麗に実現する
• 函数従属性で依存関係を記述出来る
型族

• 型レベル函数のようなもの
• 型別名を付けるものと、データを新た
 に定義するもの

• 型クラスに付随する型族
例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を無くし、かつ一引数クラスで書ける
例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 はキャストされた結果の型
例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))




• 辞書構造をキーの型によって使いわけ
洒落た多相の使い方
•   いままでの多相型の使い方

    •   ad-hoc
        •   複数の型に共通な操作を抽象化。

        •   Num, Functor, Monad,...
    •   parametric
        •   中身を抽象化したコンテナなど

        •   リスト, Map, a → a, ...
残像だ──幽霊型
• 幽霊型(phantom types)
 • 実際のデータ定義に出て来ない型引数
  を取る型

• データにタグを付けることが出来る
• GADTs と云う機構を使って制御出来る
例:構文木
• 型付き言語の構文木を書きたい
 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"
実行例
    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

•   型があわないのが実行時エラーになる

•   素朴にやるとエラーが……。一々チェック?
型チェック通ったら
落ちて欲しくない…
処理系に
やらせましょう。
例:型付き構文木
        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



•   各コンストラクタに型が付加されている

    •    変な構文木を弾いてくれる!
型引数、
更には幽霊型の威力
型引数の威力
• 型引数は「階層の区別」に使える
 • タグとしての型引数
• 「ランク2多相」と組み合わせると、安
 全なメモリアクセスやリソース管理に
 使える
Rank 2 多相?

• 多相函数を引数に取れる型拡張
 • Haskell の総称プログラミングライブ
  ラリSYBなどにも使われる

 • ここではちょっと違った例を見ます。
ST Monad

• State thread: メモリを用いた高速な状態
 更新をするための機構

• Haskell は pure なので外に inpure な操作
 が漏れないようにしたい
ST Monad
data 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 a
writeSTRef :: STRef s a -> a -> ST s ()
modifySTRef :: STRef s a -> (a -> a) -> ST s ()
-- STRef を読み書きするための函数。ST内部でしか使えない。

runST :: (forall s. ST s a) -> a
-- ST 演算を実行して、結果を返す函数。
-- 内部状態は捨てられるので純粋性は保たれる。
利用例
    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 の実装などにも使われる
外部に漏れない?
 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 を外に返すような函数を書けば漏
 れるんじゃないの?
コンパイルしてみる
        /Users/hiromi/pfi/seminar-20120411/test01.lhs:121:11:
           Couldn't 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 の引数内側で束縛

    •     何でもいい、だからこそ何だかわからない!
ちょっと理由説明
•   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
    •   と書き換えることも出来て、型があわない
さらに応用:リソース管理
•   問題:Handle など scarce なリソースはすぐに解放した
    い

    •   リソース取得と解放とが同順とは限らない

    •   解放したリソースを外でアクセス出来ないようにし
        たい

•   「リージョン」を使う!

    •   同一リージョン中は割り当てたリソースが使用可
        能、抜けると解放しアクセス不可能に
実装方針
• ST を真似て Rank 2 多相で leak を防ぐ
 • 内部状態ではなくただのタグ、幽霊型
• 同順じゃなく解放するには?
 • リージョンをネスト
 • 型クラス+モナド変換子で親リージョ
  ンのリソースを割り当てられるように
実装
newtype RIO s a = RIO (ReaderT (IORef [Handle]) IO a)
    deriving (Functor, Monad, MonadReader (IORef [Handle]))
newtype RHandle s = RHandle Handle
runRIO :: (forall s. RIO s a) -> IO a
runRIO (RIO act) = bracket (newIORef []) (readIORef >=> mapM_ hClose) $
  runReaderT act

openFileR :: 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 の形で扱い、閉じれない
使ってみる
  test = do
    h <- openFileR "conf.txt" ReadMode
    str <- hGetLineR h
    putStrLnR str

  main = runRIO test



• 型は省略しても推論してくれる(!!)
• 上の実装だと一リージョンしか扱えな
 いが、型クラスによってネスト出来る
やりたい例
        RIO s IO
        RIO t (RIO s IO)
やりたい例
1   ファイル1を開く   h1 :: RHandle s   RIO s IO
                                 RIO t (RIO s IO)
やりたい例
1   ファイル1を開く   h1 :: RHandle s   RIO s IO
                                 RIO t (RIO s IO)
やりたい例
      1   ファイル1を開く   h1 :: RHandle s   RIO s IO
                                       RIO t (RIO s IO)


 h1        2   設定ファイルを開く
を渡す
やりたい例
      1   ファイル1を開く   h1 :: RHandle s           RIO s IO
                                               RIO t (RIO s IO)


 h1        2   設定ファイルを開く
                                       h2, hc :: RHandle t
を渡す
           3   ファイル2の位置を読み、開く
やりたい例
      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
やりたい例
      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へ書き込む
やりたい例
      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は型があうので返せる
やりたい例
      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へ書き込む
やりたい例
      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 を解放
そのまま書ける
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



 •   ネストしても推論してくれる!

     •   勝手にタグがつく!

 •   リソースの受け渡しも型が監視してくれる!
幽霊型・Rank 2多相
     他の応用例

• セッション型
 • ネットワーク/スレッド通信のチャネ
  ルの「使われ方」の不整合を型が検
  出してくれる
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 多相によるリーク防止
昇進──Data Kinds

• Data Kinds?
 • データ型を型レベルへ「持ち上げ」
 • GADTs や Type Family などと組み合わ
   せて強力な威力を発揮する
例:長さ付きリスト
• 幽霊型として、長さをパラメタ化
• 空リストへの先頭要素、tail アクセスを
 防止

• 連結の際は型レベル足し算がいる
 • type family で足せばいい!
実装例
data Nat = Z | S Nat

data Vector :: Nat -> * -> * where
  Nil :: Vector Z a
  Cons :: a -> Vector n a -> Vector (S n) a

vhead :: Vector (S n) a -> a
vhead (Cons a _) = a

vtail :: Vector (S n) a -> Vector n a
vtail (Cons _ as) = as

vnull :: Vector n a -> Bool
vnull Nil = True
vnull _   = False

type family (a :: Nat) :+: (b :: Nat) :: Nat
type instance Z :+: a = a
type instance S n :+: a = S (n :+: a)

vappend :: Vector n a -> Vector m a -> Vector (n :+: m) a
vappend Nil v         = v
vappend (Cons a as) v = Cons a (as `vappend` v)
以前は…。
• Nat を型に持ち上げられなかったので
 無意味な足し算が定義出来る可能性が
 あった

• 同様に長さ以外のインデックスが付く
 可能性があった

• 持ち上げる事で範囲を制限!
補足
•   Data Kinds が強力になり、型が引数として任
    意の値を取れるようになる(!!)

    •   依存型と云う

    •   Coq, Agda など

•   Curry-Howard 対応

    •   型と論理式の対応→定理証明
時間切れ orz
References
1.   “Fun with type functions”, Oleg Kiselyov, Simon Peyton Jones and Chung-chieh
     Shan, 2010
2.   “Giving Haskell a Promotion”,B. A.Yorgey, S. Weirich, J. Cretin, Simon Peyton
     Jones and D.Vytiniotis, 2011
3.   “Lightweight Monadic Regions”, Oleg Kiselyov, Chung-chieh Shan, 2008
4.   “Algebra of Programming in Agda”, Shin-Cheng Mu, Hsiang-Shang Ko, Patrik
     Jansson, 2009
5.   “Yesod Web Framework for Haskell”
6.   “データ型 - Wikipedia”, Wikipedia

7.   “Type safety - Wikipedia”, Wikipedia
8.   “Phantom type - HaskellWiki”, HaskellWiki
9.   “Region-based resource management”, Oleg Kiselyov, 2011
10. “full-sessions-0.6.1”, id:keigoi
11. “プログラミングCoq”, 池渕未来
Any Questions?
御清聴
ありがとうございました

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

  • 1.
    How wonderful to be(statically) typed ∼(静的に)型がつくってスバラシイ∼ 石井 大海
  • 2.
    自己紹介 • 石井 大海 (id:mr_konn / @mr_konn) • 早稲田大学数学科三年 • 集合論や論理学、圏論に興味 • 2010 Summer Intern から PFI でバイト中 • Haskell Lover • Haskell でウェブ開発などしている
  • 3.
    本日のお題:型 • 型がつくと何が嬉しいのか? • 型・・・むずかしそう • ムツカシイ理論ではなく応用例の紹介 • Haskell の布教ではない • 静的型付けの布教です。
  • 4.
    Table of Contents • 型とは何か?──型付け、型推論、型安全 • wonderful な型たち──型の表現力と応用 • いいとこ取り多相──型クラス • 型レベル函数:型族──キャスト、Map共通化など • そっちは残像だ──幽霊型はタグ • 洒落た多相の使い方──多相型でリーク防止 • Haskell、昇進す。──Data Kinds • その型、強力につき──依存型と証明
  • 5.
    Table of Contents • 型とは何か?──型付け、型推論、型安全 • wonderful な型たち──型の表現力と応用 • いいとこ取り多相──型クラス • 型レベル函数:型族──キャスト、Map共通化など • そっちは残像だ──幽霊型はタグ • 洒落た多相の使い方──多相型でリーク防止 • Haskell、昇進す。──Data Kinds • その型、強力につき──依存型と証明
  • 6.
  • 7.
  • 8.
    型とは何か? • つまり? • 値の種類を区別するタグのようなもの • 世の言語は大きく「型付き言語」と「型無 し言語」に大別出来る • 型付き・・・C, Ruby, Haskell, LISP, Smalltalk... • 型無し・・・Brainf*ck, Lazy-K, ...
  • 9.
    型なし……? • 実用的なプログラミング言語は全部型がついてる気が する。 • Tcl :すべては文字列。でも実際には型別の内部表現 を持っている(らしい)。 • そもそもの untyped・・・Untyped Lambda Calculus • 型あり言語の分類 • 動的型付け・・・LISP, Ruby, Smalltalk, JavaScript... • 静的型付け・・・Java, C, Haskell, OCaml, ...
  • 10.
  • 11.
    動的と静的の違い • 動的型付け • 型検査は実行時に行われる • (compile-time) untyped と云える? • 以後「型なし」は「動的型付け」の alias とする • インタプリタ言語はこれが多い • 静的型付け • 型検査はコンパイル時に行われる • 型が合わないものは実行する前に弾かれる
  • 12.
    型検査 • 演算の値の型が合っているか検査 • 合っていなかったら? • 言語によって弾いたり、キャストした りする。 → 型安全性
  • 13.
    型安全性? • ナイーヴな定義:「型検査を通ったものが変な挙 動をしない」 • ちょっと厳密な定義 • Preservation: 型が評価の順番に依らない • Progress: 型が付いたら「刺さらない」 • 「刺さらない」ってなんや(意味論による)
  • 14.
    型安全性と言語 • Haskell……型安全と云ってよい • 黒魔術を使わなければ型安全 • 停止性を込めると型安全じゃない • f x = f (f x) みたいなコードも型は付くが停止し ない • C……型安全じゃない! • cast や printf(“%s”, 12); などあぶない
  • 15.
    型推論 • 函数の型を自動的に推論してくれる機能 • 型推論なし・・・C, Java, ... • 型推論あり・・・Haskell, Scala, OCaml, ... • 健全性とか完全性とか色々概念があるらしいです がぶっちゃけよくわからない • 色んな強さの型推論がある、と云うことで。 • コンパイラが付けられない部分は手で注釈
  • 16.
    ここからは • 静的型付け、型推論あり言語の布教 • かつある程度の型安全性を持つ言語 • Preserve は当然仮定 • 停止しないくらいは許す • SEGV ったりしない、程度。
  • 17.
    その前に……なぜ型? • 型はドキュメント •型を見れば(ある程度)挙動がわかる • 勿論 Int → Int とか見せられてもわか らない ^^;(でも無いよりマシ) • 型検査通ればある程度正しく動く • どの程度?→型の表現力による!
  • 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.
    型はドキュメント • 各 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.
    wonderful な型たち ──型の表現力と応用──
  • 21.
    型クラス • Ad-hoc 多相を実現する機構 • 複数の型に共通な演算をオーバーロード • cf. Parametric 多相:共通の「実装」を共有する • 代替手法 • 強力なモジュールシステム(like Alloy, Agda, OCaml, ...) • プリプロセッサ(camlp4?)
  • 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.
    ファンクタ型クラス class Functor f where fmap :: (a -> b) -> f a -> f b • 普通の函数を「持ち上げ」られるコンテナのよう なもの • []、Maybe、IO などなど • Maybe a は値を持つが、 Maybe は値を持たない • Maybe :: * → * :型を受けとって型を返す「函 数」みたいなもの
  • 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.
    型クラス:まとめ • 型の述語のようなもの • Ad-hoc多相を綺麗に実現する • 函数従属性で依存関係を記述出来る
  • 26.
  • 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.
    例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.
    例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.
    洒落た多相の使い方 • いままでの多相型の使い方 • ad-hoc • 複数の型に共通な操作を抽象化。 • Num, Functor, Monad,... • parametric • 中身を抽象化したコンテナなど • リスト, Map, a → a, ...
  • 31.
    残像だ──幽霊型 • 幽霊型(phantom types) • 実際のデータ定義に出て来ない型引数 を取る型 • データにタグを付けることが出来る • GADTs と云う機構を使って制御出来る
  • 32.
    例:構文木 • 型付き言語の構文木を書きたい dataExpr = 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.
    実行例 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.
  • 35.
  • 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.
  • 38.
    型引数の威力 • 型引数は「階層の区別」に使える •タグとしての型引数 • 「ランク2多相」と組み合わせると、安 全なメモリアクセスやリソース管理に 使える
  • 39.
    Rank 2 多相? •多相函数を引数に取れる型拡張 • Haskell の総称プログラミングライブ ラリSYBなどにも使われる • ここではちょっと違った例を見ます。
  • 40.
    ST Monad • Statethread: メモリを用いた高速な状態 更新をするための機構 • Haskell は pure なので外に inpure な操作 が漏れないようにしたい
  • 41.
    ST Monad data STs 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 a writeSTRef :: 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.
    利用例 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.
    外部に漏れない? 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.
    コンパイルしてみる /Users/hiromi/pfi/seminar-20120411/test01.lhs:121:11: Couldn't 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.
    ちょっと理由説明 • 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.
    さらに応用:リソース管理 • 問題:Handle など scarce なリソースはすぐに解放した い • リソース取得と解放とが同順とは限らない • 解放したリソースを外でアクセス出来ないようにし たい • 「リージョン」を使う! • 同一リージョン中は割り当てたリソースが使用可 能、抜けると解放しアクセス不可能に
  • 47.
    実装方針 • ST を真似てRank 2 多相で leak を防ぐ • 内部状態ではなくただのタグ、幽霊型 • 同順じゃなく解放するには? • リージョンをネスト • 型クラス+モナド変換子で親リージョ ンのリソースを割り当てられるように
  • 48.
    実装 newtype RIO sa = RIO (ReaderT (IORef [Handle]) IO a) deriving (Functor, Monad, MonadReader (IORef [Handle])) newtype RHandle s = RHandle Handle runRIO :: (forall s. RIO s a) -> IO a runRIO (RIO act) = bracket (newIORef []) (readIORef >=> mapM_ hClose) $ runReaderT act openFileR :: 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.
    使ってみる test= do h <- openFileR "conf.txt" ReadMode str <- hGetLineR h putStrLnR str main = runRIO test • 型は省略しても推論してくれる(!!) • 上の実装だと一リージョンしか扱えな いが、型クラスによってネスト出来る
  • 50.
    やりたい例 RIO s IO RIO t (RIO s IO)
  • 51.
    やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO)
  • 52.
    やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO)
  • 53.
    やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く を渡す
  • 54.
    やりたい例 1 ファイル1を開く h1 :: RHandle s RIO s IO RIO t (RIO s IO) h1 2 設定ファイルを開く h2, hc :: RHandle t を渡す 3 ファイル2の位置を読み、開く
  • 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.
    やりたい例 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.
    やりたい例 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.
    やりたい例 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.
    やりたい例 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.
    そのまま書ける 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.
    幽霊型・Rank 2多相 他の応用例 • セッション型 • ネットワーク/スレッド通信のチャネ ルの「使われ方」の不整合を型が検 出してくれる
  • 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.
    昇進──Data Kinds • DataKinds? • データ型を型レベルへ「持ち上げ」 • GADTs や Type Family などと組み合わ せて強力な威力を発揮する
  • 64.
    例:長さ付きリスト • 幽霊型として、長さをパラメタ化 • 空リストへの先頭要素、tailアクセスを 防止 • 連結の際は型レベル足し算がいる • type family で足せばいい!
  • 66.
    実装例 data Nat =Z | S Nat data Vector :: Nat -> * -> * where Nil :: Vector Z a Cons :: a -> Vector n a -> Vector (S n) a vhead :: Vector (S n) a -> a vhead (Cons a _) = a vtail :: Vector (S n) a -> Vector n a vtail (Cons _ as) = as vnull :: Vector n a -> Bool vnull Nil = True vnull _ = False type family (a :: Nat) :+: (b :: Nat) :: Nat type instance Z :+: a = a type instance S n :+: a = S (n :+: a) vappend :: Vector n a -> Vector m a -> Vector (n :+: m) a vappend Nil v = v vappend (Cons a as) v = Cons a (as `vappend` v)
  • 67.
    以前は…。 • Nat を型に持ち上げられなかったので 無意味な足し算が定義出来る可能性が あった • 同様に長さ以外のインデックスが付く 可能性があった • 持ち上げる事で範囲を制限!
  • 68.
    補足 • Data Kinds が強力になり、型が引数として任 意の値を取れるようになる(!!) • 依存型と云う • Coq, Agda など • Curry-Howard 対応 • 型と論理式の対応→定理証明
  • 69.
  • 70.
    References 1. “Fun with type functions”, Oleg Kiselyov, Simon Peyton Jones and Chung-chieh Shan, 2010 2. “Giving Haskell a Promotion”,B. A.Yorgey, S. Weirich, J. Cretin, Simon Peyton Jones and D.Vytiniotis, 2011 3. “Lightweight Monadic Regions”, Oleg Kiselyov, Chung-chieh Shan, 2008 4. “Algebra of Programming in Agda”, Shin-Cheng Mu, Hsiang-Shang Ko, Patrik Jansson, 2009 5. “Yesod Web Framework for Haskell” 6. “データ型 - Wikipedia”, Wikipedia 7. “Type safety - Wikipedia”, Wikipedia 8. “Phantom type - HaskellWiki”, HaskellWiki 9. “Region-based resource management”, Oleg Kiselyov, 2011 10. “full-sessions-0.6.1”, id:keigoi 11. “プログラミングCoq”, 池渕未来
  • 71.
  • 72.

Editor's Notes