在生产中使用索引monad体验报告?

时间:2016-11-06 21:13:52

标签: haskell types monads type-level-computation

在之前的一个问题中,我在寻找编码Kleisli arrows of Outrageous Fortune的方法时发现了Conor McBride的Idris examples in Haskell的存在。我努力理解McBride的代码并使其在Haskell中编译导致了这个要点:https://gist.github.com/abailly/02dcc04b23d4c607f33dca20021bcd2f

在搜索Hackage时,我发现了这些概念的几种实现,特别是(猜猜是谁?)Edward KmettGabriel Gonzalez

人们将这些代码投入生产有什么经验?特别是,IRL实际发生了预期的好处(运行时安全性,自我指导使用)吗?如何随着时间的推移维护这种代码并加入新手?

编辑:我更改了标题以更明确地了解我正在寻找的内容:在野外实际使用索引monad。我有兴趣使用它们,我有几个用例,只是想知道其他人是否已经在“生产”代码中使用过它们。

编辑2 :由于到目前为止提供了很好的答案和有用的评论,我再次编辑了该问题的标题和描述,以更准确地反映出我期望的答案类型,例如:经验报告。

3 个答案:

答案 0 :(得分:3)

我认为下面应该算作一个实际的例子:在编译器中静态强制执行“良好堆叠”。 Boilerplate首先:

{-# LANGUAGE GADTs, KindSignatures #-}
{-# LANGUAGE DataKinds, TypeOperators #-}
{-# LANGUAGE RebindableSyntax #-}

import qualified Prelude
import Prelude hiding (return, fail, (>>=), (>>))

然后是一个简单的堆栈语言:

data Op (i :: [*]) (j :: [*]) where
    IMM :: a -> Op i (a ': i)
    BINOP :: (a -> b -> c) -> Op (a ': b ': i) (c ': i)
    BRANCH :: Label i j -> Label i j -> Op (Bool ': i) j

我们不会打扰真正的Label

data Label (i :: [*]) (j :: [*]) where
    Label :: Prog i j -> Label i j

Prog rams只是Op s的类型对齐列表:

data Prog (i :: [*]) (j :: [*]) where
    PNil :: Prog i i
    PCons :: Op i j -> Prog j k -> Prog i k

因此在这个设置中,我们可以很容易地创建一个编译器,它是一个索引编写器monad;也就是说,索引monad:

class IMonad (m :: idx -> idx -> * -> *) where
    ireturn :: a -> m i i a
    ibind :: m i j a -> (a -> m j k b) -> m i k b

-- For RebindableSyntax, so that we get that sweet 'do' sugar
return :: (IMonad m) => a -> m i i a
return = ireturn
(>>=) :: (IMonad m) => m i j a -> (a -> m j k b) -> m i k b
(>>=) = ibind
m >> n = m >>= const n
fail = error

允许累积(n索引)的monoid:

class IMonoid (m :: idx -> idx -> *) where
    imempty :: m i i
    imappend :: m i j -> m j k -> m i k

就像常规Writer

newtype IWriter w (i :: [*]) (j :: [*]) (a :: *) = IWriter{ runIWriter :: (w i j, a) }

instance (IMonoid w) => IMonad (IWriter w) where
    ireturn x = IWriter (imempty, x)
    ibind m f = IWriter $ case runIWriter m of
        (w, x) -> case runIWriter (f x) of
            (w', y) -> (w `imappend` w', y)

itell :: w i j -> IWriter w i j ()
itell w = IWriter (w, ())

所以我们只需将此机制应用于Prog rams:

instance IMonoid Prog where
    imempty = PNil
    imappend PNil prog' = prog'
    imappend (PCons op prog) prog' = PCons op $ imappend prog prog'

type Compiler = IWriter Prog

tellOp :: Op i j -> Compiler i j ()
tellOp op = itell $ PCons op PNil

label :: Compiler i j () -> Compiler k k (Label i j)
label m = case runIWriter m of
    (prog, ()) -> ireturn (Label prog)

然后我们可以尝试编译一个简单的表达式语言:

data Expr a where
    Lit :: a -> Expr a
    And :: Expr Bool -> Expr Bool -> Expr Bool
    Plus :: Expr Int -> Expr Int -> Expr Int
    If :: Expr Bool -> Expr a -> Expr a -> Expr a

compile :: Expr a -> Compiler i (a ': i) ()
compile (Lit x) = tellOp $ IMM x
compile (And x y) = do
    compile x
    compile y
    tellOp $ BINOP (&&)
compile (Plus x y) = do
    compile x
    compile y
    tellOp $ BINOP (+)
compile (If b t e) = do
    labThen <- label $ compile t
    labElse <- label $ compile e
    compile b
    tellOp $ BRANCH labThen labElse

如果我们省略例如BINOP的一个参数,类型检查器会检测到这个:

compile (And x y) = do
    compile x
    tellOp $ BINOP (&&)
  
      
  • 无法推断:i ~ (Bool : i)     来自上下文:a ~ Bool
  •   

答案 1 :(得分:3)

会话类型试图为网络协议提供类型级别描述。这个想法是,如果客户端发送一个值,服务器必须准备好接收它,反之亦然。

所以这里是一个类型(使用TypeInType)来描述由要发送的值序列和要接收的值组成的会话。

infixr 5 :!, :?
data Session = Type :! Session
             | Type :? Session
             | E

a :! s表示&#34;发送a类型的值,然后继续使用协议s&#34;。 a :? s表示&#34;收到a类型的值,然后继续使用协议s&#34;。

因此Session表示(类型级别)操作列表。我们的monadic计算将按照此列表的方式工作,按类型要求发送和接收数据。更具体地说,类型Chan s t a的计算减少了为满足从st的协议而要完成的剩余工作。我将使用我在回答您的其他问题时使用的索引免费monad 来构建Chan

class IFunctor f where
    imap :: (a -> b) -> f i j a -> f i j b
class IFunctor m => IMonad m where
    ireturn :: a -> m i i a
    (>>>=) :: m i j a -> (a -> m j k b) -> m i k b


data IFree f i j a where
    IReturn :: a -> IFree f i i a
    IFree :: f i j (IFree f j k a) -> IFree f i k a

instance IFunctor f => IFunctor (IFree f) where
    imap f (IReturn x) = IReturn (f x)
    imap f (IFree fx) = IFree (imap (imap f) fx)

instance IFunctor f => IMonad (IFree f) where
    ireturn = IReturn
    IReturn x >>>= f = f x
    IFree fx >>>= f = IFree (imap (>>>= f) fx)

Chan monad中的基本操作只会发送和接收值。

data ChanF s t r where
    Send :: a -> r -> ChanF (a :! s) s r
    Recv :: (a -> r) -> ChanF (a :? s) s r

instance IFunctor ChanF where
    imap f (Send x r) = Send x (f r)
    imap f (Recv r) = Recv (fmap f r)

send :: a -> Chan (a :! s) s ()
send x = IFree (Send x (IReturn ()))

recv :: Chan (a :? s) s a
recv = IFree (Recv IReturn)

type Chan = IFree ChanF
type Chan' s = Chan s E  -- a "complete" Chan

send将会话的当前状态从a :! s转移到s,履行发送a的义务。同样,recv会话会话从a :? s转换为s

这是有趣的部分。当协议的一端发送一个值时,另一端必须准备好接收它,反之亦然。这导致了会话类型 dual

的想法
type family Dual s where
    Dual (a :! s) = a :? Dual s
    Dual (a :? s) = a :! Dual s
    Dual E = E

总体语言Dual (Dual s) = s可以证明,但是Haskell并不完全。

如果类型为双通道,则可以连接一对通道。 (我猜你称之为连接客户端和服务器的内存模拟。)

connect :: Chan' s a -> Chan' (Dual s) b -> (a, b)
connect (IReturn x) (IReturn y) = (x, y)
connect (IReturn _) (IFree y) = case y of {}
connect (IFree (Send x r)) (IFree (Recv f)) = connect r (f x)
connect (IFree (Recv f)) (IFree (Send y r)) = connect (f y) r

例如,这是一个服务器的协议,用于测试一个数字是否大于3.服务器等待接收Int,发送回Bool,然后结束计算

type MyProtocol = Int :? Bool :! E

server :: Chan' MyProtocol ()
server = do  -- using RebindableSyntax
    x <- recv
    send (x > 3)

client :: Chan' (Dual MyProtocol) Bool
client = do
    send 5
    recv

并测试它:

ghci> connect server client
((),True)

会话类型是一个活跃的研究领域。这种特殊的实现适用于非常简单的协议,但是您无法描述通过线路发送的数据类型取决于协议状态的协议。为此你需要,惊喜,依赖类型。有关会话类型的最新技术的快速演示,请参阅this talk by Brady

答案 2 :(得分:1)

另一个很好的例子是在编译时使用锁定解锁检查的互斥锁。你可以在Stephen Diehl网站上找到这个例子:

http://dev.stephendiehl.com/hask/#indexed-monads