Type safe readonly and writeonly attributes with phantom types

Yes, I ask dumb questions on haskell-cafe for a reason ;) As the subject already states, I think that it's worth to be considered to use phantom types and type classes to encode the access mode of an attribute. A first approach is the following: ---- Begin 1 ---- class ReadMode mode class WriteMode mode class Mode a data Readable = Readable data Writeable = Writeable data ReadWrite = ReadWrite instance ReadMode Readable instance WriteMode Writeable instance ReadMode ReadWrite instance WriteMode ReadWrite instance ReadMode a => ReadMode (a,b) instance WriteMode b => WriteMode (a,b) data Attr m w a = Attr (w -> IO a) (w -> a -> IO ()) newAttr :: (w -> IO a) -> (w -> a -> IO ()) -> Attr ReadWrite w a readAttr :: (w -> IO a) -> Attr Readable w a writeAttr :: (w -> a -> IO ()) -> Attr Writeable w a get :: ReadMode m => w -> Attr m w a -> IO a ---- End 1 ---- However this has a problem: the combinatorial explosion of data declarations to represent modes, if one has more than two "switches" (as I will have in the next days ;-) The second one, wich I particularly like, is: ---- Begin 2 ---- class ReadMode mode class WriteMode mode class Mode a data Readable = Readable data Writeable = Writeable instance Mode Readable instance Mode Writeable instance ReadMode Readable instance WriteMode Writeable instance (Mode a,Mode b,ReadMode a) => ReadMode (a,b) instance (Mode a,Mode b,WriteMode b) => WriteMode (a,b) data Attr m w a = Attr (w -> IO a) (w -> a -> IO ()) newAttr :: (w -> IO a) -> (w -> a -> IO ()) -> Attr (Readable,Writeable) w a readAttr :: (w -> IO a) -> Attr Readable w a writeAttr :: (w -> a -> IO ()) -> Attr Writeable w a data Prop ... (=:) :: WriteMode m => Attr m w a -> a -> Prop w get :: ReadMode m => w -> Attr m w a -> IO a set :: w -> [Prop w] -> IO () ---- End 2 ---- Note that this is everything haskell98, and can be used (yes, I still haven't tried to recompile htoolkit, but going to try now) like Attr.hs is used right now. This also can solve the already mentioned problem wich is: "I want to breakdown my types in a class hierarchy, so I want to define thing that, say, have a position. So I want to put the type "Menu" in that class, and also the type "Window". But a window can be repositioned, while a menu can't. So I have to make two classes, if I have separated "ReadOnlyAttr" and "ReadWriteAttr" at the type level." To solve this problem, unfortunately, I need multi-parameter type classes, wich could be considered almost legal, but also functional dependencies if I want to avoid type declarations everywhere (and even in places where they aren't haskell98 anyway): ---- Begin 3 ---- class HasMyAttr w m | w -> m where myAttr :: Attr m w () data TestRW = TestRW data TestR = TestR instance HasMyAttr TestRW (Readable,Writeable) where myAttr = newAttr undefined undefined instance HasMyAttr TestR Readable where myAttr = readAttr undefined t1 = set1 TestRW (myAttr =: ()) t2 = get TestRW myAttr t3 = set1 TestR (myAttr =: ()) -- this row can't be compiled t4 = get TestR myAttr ---- End 3 ---- But note how self documenting this code is: the two instance declarations say exactly that "myAttr" is readable and writeable in type TestRW, and read-only in testR. Besides, note that the mode system is extensible: no one constrains an attribute to have a mode wich is one of the types here mentioned, an user has just to conscientiously put its own modes into the right typeclasses. However, if we wish not to allow extention, we still can in some way. Besides, I think that this mode system could be used even if one does not want h98 extentions: it does not limit in any way (AFAIK) the standard (type-correct) usage wrt the present implementation and adds stronger type-checking. I attach the complete modified Attr.hs for reference and wait hopefully for some comment. Vincenzo

The simpler alternative, wich disables the definition of new modes for attributes in other modules, but is also easier to use, is: ------- data Readable = Readable data Writeable = Writeable data Attr m w a = Attr (w -> IO a) (w -> a -> IO ()) newAttr :: (w -> IO a) -> (w -> a -> IO ()) -> Attr (Readable,Writeable) w a readAttr :: (w -> IO a) -> Attr (Readable,()) w a writeAttr :: (w -> a -> IO ()) -> Attr ((),Writeable) w a get :: w -> Attr (Readable,m) w a -> IO a (=:) (Attr getter setter) x = newProp (\w -> setter w x) getter setter ... ------- Also, again by using typeclasses, it can be enforced that modes are in a fixed set, for example to avoid that one writes "Attr (Writeable,Readable)" in place of "Attr (Readable,Writeable)". As usual, I have compiled htoolkit with the modifications. This time I have not used any non H98 feature, I just have the exact type hierarchy of htoolkit but with well-typed read-only and write-only attributes. The system can be extended to other switches, like "listenable" or "write-once" or similar with a linear number of types, if I am not wrong (so this proposition is always true, uh?). I don't know if it's better to leave around the ReadMode and WriteMode typeclasses or to leave things as in this message, wich looks clearer also in type declarations (maybe extensible is better, anyways). Vincenzo
participants (1)
-
Nick Name