Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the idiomatic way to build functions over newtypes ("wrapped types") in Haskell?

Tags:

haskell

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 Strings. 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?

like image 811
George Avatar asked Dec 19 '22 12:12

George


2 Answers

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
like image 173
David Young Avatar answered May 18 '23 14:05

David Young


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 Functors, 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 Isos provided by lens. That, however, is probably worth a separate answer.

like image 28
duplode Avatar answered May 18 '23 12:05

duplode