Reading — step 1 of 5
Learn
newtype wraps a type with a different name — at zero runtime cost. Used for: documentation, smart constructors, multiple instance choices.
newtype Age = Age Int -- now Age is its own type, but compiles to Int
newtype UserId = UserId Int
function sendEmail :: UserId -> Email -> IO ()
function registerUser :: Age -> Email -> IO UserId
-- Compile error: can't pass UserId where Age expected, even though both are Int
The wrapper is erased at compile time — newtype has no runtime overhead, unlike data (which is a real wrapper).
Smart constructors — hide the constructor:
module MyModule (Email, mkEmail, emailValue) where
newtype Email = Email String -- constructor not exported
mkEmail :: String -> Maybe Email
mkEmail s = if '@' `elem` s then Just (Email s) else Nothing
emailValue :: Email -> String
emailValue (Email s) = s
Users can't construct an Email without going through mkEmail — guarantees validity.
Multiple type class instances — the killer use case:
-- The Sum/Product newtypes pick which monoid Int uses:
import Data.Monoid
getSum (Sum 3 <> Sum 4) -- 7 (addition)
getProduct (Product 3 <> Product 4) -- 12 (multiplication)
Int is a Monoid in two ways (sum, product). Without newtypes, you couldn't define both — the compiler picks one. With newtypes, each wrapper picks one.
Similar pattern: First/Last for Maybe (pick first / last non-Nothing).
Deriving via:
newtype Email = Email String
deriving (Show, Eq, Ord)
Gets the underlying type's instances for free.
Compare to data:
data Email = Email String -- works, but: extra runtime indirection
newtype Email = Email String -- preferred for single-field wrappers
If the wrapper has exactly one field, prefer newtype. Multiple fields require data.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…