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 Element
s from different Container
s? 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" Element
s from different Container
s.
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)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With