2015-12-11 5 views
25

Я моделирую систему, в которой есть операция, которая создает ресурс и другие операции, которые потребляют этот ресурс. Однако данный ресурс можно использовать только один раз - есть ли способ, которым я могу гарантировать, что во время компиляции?Есть ли способ эмулировать линейные типы в Haskell?

Для конкретного случая, скажем, первая операция выпекает торт и что есть две другие операции: одна для «выбора еды» для пирога, а другая для «выбора торта» и что я могу сделать только один или другой.

-- This is my current "weakly typed" interface: 
bake :: IO Cake 
eat :: Cake -> IO() 
keep :: Cake -> IO() 

-- This is OK 
do 
    brownie <- bake 
    muffin <- bake 
    eat brownie 
    keep muffin 

-- Eating and having the same cake is not OK: 
do 
    brownie <- bake 
    eat brownie 
    keep brownie -- oops! already eaten! 

Его легко применять ограничение не держит уже съеден торт (или наоборот) во время выполнения, установив флаг на торте после того как мы используем его. Но есть ли способ обеспечить это во время компиляции?

BTW, этот вопрос предназначен для доказательства концепции, поэтому я в порядке с любой черной магией, которая может дать мне статическую безопасность, которую я хочу.

+0

Не могли бы вы использовать фантомные типы, чтобы указать кастрированный баран торт был съеден - как 'Cake данные а = Cake' и' данных Eaten' 'данных Fresh' затем' испечь = Cake :: торт Fresh' и ' есть :: Торт Свежий -> Торт Съеден; есть Торт = Торт'? – epsilonhalbe

+3

@epsilonhalbe Конечно, но если вы тогда «съелиCookie <- uneatenCookie», ничто не остановило бы вас от использования 'uneatenCookie' впоследствии. – sepp2k

+0

Я вижу - вам нужно будет написать собственную версию '<-', которая позаботится об очистке' uneatenCookie', но я думаю, что это выходит за рамки моей haskpertise. – epsilonhalbe

ответ

15

В Haskell, базовая версия этого может быть выражено с GADT индексируется магазин тортов (представлен список Nat -s):

{-# LANGUAGE 
    TypeFamilies, GADTs, TypeOperators, PartialTypeSignatures, 
    DataKinds, PolyKinds #-} 

import GHC.TypeLits 
import Data.Proxy 
import GHC.Exts 

-- Allocate a new cake 
type family New cs where 
    New '[]  = 0 
    New (c ': cs) = c + 1 

-- Constraint satisfiable if "c" is in "cs" 
type family Elem c cs :: Constraint where 
    Elem c (c ': cs) =() 
    Elem c (c' ': cs) = Elem c cs 

type family Remove c cs where 
    Remove c '[]  = '[] 
    Remove c (c ': cs) = cs 
    Remove c (c' ': cs) = c' ': Remove c cs 

data Bake :: [Nat] -> [Nat] -> * -> * where 
    Pure :: a -> Bake cs cs a 
    Bake :: (Proxy (New cs) -> Bake (New cs ': cs) cs' a) -> Bake cs cs' a 
    Eat :: Elem c cs => Proxy c -> Bake (Remove c cs) cs' a -> Bake cs cs' a 
    Keep :: Elem c cs => Proxy c -> Bake cs cs' a -> Bake cs cs' a 

ok :: Bake '[] _ _ 
ok = 
    Bake $ \cake1 -> 
    Bake $ \cake2 -> 
    Eat cake1 $ 
    Keep cake2 $ 
    Eat cake2 $ 
    Pure() 

not_ok :: Bake '[] _ _ 
not_ok = 
    Bake $ \cake1 -> 
    Bake $ \cake2 -> 
    Eat cake1 $ 
    Keep cake1 $ -- we already ate that 
    Eat cake2 $ 
    Pure() 

К сожалению, мы не можем удалить тип аннотации от Bake действий и оставить типы должны быть выведены:

foo = 
    Bake $ \cake1 -> 
    Bake $ \cake2 -> 
    Eat cake1 $ 
    Pure() 

-- Error: Could not deduce (Elem (New cs0) (New cs0 + 1 : New cs0 : cs0)) 

Очевидно, что (Elem (New cs0) (New cs0 + 1 : New cs0 : cs0)) выполнима для всех cs0, но GHC не может видеть это, потому что он не может решить, стоит ли New cs0 является неравенством к New cs0 + 1, потому что GHC ничего не может принять в отношении гибкой переменной cs0.

Если мы добавим NoMonomorphismRestriction, foo будет typecheck, но это сделает даже неправильные программы typecheck, нажав все ограничения Elem на верх. Тем не менее это все равно помешает делать что-нибудь полезное с неправильными терминами, но это довольно уродливое решение.


В целом, мы можем выразить Bake как индексированный свободной монады, которая получает нас do -notation с RebindableSyntax и позволяет определение для BakeF, который несколько яснее, чем то, что мы видели раньше. Это также могло бы уменьшить шаблон, похожий на обычную старую монаду Free, хотя я считаю маловероятным, что люди найдут применение для индексированных свободных монад в двух разных случаях в практическом коде.

{-# LANGUAGE 
    TypeFamilies, GADTs, TypeOperators, PartialTypeSignatures, StandaloneDeriving, 
    DataKinds, PolyKinds, NoImplicitPrelude, RebindableSyntax, DeriveFunctor #-} 

import Prelude hiding (Monad(..)) 
import GHC.TypeLits 
import Data.Proxy 
import GHC.Exts 

class IxFunctor f where 
    imap :: (a -> b) -> f i j a -> f i j b 

class IxFunctor m => IxMonad m where 
    return :: a -> m i i a 
    (>>=) :: m i j a -> (a -> m j k b) -> m i k b 
    fail :: String -> m i j a 

infixl 1 >> 
infixl 1 >>= 

(>>) :: IxMonad m => m i j a -> m j k b -> m i k b 
ma >> mb = ma >>= const mb 

data IxFree f i j a where 
    Pure :: a -> IxFree f i i a 
    Free :: f i j (IxFree f j k a) -> IxFree f i k a 

liftf :: IxFunctor f => f i j a -> IxFree f i j a 
liftf = Free . imap Pure 

instance IxFunctor f => IxFunctor (IxFree f) where 
    imap f (Pure a) = Pure (f a) 
    imap f (Free fa) = Free (imap (imap f) fa) 

instance IxFunctor f => IxMonad (IxFree f) where 
    return = Pure 
    Pure a >>= f = f a 
    Free fa >>= f = Free (imap (>>= f) fa) 
    fail = error 

-- Old stuff for Bake 

type family New cs where 
    New '[]  = 0 
    New (c ': cs) = c + 1 

type family Elem c cs :: Constraint where 
    Elem c (c ': cs) =() 
    Elem c (c' ': cs) = Elem c cs 

type family Remove c cs where 
    Remove c '[]  = '[] 
    Remove c (c ': cs) = cs 
    Remove c (c' ': cs) = c' ': Remove c cs 

-- Now the return type indices of BakeF directly express the change 
-- from the old store to the new store. 
data BakeF cs cs' k where 
    BakeF :: (Proxy (New cs) -> k) -> BakeF cs (New cs ': cs) k 
    EatF :: Elem c cs => Proxy c -> k -> BakeF cs (Remove c cs) k 
    KeepF :: Elem c cs => Proxy c -> k -> BakeF cs cs k 

deriving instance Functor (BakeF cs cs') 
instance IxFunctor BakeF where imap = fmap 

type Bake = IxFree BakeF 

bake = liftf (BakeF id) 
eat c = liftf (EatF c()) 
keep c = liftf (KeepF c()) 

ok :: Bake '[] _ _ 
ok = do 
    cake1 <- bake 
    cake2 <- bake 
    eat cake1 
    keep cake2 
    eat cake2 

-- not_ok :: Bake '[] _ _ 
-- not_ok = do 
-- cake1 <- bake 
-- cake2 <- bake 
-- eat cake1 
-- keep cake1 -- already ate it 
-- eat cake2 
+2

[Здесь] (https://github.com/effectfully/random-stuff/blob/master/FreeKitchen.agda) - это то же самое, но с строго положительными индексированными свободными монадами по индексированным (только с формами индексированными) контейнерами. Я не включил конструктор 'keep', так как меня не удовлетворяет тот факт, что' keep' не меняет ни входной, ни выходной индекс. Мне кажется, что 'keep' должен исключать торт из контекста, например' eat', но включать торт в выходной файл. Мой [исходный код] (https://github.com/effectfully/random-stuff/blob/master/Kitchen.agda) делает это, но я думаю, нам нужно дважды индексировать функтор 'BakeF'. – user3237465

+0

Спасибо, это хороший и чистый пример контейнеров (я еще не узнал о них много). –

+1

И [тот же] (https://github.com/effectfully/random-stuff/blob/master/FreerKitchen.agda) с помощью ['Freer'] (http://okmij.org/ftp/Haskell/extensible /more.pdf) монада. – user3237465

1

Частичное решение. Мы могли бы определить тип обертки

data Caked a = Caked { getCacked :: IO a } --^internal constructor 

, из которого мы не экспортируем конструктор/аксессор.

Это будет иметь два почти но-не-совсем-как-связываемые функции:

beforeCake :: IO a -> (a -> Caked b) -> Caked b 
beforeCake a f = Caked (a >>= getCaked . f) 

afterCake :: Caked a -> (a -> IO b) -> Caked b 
afterCake (Caked a) f = Caked (a >>= f) 

Единственный способ для клиентов, чтобы создать Caked значения будет осуществляться через:

eat :: Cake -> Caked() 
eat = undefined 

keep :: Cake -> Caked() 
keep = undefined 

И мы будет выделять Cake значения на обратный вызов:

withCake :: (Cake -> Caked b) -> IO b 
withCake = undefined 

Это, я думаю, обеспечит, чтобы eat и keep вызывались только один раз в обратном вызове.

проблема: не работает с несколькими Cake распределения и Cake значения все еще может избежать объема обратного вызова (могут фантомные типы помочь здесь?)

+2

'' 'withCake (\ cake -> eat cake' afterCake' (\() -> withCake (\ cake '-> eat cake))) '' 'похоже, что он ест' торт' дважды (и 'cake'' не раз). –

+0

@ Даниэль Вагнер. Вы правы. Возможно, методы фантомного типа à la ST monad могут подключить это отверстие. – danidiaz

7

Polakow показал в своей Haskell Symposium бумаге Embedding a full linear lambda calculus in Haskell (pdf), как сделать это.

Основная идея состоит в том, чтобы индексировать каждый конструктор с помощью входного и выходного контекстов, отслеживающих ресурсы, потребляемые в различных субтермах.