I am sorry if this question seems ill thought-out, but I was wondering if it would be possible to define a consistent semantics for something like the following in Haskell:
derive Num String from Int where
get = read
set = show
derive Ord Bool from Integer where
get = fromEnum
set = toEnum
derive (Monad, Functor) Maybe from [] where
get (Just x) = [x]
get Nothing = [ ]
set [x] = Just x
set [ ] = Nothing
I see no reason why not, and it seems like it would cut down on boilerplate in some situations but I don't know if (and if so, how easily) this could be implemented.
Edit:
My intention would be for e.g. the first example to be replaced with something like this:
instance Num String where
get = read :: String -> Int
set = show :: Int -> String
a + b = set $ get a + get b
a * b = set $ get a * get b
...
What you've described here is essentially defining class instances by isomorphism. This can obviously be achieved by defining the
getandsetfunctions, though let's call themtoandfrofor a little more clarity.These are indeed pretty easy definitions to write and there could be some value for reducing boilerplate by lifting class instantiation over
(to, fro)automatically, but this system must carefully follow many rules in order to not pollute the global typeclass instance space with junk.In particular, we might ask
(to, fro)to form an isomorphism. This means that roundtrips in both directions are identities. Stated differently it means that given any integern, I should not be able to distinguish(froSI (toSI n))fromnby any means (well, some things like computational speed are ignored). Furthermore, I must have the same property for any strings:toSI (froSI s)must be indistinguishable fromn.That second one clearly fails as
"I am not a number!"throws an error on that roundtrip. There are many reasons why throwing errors in pure code is dangerous and with typeclasses that danger would carry on and pollute the code of anyone who ever imports your code.You might point out that this only comes about because not all strings are valid numbers. It seems like
fromSI . toSIis always trouble, buttoSI . fromSIought to work. Maybe it only affects things like instantiatinginstance Num Stringand if we instead used our(toSI, froSI)pair to derive someinstanceforIntegerthatStringhas would we be in a good position. Maybe.Let's try it.
Stringis an instance ofMonoidwhich looks like thisIf we implemented
mappendviamappend = toSI . mappend . fromSIit gives us "concatenating integers" likeand if we're careful to define
"" -> 0instead of making it fail then we can get a usefulmemptytoo.which seems like it ought to work well. It's truly a
MonoidofIntegerinherited from its "one-way isomorphism" withString(think about why I have to useIntegerhere, notInt). More specifically, we can't distinguishtoSI (fromSI n)fromnby any test built from functions in theMonoidtypeclass, so it's "good enough" to make this mapping.But then we run smack into another problem.
Integeralready has aMonoidinstance. It already has about 10 of them, the most popular being multiplication and additionSo, we're losing a lot of information by picking any one of these instances to be the canonical typeclass instance for Monoid. It's more accurate to state that
Integerbecomes aMonoidvia its "one-way isomorphism" (a.k.a. "retract") withStringbut also via its stripping to just have addition but also via its stripping to just have multiplication.Really we want to keep that information around which is why the
Monoidpackage defines things likeSumandProductwhich indicate that thisIntegerhas been specialized to just use itsAdditionproperties.And that's, at the end of the day, exactly the problem you have with lifting typeclass instances over isomorphisms at large. Usually, types have a lot of isomorphisms and retracts that can be abused in this way and it's hard to have truly canonical, law-abiding instances. When you find one, it's usually worth the code tax to write it out explicitly, even if you end up using an isomorphism to do so.
When there isn't a canonical choice you have instruments like
newtypeand a slew of libraries for quickly accessing what's just "underneath" yournewtypelayer all the way from the commonGeneralizedNewtypeDerivingextension out to theControl.Newtypepackage or the directly inspiredIso,auandaufand entireWrapped,ala,alafmechanisms oflens.Essentially this machinery is all in place to make it easier to talk richly about the instances inherited over various isomorphisms, especially those induced by
newtype.