Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can constructor be captured and reused with different set of parameters?

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
like image 453
omittones Avatar asked Jan 16 '14 22:01

omittones


4 Answers

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
like image 54
Nikita Volkov Avatar answered Sep 23 '22 02:09

Nikita Volkov


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.

like image 37
GS - Apologise to Monica Avatar answered Sep 23 '22 02:09

GS - Apologise to Monica


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.

like image 24
asm Avatar answered Sep 26 '22 02:09

asm


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
like image 20
viorior Avatar answered Sep 25 '22 02:09

viorior