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
get
andset
functions, though let's call themto
andfro
for 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))
fromn
by 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 . toSI
is always trouble, buttoSI . fromSI
ought to work. Maybe it only affects things like instantiatinginstance Num String
and if we instead used our(toSI, froSI)
pair to derive someinstance
forInteger
thatString
has would we be in a good position. Maybe.Let's try it.
String
is an instance ofMonoid
which looks like thisIf we implemented
mappend
viamappend = toSI . mappend . fromSI
it gives us "concatenating integers" likeand if we're careful to define
"" -> 0
instead of making it fail then we can get a usefulmempty
too.which seems like it ought to work well. It's truly a
Monoid
ofInteger
inherited from its "one-way isomorphism" withString
(think about why I have to useInteger
here, notInt
). More specifically, we can't distinguishtoSI (fromSI n)
fromn
by any test built from functions in theMonoid
typeclass, so it's "good enough" to make this mapping.But then we run smack into another problem.
Integer
already has aMonoid
instance. 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
Integer
becomes aMonoid
via its "one-way isomorphism" (a.k.a. "retract") withString
but 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
Monoid
package defines things likeSum
andProduct
which indicate that thisInteger
has been specialized to just use itsAddition
properties.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
newtype
and a slew of libraries for quickly accessing what's just "underneath" yournewtype
layer all the way from the commonGeneralizedNewtypeDeriving
extension out to theControl.Newtype
package or the directly inspiredIso
,au
andauf
and entireWrapped
,ala
,alaf
mechanisms 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
.