Tips to reduce newtype wrapper boilerplate

Here’s a basic routing snippet that risks mixing-up the order of the “first” and “second” query params in Foo:

module Main where

import Prelude
import Data.Foldable (oneOf)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Routing.Match (Match, param, root)

data MyRoute
  = Foo String String

derive instance genericMyRoute :: Generic MyRoute _

instance showMyRoute :: Show MyRoute where
  show = genericShow

myRoute :: Match MyRoute
myRoute =
  root
    *> oneOf
        [ Foo <$> param "first" <*> param "second"
        ]

If I want to improve type safety by adding newtype wrappers for these strings, I find that a lot of duplicated code is required:

module Main where

import Prelude
import Data.Foldable (oneOf)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Routing.Match (Match, param, root)

newtype First = First String

derive instance genericFirst :: Generic First _

instance showFirst :: Show First where
  show = genericShow

newtype Second = Second String

derive instance genericSecond :: Generic Second _

instance showSecond :: Show Second where
  show = genericShow

data MyRoute
  = Foo First Second

derive instance genericMyRoute :: Generic MyRoute _

instance showMyRoute :: Show MyRoute where
  show = genericShow

myRoute :: Match MyRoute
myRoute =
  root
    *> oneOf
        [ (\s1 s2 -> Foo (First s1) (Second s2)) <$> param "first" <*> param "second"

Are there recommendations for:

  1. A way to reduce this repeated boilerplate for each wrapped String?
  2. Shorthand to avoid duplicating the name/constructor of the newtype? For example newtype _ = First String?
  3. A nicer replacement for (\s1 s2 -> Foo (First s1) (Second s2))?
1 Like

I made a small gist for the last question, try it here

Well ngl I don’t know if I’d call this cleaner from doing it manually:)

For 3, you would lift the constructors with more applicative combinators.

Foo <$> (First <$> param "first") <*> (Second <$> param "second")

Once we have a release with Coercible, you will be able to coerce the newtype wrappers.

Foo <$> coerce (param "first") <*> coerce (param "second")
1 Like

That’s much nicer! The lifted constructors approach seems better than coerce in this case because it makes it more obvious if the param ordering is accidentally reversed.

I appreciate the example. This is a good demonstration of a trade-off between generic show and custom show.

Custom show requires more maintenance effort for that instance, but let’s you avoid creating show instances for each child.

Custom:

instance showName :: Show Name where
  show (Name (Firstname f) (Lastname l)) = f <> " " <> l

Generic:

instance showName :: Show Name where
  show = genericShow

derive instance genericFirstName :: Generic FirstName _
derive instance genericLastName :: Generic FLastName _

instance showFirstName :: Show FirstName where
  show = genericShow
instance showLastName :: Show LastName where
  show = genericShow

It would be really convenient if there was some shorthand way to “recursively derive” generic instances for that type and its children. For example, something like:

rec_derive instance genericName :: Generic Name _

rec_instance showName :: Show Name where
  show = genericShow

For me, the solution would be shorter deriving syntax, and more deriving options (possibly through deriving via).

5 Likes