6

So I have a situation very similar to this (much simplified) code:

import Data.Maybe
import Data.List

data Container a = Container [a] 

-- Assumption: an Element can only obtained from a Container.  The operation
-- guarentees the Element is in the Container.
data Element a = Element (Container a) a

-- This operation is only valid for Elements that have the same Container.
before :: Eq a => Element a -> Element a -> Bool
before (Element (Container xs) x) (Element (Container ys) y) 
    | xs == ys = fromJust (elemIndex x xs) < fromJust (elemIndex y xs)
    | otherwise = error "Elements from different Containers"

How can I use Haskell's type system (or GHC extensions) to restrict before and similar operations from taking Elements from different Containers? I've been looking into GADTs and DataKinds but, well, that's going to take a long while and I could use some suggestions or pointers. (Another idea I've thought of but haven't worked out: use a similar trick as the ST monad's s parameter...)

Am I too pessimistic if I conclude that this would require a dependently typed language? Because, as my limited understanding of dependent typing goes, I think that I'm trying to index the Element type by values of the Container type.


EDIT: Just for extra color, this all ultimately arises because I'm trying to define something very much like this:

import Data.Function
import Data.Ix

-- I want to use Elements as Array indexes; so Elements need to know
-- their Container to calculate their Int
instance Eq a => Ix (Element a) where
    -- Ignore how crap this code is
    range (l, u) = map (getSibling l) [getIndex l .. getIndex u]
    index (l,u) i = index (getIndex l, getIndex u) (getIndex i)
    inRange (l,u) i = inRange (getIndex l, getIndex u) (getIndex i)

getIndex :: Eq a => Element a -> Int
getIndex (Element (Container xs) x) = fromJust $ elemIndex x xs

getList :: Element a -> [a]
getList (Element (Container xs) _) = xs

getSibling :: Element a -> Int -> Element a
getSibling (Element (Container xs) _) i = Element (Container xs) (xs!!i)

instance Eq a => Eq (Element a) where
    (==) = (==) `on` getIndex

instance Eq a => Ord (Element a) where
    compare = compare `on` getIndex

All of this code requires that you never "mix" Elements from different Containers.

4

2 回答 2

5

You can tell containers apart statically by associating a type with each container. Container elements are tagged with the type to determine whether two given elements came from the same container. This uses -XExistentialQuantification and -XRank2Types. Basically, this is dependent typing, except that types depend on the tag instead of the container value.

-- Containers with existentially typed tags 'c'
data Container a = forall c. Container !(OpenContainer c a)

-- Containers with a tag parameter 'c'
data OpenContainer c a = OpenContainer [a]

-- A container element with a tag parameter 'c'
data Element c a = Element (OpenContainer c a) a

-- Create a container.  An arbitrary tag is chosen for the container.
container :: [a] -> Container a
container = Container . OpenContainer

-- Use a container.  The tag is read.
openContainer :: Container a -> (forall c. OpenContainer c a -> b) -> b
openContainer c k = case c of Container c' -> k c'

-- Get a container's elements.  The elements are tagged.
getElements :: OpenContainer c a -> [Element c a]
getElements c@(OpenContainer xs) = map (Element c) xs

Any time openContainer is called, it will yield a collection of values belonging to the same container. Two different calls to openContainer will be assumed to refer to different containers.

-- Ok
f c = openContainer c $ \oc -> getElements oc !! 0 `before` getElements oc !! 1

-- Error
f c d = openContainer c $ \oc -> openContainer d $ \od -> getElements oc !! 0 `before` getElements od !! 0

This is safe, but conservative, because it is not based on which container is used, but rather which call to openContainer was used.. Calling openContainer on a container, then calling it again, will produce incompatible elements.

-- Error
f c = openContainer c $ \oc -> openContainer c $ \od -> getElements oc !! 0 `before` getElements od !! 1

Now you can write before without explicitly testing for equality. Since both elements have the same index, they must have come from the same container.

before :: Eq a => Element c a -> Element c a -> Bool
before (Element (OpenContainer xs) x) (Element _ y) = fromJust (elemIndex x xs) < fromJust (elemIndex y xs)
于 2012-10-25T23:52:30.307 回答
1

I don't know if this qualifies as an answer, but I will just throw it out there. You can make every Element a function of the Container that it derives from:

newtype a `HasA` b = H { using :: a -> b }
    deriving (Monad, Applicative, Functor)

By restricting the set of permissible operations to the above classes, you ensure that any elements you combine share the same originating container.

You export the "using" function but not the H constructor. The primitive functions that create Elements are provided by you, but then the user can just combine them using the Monad instance, ensuring that they always refer to the same container once you unwrap and supply them with the container.

As a bonus, it answers your title's question literally: it indexes the element on its container's value.

于 2012-10-26T01:54:51.783 回答