Unwrapping newtypes with hidden constructors

I have a newtype that needs to satisfy some validation logic in order to be constructed. This is why I don’t want to export the constructor outside of the module. But I also want to use the newtype with functions that accept the wrapped value. To give a simplified example, I have in one module:

module Username (Username, validateUsername) where

newtype Username = Username String

validateUsername :: String -> Maybe Username

and in another module

module StringStuff where

doStringStuff :: String -> Effect Unit

What options do I have for doing the closest to doStringStuff (username :: Username)?

Some things I’ve looked into so far:

  1. derive instance Newtype Username _ gives me unwrap which is nice, but it also gives me wrap which allows me to circumvent the validation function, which is not nice.
  2. derive newtype instance Coercible String Username doesn’t work since compiler complains that the constructor needs to be in scope when using coerce.
  3. Manually writing toString :: Username -> String in the newtype module works, but if I have a number of other newtypes, it’s slightly ugly having all these toString, toInt etc. functions.
  4. Writing my own class Unwrappable t a | t -> a where unwrap :: t -> a, implementing it for Username and other newtypes and then having doStringStuff :: forall t. Unwrappable t String => t -> Effect Unit. This is the nicest solution, though I’m somewhat reluctant to roll a type class just for this.

Is there another option I’m missing? What have you tried in your code for a similar problem?

2 Likes

Hi I usually go for something like toString in cases like this (it’s easier and I don’t mind not having some overloaded function). But in this special case here I’d go for just show (make it an instance of Show) - it’s basically toString anyway.


To make the first remark a bit more concrete: Let’s say I have

newtype PositiveInt = PositiveInt Int

then I usually have a

toInt :: PositiveInt -> Int

and on use side I like to import my module like this:

import Data.PositiveInt (PositiveInt)
import Data.PositiveInt as PositiveInt

and use it like

PositiveInt.toInt

yes it’s a bit boiler-plate code here and there but it’s very clear where the stuff is coming from and what is happening.

2 Likes

Just endorsing @CarstenKoenig’s answer here. For smart constructors I usually have a print function so I can use e.g. Username.print when I need it to be a string. A little boilerplate but not usually a big deal.

You can also have the equivalent of a Show class in your application that does this for you, where all your toString or print functions are instances for the class. But I feel like this offers marginal benefit, and it can make it harder to tell where a print function is coming from and can make type inference a bit worse.

1 Like

I have also asked myself this question many times. Usually because I want to pattern match on the newtype.

doStringStuff :: Username -> Effect Unit
doStringStuff (Username "root")  = doSpecialRootStuff
doStringStuff (Username "admin") = doSpecialAdminStuff
doStringStuff (Username uname)   = doNormalUserStuff uname

If the constructor is not exported, then we cannot pattern match in this nice obvious way. I wish there were a better answer.

1 Like

if you export a show / toString whatever you can still do

doStringStuff :: Username -> Effect Unit
doStringStuff un = case show un of
    "root" -> doSpecialRootStuff
    "admin" -> doSpecialAdminStuff
    uname  -> doNormalUserStuff uname

or in this special case you could have functions isRoot :: Username -> Bool, isAdmin :: Username -> Bool or even a

matchRoles :: forall a. a -> a -> (String -> a) -> Username -> a
matchRoles forRoot forAdmin forOther ...

This (probably the easier/first one first) is what I’d do - this way the “magic user names” would stay inside a single module.

2 Likes

Hi!

I would say to map a String back to a Username type without exposing the constructor in another module, that maybe that could be achieved by using some kind of function like validateUsername which takes a String as input and returns a Maybe Username as output.

So, the validateUsername function checks if the input string is equal to either “ROOT” or “ADMIN”. If it is, the function returns a Just (Username input). Otherwise, it returns Nothing.

This Maybe Username value can then be unwrapped using the fmap function, which takes a function g and a Maybe a value as inputs, and applies g to the unwrapped a value if it exists.

In the doStringStuff function, the input String is passed through validateUsername and then unwrapped using fmap. If the input string is either “ROOT” or “ADMIN”, then doSpecialRootStuff or doSpecialAdminStuff are applied to the unwrapped Username value, respectively.

This way, the constructor of the Username type is not exposed, but its contents can still be mapped from String to Username in a clean and elegant way.

I hope this helps! Greetings and thx for the acceptance.

module Constants where

rootUsername :: String
rootUsername = “ROOT”

adminUsername :: String
adminUsername = “ADMIN”

module Username (unwrapUsername, validateUsername) where

import Constants
import Data.Maybe (fromMaybe)

newtype Username = Username String

validateUsername :: String → Maybe Username
validateUsername input = case input of
rootUsername → Just (Username rootUsername)
adminUsername → Just (Username adminUsername)
_ → Nothing

unwrapUsername :: String → Maybe String
unwrapUsername input = fmap ((Username x) → x) (validateUsername input)

module StringStuff where

import Constants
import Username (unwrapUsername)
import Effect (Effect)

doStringStuff :: String → Effect Unit
doStringStuff input = case unwrapUsername input of
Just username →
case username of
rootUsername → doSpecialRootStuff
adminUsername → doSpecialAdminStuff
uname → doNormalUserStuff uname
Nothing → – handle the validation failure

doSpecialRootStuff :: Effect Unit
doSpecialRootStuff = putStrLn “You have special root privileges”

doSpecialAdminStuff :: Effect Unit
doSpecialAdminStuff = putStrLn “You have special admin privileges”

doNormalUserStuff :: String → Effect Unit
doNormalUserStuff uname = putStrLn $ "Hello " ++ uname ++ “, you are a normal user”

1 Like