在之前的一个问题中,我在寻找编码Kleisli arrows of Outrageous Fortune的方法时发现了Conor McBride的Idris examples in Haskell的存在。我努力理解McBride的代码并使其在Haskell中编译导致了这个要点:https://gist.github.com/abailly/02dcc04b23d4c607f33dca20021bcd2f
在搜索Hackage时,我发现了这些概念的几种实现,特别是(猜猜是谁?)Edward Kmett和Gabriel Gonzalez。
人们将这些代码投入生产有什么经验?特别是,IRL实际发生了预期的好处(运行时安全性,自我指导使用)吗?如何随着时间的推移维护这种代码并加入新手?
编辑:我更改了标题以更明确地了解我正在寻找的内容:在野外实际使用索引monad。我有兴趣使用它们,我有几个用例,只是想知道其他人是否已经在“生产”代码中使用过它们。
编辑2 :由于到目前为止提供了很好的答案和有用的评论,我再次编辑了该问题的标题和描述,以更准确地反映出我期望的答案类型,例如:经验报告。
答案 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
的计算减少了为满足从s
到t
的协议而要完成的剩余工作。我将使用我在回答您的其他问题时使用的索引免费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网站上找到这个例子: