LiquidHaskell 入門 その3
前回の記事ではLiquidHaskellを用いて簡単な言語の評価機を作ってみました。 今回はそれをStateモナドを用いて書き直すために必要な新しい機能であるAbstarct Refinement, Bounded Refinmenetの解説をしていきたいと思います。
Abstract Refinement
LiquidHaskell入門その1で述べたように、LiquidHaskellでは量化子のない一階述語論理式を書くことができますが、それだけでは表現力が足りないケースが出てきます。
公式ブログにある例を引っ張ってみます。
次の関数maxInt
を考えてみます。
maxInt :: Int -> Int -> Int
maxInt = if x >= y then x else y
maxInt :: Nat -> Nat -> Nat
maxInt :: {v: Int | v < 10} -> {v: Int | v < 10} -> {v: Int | v < 10}
maxInt : Even -> Even -> Even
{-@ maxInt :: Nat -> Nat -> Nat @-}
{-@ test :: Even @-}
test :: Int
test = maxInt 0 2
$ stack exec -- liquid src/Liquid/AbstractRefinement.hs
...
**** RESULT: UNSAFE ************************************************************
/src/Liquid/AbstractRefienement.hs:126:1-18: Error: Liquid Type Mismatch
126 | test = maxInt 2 0
^^^^^^^^^^^^^^^^^^
Inferred type
VV : {v : GHC.Types.Int | v >= 0}
not a subtype of Required type
VV : {VV : GHC.Types.Int | VV mod 2 == 0}
maxInt
に対しては以下のような型を付けることができます。
{-@ maxInt :: forall <p :: Int -> Bool>. Int<p> -> Int<p> -> Int<p> @-}
maxInt :: Int -> Int -> Int
maxInt x y = if x >= y then x else y
forall <p :: Int -> Bool>
が全称量化された述語を導入しており、Int<p>
は(感覚的には){v: Int | p v}
と等価です。(実際に右の書き方をするとSort Error
と怒られます。)
つまり、述語p
を満たすInt
型の値を2つ受け取って、p
を満たすInt
型の値を返すという型がmaxInt
にはつきました。
実際に使ってみましょう。
本来ならばmaxInt
を使うとき、述語を指定する必要がありそうですが、うまいこと推論してくれます。
{-@ test2 :: Even @-}
test2 :: Int
test2 = maxInt 2 0
$ stack exec -- liquid src/Liquid/AbstractRefinement.hs
...
**** RESULT: SAFE **************************************************************
List
型の宣言にAbstract Refinementを用いた例です。
テストでは述語としてすべての値が等しいというものを与えているので、testListUnSafe
のみエラーを報告されています。
{-@
data List a <p :: a -> a -> Bool> where
Nil :: List <p> a
| Cons :: h:a -> List <p> (a<p h>) -> List <p> a
@-}
data List a where
Nil :: List a
Cons :: a -> List a -> List a
{-@ testListSafe :: List <{\s t -> s == t}> Int @-}
testListSafe :: List Int
testListSafe = Cons 1 $ Cons 1 Nil
{-@ testListUnSafe :: List <{\s t -> s == t}> Int @-}
testListUnSafe :: List Int
testListUnSafe = Cons 2 $ Cons 1 Nil
$ stack exec -- liquid src/Liquid/AbstractRefinement.hs
...
**** RESULT: UNSAFE ************************************************************
/src/Liquid/AbstractRefinement.hs:39:1-36: Error: Liquid Type Mismatch
39 | testListUnSafe = Cons 2 $ Cons 1 Nil
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Inferred type
VV : GHC.Types.Int
not a subtype of Required type
VV : {VV : GHC.Types.Int | ?a == VV}
In Context
?a : GHC.Types.Int
Bounded Refinement
有界量化や型クラスのように、全称量化を得たらそのドメインを制限して情報量を増やすような仕組みが欲しくなります。
述語に対する全称量化であったAbstract Refinementに対し、そのドメインを制限するのがBounded Refinement*2です。
早速具体例を見ていきましょう。
関数合成(.)
は以下のように書けます。
引数として2つの関数f: (y:b -> c<p y>)
、g: (z:a -> b<q z>)
を受け取って新しい関数f . g: (a -> c<r x>)
を返すのですが、r
の条件として{x::a, w::b<q x> |- c<p w> <: c<r x>}
という制限を与えています。
つまり、事後条件が連鎖していきます。
実際に使ってみましょう。
incr
を2回適用する関数を合成したdoubleIncr
は確かに検査されました。
{-@
(.) :: forall < p :: b -> c -> Bool
, q :: a -> b -> Bool
, r :: a -> c -> Bool
>.
{x::a, w::b<q x> |- c<p w> <: c<r x>}
f:(y:b -> c<p y>)
-> g:(z:a -> b<q z>)
-> x:a -> c<r x>
@-}
(.) f g x = f (g x)
-- example
{-@ incr :: x: Int -> {y: Int | y = x + 1} @-}
incr :: Int -> Int
incr = (+) 1
{-@ doubleIncr :: x: Int -> {y: Int | y = x + 2} @-}
doubleIncr = incr . incr
$ stack exec -- liquid src/Liquid/AbstractRefinement.hs
...
**** RESULT: SAFE **************************************************************
補足: 構文に関するいくつかの注意点
- Abstract Refinementではあくまで述語の全称量化しかできないので、
forall <p :: Int -> Int>
みたいな書き方をするとコンパイルが通りません。 古い情報だとforall <p :: Int -> Prop>
のように書かれていますが、これもコンパイルが通りません。 - 全称量化された述語の具体するときの構文についてはまとまった情報がありません。(Parserを読む気にはなりませんでした。) 公式レポジトリのテストを読むなりして適当に推測しましょう。 今回の記事と次回の記事でもいくつか例を出していきます。
- Bounded Refinement周りの構文についてもまとまった情報がありません。これも次回の記事でいくつか具体例を出してフォローしていくつもりです。
まとめ
今回はAbstract RefinementとBounded Refinementについて解説しました。 LiquidHaskellでは量化子のない一階述語論理式しか書けませんが、Abstract Refinementは述語の全称量化、Bounded Refinementは述語の有界量化を提供します。 これらによってLiquidHaskellの表現力が広がることを確認しました。
次回はこれらを用いてHoareモナドを実装し、前回の評価機をHoareモナドを用いてよりHaskellらしいコードに書き直します。