Reputation: 613
While the example is contrived, why can I not use the wildcard pattern if the data constructor is ignored?
module Main where
import Prelude
import Control.Monad.Eff.Console (log)
data Person = Amy { name :: String } | George { name :: String }
--Implementations Options Below
main = log $ personToString $ George { name: "George" }
No Error
personToString :: Person -> String
personToString (Amy { name: n }) = n
personToString (George { name: n }) = n
Error
personToString :: Person -> String
personToString (_ { name: n }) = n
http://try.purescript.org/?session=a1503b9a-0546-7832-39b0-6321a89ef2e3
Unable to parse module:
unexpected {
expecting ::, operator or )
Upvotes: 4
Views: 561
Reputation: 225
If the constructor can safely be ignored, that's a smell that the type can be refactored:
data AmyOrGeorge = Amy | George
data Person = Person AmyOrGeorge { name :: String }
personToString (Person _ { name: n }) = n
I agree with the language designers' choice to leave this feature out, because working around it actually improves the code.
Upvotes: 0
Reputation: 934
I'm not sure exactly why the compiler can't infer that both sum types have { name :: String }
as an argument. I don't think the compiler can do that right now, and I'm not sure it's even possible.
Having said that, there are ways to introspect the types that you use, and you could define the personToString
function so it can work on your Person
type. Keep in mind that this is delving into a more advanced area of the language, and this is also a new area for me. This is probably going way beyond your question, but it might be helpful to others, and it's good to know what's possible.
First, let's define a typeclass for "types that have names".
class DoesHaveName a where
getName :: a -> String
Now we need to examine the structure of the Person
type. To do that, we can use the purescript-generics-rep package. First we'll tell the compiler to examine the data type and create a general-purpose representation of it. We're going to create an instance of Generic
for the Person
type.
import Data.Generic.Rep (class Generic)
derive instance genericPerson :: Generic Person _
We can see all the different ways to represent the type by looking at the constructors in Data.Generic.Rep, and we can transform a Person
into that structure by using from.
import Data.Generic.Rep (class Generic, from)
personToString :: Person -> String
personToString a = getName (from a)
So now we have to create an instance of DoesHaveName
for any one-argument constructor that accepts { name :: String }
.
import Data.Generic.Rep (class Generic, to, from, Sum(..), Rec(..), NoConstructors, Constructor(..), Field(..))
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
instance doesHaveNameConstructor
:: (IsSymbol t0, IsSymbol t1)
=> DoesHaveName (Constructor t0 (Rec (Field t1 String))) where
getName (Constructor (Rec (Field c))) =
case (reflectSymbol (SProxy :: SProxy t1)) of
"name" -> c
_ -> "NoName"
That's a lot to chew on. I'll try and break it down as best I can. t0
and t1
are Symbols - So they're part of the literal code you write. In this case t0
is the name of the Sum type constructor (either Amy or George). t1
is the label of the record (in your example it will be "name"). So we use reflectSymbol
to turn the symbols into strings that we can match on. If the label is "name", then we'll return the value inside the field, otherwise we'll return "NoName".
The last thing we need to do is create a DoesHaveName
instance for the Sum type structure. Sum types contain the Constructors so this instance is basically just handling the outer structure and delegating to the instance we defined above.
instance doesHaveNameSum
:: (DoesHaveName a, DoesHaveName b)
=> DoesHaveName (Sum a b) where
getName (Inl a) = getName a
getName (Inr b) = getName b
Now we can log all sorts of people's names...
data Person
= Amy { name :: String }
| George { name :: String }
| Jim { name :: String }
-- Logs "amy"
log $ personToString (Amy { name: "amy" }
-- Logs "george"
log $ personToString (George { name: "george" }
-- Logs "jim"
log $ personToString (Jim { name: "jim" }
Demo: http://try.purescript.org/?gist=2fc95ad13963e96dd2a49b41f5703e21
Upvotes: 6