Let StringWrapper1
and StringWrapper2
be two types that wrap over a string (i.e. newtype StringWrapper1 = StringWrapper1 String
and newtype StringWrapper2 = StringWrapper2
).
Now suppose we're trying to craft a function from StringWrapper1
to StringWrapper2
.
funcWrapper :: StringWrapper1 -> StringWrapper2
On the one hand, we want to be explicit that what we're passing into this function is a StringWrapper1
, so we don't want merely treat StringWrapper1
as a type synonym for String
(that leads to bugs, as my own experience can attest). On the other hand, when conceptually building the function, we are still somehow thinking in terms of String
s. What we want to do then is to first build func
which doesn't us to constantly wrap and unwrap types:
func :: String -> String
Then, we use func
to build funcWrapper
:
funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper (StringWrapper1 str) = StringWrapper2 (func str)
Problem/Question: Is this idiomatic? It seems awkward to constantly be duplicating every function with a func
and a funcWrapper
. Does Haskell provide some other way of doing this that I'm missing? Or should I just use type synonyms?
As others have said, you should make sure this is really what you want to do (see leftaroundabout's comment). If it is, you can use coerce
from the standard library to convert between types that have the same runtime representation:
func :: String -> String
func = ...
...
funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper = coerce func
First of all, you should take into account leftaroundabout's comment and make sure the newytpes are actually meaningful. That said, this sort of wrapping and unwrapping is everyday stuff indeed, but you can make it more convenient. One way is taking advantage of how your string wrappers are monomorphic functors (as opposed to Functor
s, which are polymorphic), and so you can write mapping functions such as:
mapWrapper1 :: (String -> String) -> StringWrapper1 -> StringWrapper1
mapWrapper1 f (StringWrapper1 str) = StringWrapper1 (f str)
mapWrapper2 :: (String -> String) -> StringWrapper2 -> StringWrapper2
mapWrapper2 f (StringWrapper2 str) = StringWrapper2 (f str)
A well-known generalisation of this pattern is the MonoFunctor
class from the mono-traversable package.
It is also easy to define a conversion function between the two wrappers (in fancy jargon, we would say it is a natural transformation between the two functors):
rewrap1as2 :: StringWrapper1 -> StringWrapper2
rewrap1as2 (StringWrapper1 str) = StringWrapper2 str
(rewrap1as2
can be implemented simply as coerce
from Data.Coerce
. See David Young's answer for details.)
The wrap
from user2297560's answer can then be defined in terms of these more elementary functions:
mapAndRewrap1as2 :: (String -> String) -> StringWrapper1 -> StringWrapper2
mapAndRewrap1as2 f = rewrap1as2 . mapWrapper1 f
If you want something even less verbose, you might appreciate the newtype package, or the equivalent Iso
s provided by lens. That, however, is probably worth a separate answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With