M. Walker
M. Walker

Reputation: 613

Purescript: Pattern match wildcard data constructor

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

Answers (2)

Sam Thomson
Sam Thomson

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

Albtzrly
Albtzrly

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

Related Questions