Reputation: 461
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
...
Upvotes: 1
Views: 163
Reputation: 74384
What you've described here is essentially defining class instances by isomorphism. This can obviously be achieved by defining the get
and set
functions, though let's call them to
and fro
for a little more clarity.
toSI :: String -> Integer
fromSI :: Integer -> String
instance Num String where
a + b = fromSI $ (toSI a) + (toSI b)
abs = fromSI . abs . toSI
...
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 integer n
, I should not be able to distinguish (froSI (toSI n))
from n
by any means (well, some things like computational speed are ignored). Furthermore, I must have the same property for any string s
: toSI (froSI s)
must be indistinguishable from n
.
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, but toSI . fromSI
ought to work. Maybe it only affects things like instantiating instance Num String
and if we instead used our (toSI, froSI)
pair to derive some instance
for Integer
that String
has would we be in a good position. Maybe.
Let's try it. String
is an instance of Monoid
which looks like this
instance Monoid [a] where -- if a ~ Char then this is String
mempty = []
mappend as bs = as ++ bs
If we implemented mappend
via mappend = toSI . mappend . fromSI
it gives us "concatenating integers" like
(1 <> 2) <> 3 == 123
1 <> (2 <> 3) == 123
(0 <> 1) <> 1 == 11
0 <> (1 <> 1) == 11
and if we're careful to define "" -> 0
instead of making it fail then we can get a useful mempty
too.
mempty = toSI mempty
which seems like it ought to work well. It's truly a Monoid
of Integer
inherited from its "one-way isomorphism" with String
(think about why I have to use Integer
here, not Int
). More specifically, we can't distinguish toSI (fromSI n)
from n
by any test built from functions in the Monoid
typeclass, so it's "good enough" to make this mapping.
But then we run smack into another problem. Integer
already has a Monoid
instance. It already has about 10 of them, the most popular being multiplication and addition
instance Monoid Integer instance Monoid Integer
mempty = 0 mempty = 1
mappend = (+) mappend = (*)
So, 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 a Monoid
via its "one-way isomorphism" (a.k.a. "retract") with String
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 like Sum
and Product
which indicate that this Integer
has been specialized to just use its Addition
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" your newtype
layer all the way from the common GeneralizedNewtypeDeriving
extension out to the Control.Newtype
package or the directly inspired Iso
, au
and auf
and entire Wrapped
, ala
, alaf
mechanisms of lens
.
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
.
Upvotes: 4