The abstraction continues
I got several comments to my lament about my attempts at abstraction in my previous blog post. Two of the comments involve adding an extra argument to display. I dont regard this as an acceptable solution; the changes to the code should not be that intrusive. Adding an argument to a function is a change that ripples through the code to many places and not just the implementation of display.Reiner Pope succeeded where I failed. He split up the operations in Ops into two classes and presto, it works.
data Person t = Person { firstName :: XString t, lastName :: XString t, height :: XDouble t } class (Show s, IsString s) => IsXString s where (+++) :: s -> s -> s class (Num d, IsXString s) => IsXDouble s d where xshow :: d -> s class (IsXDouble (XString t) (XDouble t)) => Ops t where type XString t :: * type XDouble t :: * instance IsXString String where (+++) = (++) instance IsXDouble String Double where xshow = show data Basic = Basic instance Ops Basic where type XString Basic = String type XDouble Basic = Double display :: Ops t => Person t -> XString t display p = firstName p +++ " " +++ lastName p +++ " " +++ xshow (height p + 1)That's neat, but a little fiddly if there are many types involved.
Another problem
Armed with this solution I write another function.incSpace :: (Ops t) => XDouble t -> XString t incSpace x = xshow x +++ " "It typechecks fine. But as far as I can figure out there is no way to use this function. Let's see what ghci says:
> :t incSpace (1 :: XDouble Basic) :: XString BasicDespite my best efforts at providing types it doesn't work. The reason being that saying, e.g., (1 :: XDouble Basic) is the same as saying (1 :: Double). And that doesn't match XDouble t. At least not to the typecheckers knowledge.:1:0: Couldn't match expected type `[Char]' against inferred type `XString t' In the expression: incSpace (1 :: XDouble Basic) :: XString Basic :1:10: Couldn't match expected type `XDouble t' against inferred type `Double' In the first argument of `incSpace', namely `(1 :: XDouble Basic)' In the expression: incSpace (1 :: XDouble Basic) :: XString Basic
In the example of display things work because the parameter t occurs in Person t which is a real type and not a type family. If a type variable only occurs in type family types you are out of luck. There's no way to convey the information what that type variable should be (as far as i know). You can "solve" the problem by adding t as an argument to incSpace, but again, I don't see that as a solution.
In the paper ML Modules and Haskell Type Classes: A Constructive Comparison Wehr and Chakravarty introduce a notion of abstract associated types. That might solve this problem. I really want XDouble and XString to appear as abstract types (or associated data types) outside of the instance declaration. Only inside the instance declaration where I provide implementations for the operations do I really care what the type is.
A reflection on type signatures
If I writef x = xHaskell can deduce that the type is f :: a -> a.
If I instead write
f :: Int -> Int f x = xHaskell happily uses this type. The type checker does not complain as to say "Sorry dude, but you're wrong, the type is more general than what you wrote.". I think that's nice and polite.
Now a different example.
class C a b where x :: a y :: b f z = [x, x, z]What does ghc have to say about the type of f?
f :: (C a b, C a b1) => a -> [a]OK, that's reasonable; the two occurences of x could have different contexts. But I don't want them to. Let's add a type signature.
f :: (C a b) => a -> [a] f z = [x, x, z]What does ghc have to say?
Blog2.hs:9:7: Could not deduce (C a b) from the context (C a b2) arising from a use of `x' at Blog2.hs:9:7 Possible fix: add (C a b) to the context of the type signature for `f' In the expression: x In the expression: [x, x, z] In the definition of `f': f z = [x, x, z] Blog2.hs:9:10: Could not deduce (C a b1) from the context (C a b2) arising from a use of `x' at Blog2.hs:9:10 Possible fix: add (C a b1) to the context of the type signature for `f' In the expression: x In the expression: [x, x, z] In the definition of `f': f z = [x, x, z]Which is ghc's way of say "Dude, I see your context, but I'm not going to use it because I'm more clever than you and can figure out a better type." Rude, is what I say.
I gave a context, but there is nothing to link the b in my context to what ghc internally figures out that the type of the two occuerences of x should. I wish I could tell the type checker, "This is the only context you'll ever going to have, use it if you can." Alas, this is not how things work.
A little ML
Stefan Wehr provided the ML version of the code that I only aluded tomodule MkPerson(O: sig type xString type xDouble val opConcat : xString -> xString -> xString val opShow : xDouble -> xString end) = struct type person = Person of (O.xString * O.xString * O.xDouble) let display (Person (firstName, lastName, height)) = O.opConcat firstName (O.opConcat lastName (O.opShow height)) end module BasicPerson = MkPerson(struct type xString = string type xDouble = float let opConcat = (^) let opShow = string_of_float end) let _ = let p = BasicPerson.Person ("Stefan", "Wehr", 184.0) in BasicPerson.display pIn this case, I think this is the natural way of expressing the abstraction I want. Of course, this ML code has some shortcomings too. Since string literals in ML are not overloaded the cannot be used neatly in the display function like I could in the Haskell version, but that's a minor point.
Again, it is "intrusive" in ML to add the argument O to the functor MkPerson. It is "a change that ripples through the code to many places and not just the implementation". So I don't see why you accept it.
ReplyDeleteI've made a new blog post that I hope shows why I prefer functors and open for this kind of abstraction.
ReplyDeleteIf you use type families to create `C` and `f`, there is no problem.
ReplyDelete> class C a where
> Other a
> x :: a
> y :: Other a
> f :: (C a) => a -> [a]
> f = [x,x,z]
Then again, I don't *fully* understand type families yet :)
Again, it is "intrusive" in ML to add the argument O to the functor MkPerson. It is "a change that ripples through the code to many places and not just the implementation". So I don't see why you accept it.cheap electronics
ReplyDelete