If we have a type Person defined like:
--datatype in record syntax
data Person = Male { firstName :: String, lastName :: String } |
Female { firstName :: String, lastName :: String }
Can this:
flipNames :: Person -> Person
flipNames p@(Male{}) = Male (lastName p) (firstName p)
flipNames p@(Female{}) = Female (lastName p) (firstName p)
be written as one definition of flipNames? Can we somehow capture constructor used, and reuse it with different parametes? Something like:
flipNames (constructor fname lname) = c lname fname
Although Ganesh has answered your exact question, I want to say that your problem simply indicates an incorrect approach to design of datatypes.
The following approach is much more flexible and removes your problem as such:
data Person = Person { gender :: Gender, firstName :: String, lastName :: String }
data Gender = Male | Female
flipNames (Person gender firstName lastName) = Person gender lastName firstName
The rule behind this is pretty simple: whenever you see yourself creating multiple constructors with the same fields, just use a single constructor and introduce another field with an enum type, as in the code above.
You won't lose any pattern matching capabilities, as the patterns can be like Person Male firstName lastName
, and you'll be able to make the Gender
type derive Enum
and Bounded
which will surely help you with types which aren't as trivial. E.g.:
data Gender = Male | Female deriving (Enum, Bounded)
allGenders :: [Gender]
allGenders = enumFrom minBound
maidenName :: Person -> Maybe String
maidenName (Person Female _ z) = Just z
maidenName _ = Nothing
In this particular case, you can do it like this:
flipNames :: Person -> Person
flipNames p = p { firstName = lastName p , lastName = firstName p }
However this only works because the record selectors for Male
and Female
are the same. There's no general abstraction that captures constructors without their arguments.
To add another option, you could do something similar with phantom types. Note that you're wanting to do this because your Person
constructors are redundant, they only exist to differentiate male and female. You could lift this distinction into the type system and let the type inferencing take care of the Male
/Female
portion.
{-# LANGUAGE FlexibleInstances #-}
data Person a = Person { first :: String, last :: String }
deriving (Show, Eq)
data Male
data Female
flipName :: Person a -> Person a
flipName (Person f l) = Person l f
main = do
let m = Person "John" "Doe" :: Person Male
f = Person "Jane" "Doe" :: Person Female
print m
print f
print (flipName m)
print (flipName f)
print (gender f)
print (gender m)
class Gender a where
gender :: a -> String
instance Gender (Person Male) where
gender _ = "Male"
instance Gender (Person Female) where
gender _ = "Female"
With this in the file person.hs
you get this output:
╰─➤ runhaskell person.hs
Person {first = "John", last = "Doe"}
Person {first = "Jane", last = "Doe"}
Person {first = "Doe", last = "John"}
Person {first = "Doe", last = "Jane"}
"Female"
"Male"
The downside of doing this is that you may not want to carry around the extra type parameter. However, the upside is that you could now define typeclass instances with different implementations based on Male
and Female
types. Although to do the latter requires the FlexibleInstances
extension.
Yes, you can do similar (but not identical) using view patterns!
{-# LANGUAGE ViewPatterns #-}
data Person = Male { firstName :: String, lastName :: String }
| Female { firstName :: String, lastName :: String }
| NewBorn { birthdate :: String }
| Child { firstName :: String, lastName :: String }
| Teenager { firstName :: String, lastName :: String }
isAdult :: Person -> Bool
isAdult (Male {}) = True
isAdult (Female {}) = True
isAdult _ = False
flipNames :: Person -> Person
flipNames p@(isAdult -> True) = p{firstName=lastName p, lastName=firstName p}
flipNames p@(isAdult -> False) = p
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