سؤال

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
هل كانت مفيدة؟

المحلول 2

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.

نصائح أخرى

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

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

You can't match a variable against a constructor like that because patterns are not first-class citizens in Haskell. You can have code that's specific to your function, but not something general.

If you're interested in ideas like that, take a look at the research language bondi which does support matching constructors like this. It actually opens up an interesting new vein of expressiveness. In practice, it makes writing code that's generic over the exact structure of the algebraic data type much easier.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top