If anyone has time/expertise to review a tutorial on phantom types I'm working on, it would be greatly appreciated:
{-module: Phantom.hsdescription: A phantom types tutorialauthor: James King <[email protected]>Here's a common scenario:There's a web service you need to integrate with. It uses differentcredentials for the "staging" sandbox environment and the productionone.It would be bad to use the wrong set of credentials for the wrongenvironment.This module is a tutorial on how to use "phantom types." This patterncan ensure that users of our module cannot make the obvious mistake ofmixing up different kinds of credentials meant for differentenvironments.-}modulePhantomwhere-- | This is the type for staging credentialsdataStaging-- | And this one is for production.dataProduction-- | These two kinds of environments can then be used as values for-- the type parameter, @kind@.dataCredentialkind=Credential{name::String,token::String}deriving(Eq,Show){-The reason why it's called the "phantom type" pattern is because the@kind@ type parameter only appears on the left-hand side of theequals-sign. It's like a ghost which we'll see if we write a functionthat takes @Credential kind@ as a parameter.-}typeStagingCredential=CredentialStaging-- | This pattern works well with smart-constructors for ensuring that-- your users create only valid credentials. Normally this might have-- a return type like, @Either String StagingCredential@. For brevity-- we omit it here. The important thing is that by using this-- function to construct values of StagingCredential Haskell will-- ensure that we can't mix these up with ProductionCredential.stagingCredential::String->String->StagingCredentialstagingCredential=Credential-- | It also has the benefit of making our code more descriptive at-- the type-level. We can have different functions and behaviours-- that are specific to StagingCredential without having to also-- update code relating to the production environment.stagingRequest::StagingCredential->IO()stagingRequest_=putStrLn"Making request to staging server..."-- | We do the same for our production credentials and have isolated,-- separated code for the production environment.typeProductionCredential=CredentialProductionproductionCredential::String->String->ProductionCredentialproductionCredential=CredentialproductionRequest::ProductionCredential->IO()productionRequest_=putStrLn"Making request to production server..."-- | Here's a silly example function to demonstrate how our `kind`-- value disappears like a ghost...ghostCredential::Credentialkind->IO()ghostCredentialcreds=do-- We can't really talk about the value of `kind` in any of the-- terms here... it exists only at the type-level and disappears-- here! Try replacing the body of this function with a type hole-- and see what binds you get. See if you can write something here-- that pattern-matches on @kind@ so we can have different-- behaviours for the different kinds of credentials. Once you're-- convinced it cannot be done, feel free to move on.putStrLn$"ghostCredential: "++showcreds-- | If you were frustrated, that's okay! Maybe we cannot write code-- that is polymorphic over our credentials... Don't give up! If you-- want to be able to write code that is polymorphic over @kind@ you-- still can! In order to talk about @kind@ we need to use type-- classes.classRequestkindwhereexecuteRequest::Credentialkind->IO()-- | When we give an instance for Request we can fix the type of-- @kind@. We have to. That's what an instance is by definition. So-- for this implementation of @executeRequest@ we give the one we-- defined above for staging credentials.instanceRequestStagingwhereexecuteRequest=stagingRequest-- | And the same for Production.instanceRequestProductionwhereexecuteRequest=productionRequestexample::IO()example=do-- And now, using our smart constructors we can create credentials-- for the respective environments...letstaging=stagingCredential"staging""foo-token"production=productionCredential"production""bar-token"-- And we can dispatch to the correct implementation using the type-- class function!executeRequeststagingexecuteRequestproduction{-When to use this pattern: - When you have a type that can be used in many contexts - And using the type in the wrong context would be a programming errorIt's also useful in situations where you might use a sum type but theconstructors of that sum type would have no semantic meaning otherthan expressing choice. For example:-}-- | Instead of having our phantom type parameter here we have a-- plain, unadorned record.dataSumCredential=SumCredential{sumName::String,sumToken::String}deriving(Eq,Show)-- | And we use a sum type to add the distinct contexts to our-- unadorned type. The constructors of our sum type here stand in as-- the phantom type parameter instead. The value contained in each-- case is the same.dataSumCredentialKind=SumStagingSumCredential|SumProductionSumCredentialderiving(Eq,Show)-- | This enables us to write our request function using pattern-- matching. This seems straight-forward: we only use functions and-- data structures and don't need to use type classes and type-- parameters!sumExecuteRequest::SumCredentialKind->IO()sumExecuteRequest(SumStaging_)=putStrLn"Making sum request to staging..."sumExecuteRequest(SumProduction_)=putStrLn"Making sum request to prod..."sumExample::IO()sumExample=doletstaging=SumStaging$SumCredential"staging""staging-token"production=SumProduction$SumCredential"production""production-token"sumExecuteRequeststagingsumExecuteRequestproduction-- | But we lose the ability to distinguish the kinds of credentials-- at the type level. Which has the effect of forcing our code to-- couple together under pattern matching at the term-level. Using a-- smart constructor like this doesn't give us any type hints that-- this produces a credential for a staging environment. In order to-- find out what kind of credential our user is holding they'll have-- to inspect it!sumStagingCredential::String->String->SumCredentialKindsumStagingCredentialsnamestoken=SumStaging$SumCredentialsnamestokenmodifySumStagingRequest::SumCredentialKind->SumCredentialKindmodifySumStagingRequest(SumStagingcreds)=ifsumNamecreds=="specialCreds"thenSumStagingcreds{sumToken="special-token"}elseSumStagingcreds-- | We have to handle every case even if we're only interested in one-- unless we ignore exhaustive pattern matching... which is not what-- we want to do.modifySumStagingRequestcreds=creds-- | Contrast that here where we have the same behaviour but no need-- to mention the alternative case.modifyStagingCredential::StagingCredential->StagingCredentialmodifyStagingCredentialcreds=ifnamecreds=="specialCreds"thencreds{token="special-token"}elsecreds{-There are many use-cases for this pattern. You could have adata-structure with user-submitted input that uses a phantom typeparameter to check if the data has been validated. That way code canbe guaranteed by the type system to not handle unrecognized inputs.I'm sure once you get a little more experience with this pattern youwill see more.What I like about this pattern is how it can separate concerns andkeep code decoupled. Using type constructors for control flow is acode smell in my opinion and an opportunity to enrich your code withstronger, more flexible types.This pattern works well for the scenarios listed earlier in thistutorial. If you find that your application is requiring data inorder to make a decision about which method to dispatch to then it'snot going to be a good fit. It only really works well if yoursum-types are merely being used to pattern match to differentimplementations.When you see that code-smell you can turn those constructors intotypes and create a phantom type parameter to the type they formerlycontained as constructors:-}dataFoo=FooStringInt-- | Lift the constructors up...dataBar=BazFoo|QuzFoodataBaz'dataQuz'-- | And add a phantom type parameter to Foo!dataFoo'a=Foo'StringInt-- | Aliases actually make code more clear...typeFooBaz=Foo'Baz'typeFooQuz=Foo'Quz'{-Happy hacking!-}
Try replacing the body of this function with a type hole
and see what binds you get. See if you can write something here
that pattern-matches on @kind@ so we can have different
behaviours for the different kinds of credentials. Once you're
convinced it cannot be done, feel free to move on.
Not sure if the example is going to work well in this case - you could just as well supply some opaque GADT that actually makes use of the type and user wouldn't be able to tell a difference
Maybe what you can show is one can easily "repack" a credential into one with different type without changing it's content:
BTW, if I remember right, record updates can be type-changing - so creds { token = "special-token" } would actually have this repacking behaviour, which probably isn't what you want
Small nit: I object to using the param name kind as it is a type variable, not a kind variable. It happens to be fillable by a type of any kind (like Maybe :: Type -> Type or Int :: Type), but not a kind (like Type -> Type or Type) per se.
Small nit: I object to using the param name kind as it is a type variable, not a kind variable. It happens to be fillable by a type of any kind (like Maybe :: Type -> Type or Int :: Type), but not a kind (like Type -> Type or Type) per se.
A good point, I felt it read a little odd too. Thanks for the suggestion. :)
If anyone has time/expertise to review a tutorial on phantom types I'm working on, it would be greatly appreciated:
Not sure if the example is going to work well in this case - you could just as well supply some opaque GADT that actually makes use of the type and user wouldn't be able to tell a difference
Maybe what you can show is one can easily "repack" a credential into one with different type without changing it's content:
BTW, if I remember right, record updates can be type-changing - so
creds { token = "special-token" }
would actually have this repacking behaviour, which probably isn't what you wantThanks a bunch! That helps a lot.
No problem, otherwise it's a nice resource :smile:
you can also add this:
if you want to be expressive with the type param (and close it)
Small nit: I object to using the param name
kind
as it is a type variable, not a kind variable. It happens to be fillable by a type of any kind (likeMaybe :: Type -> Type
orInt :: Type
), but not a kind (likeType -> Type
orType
) per se.Gabriel Lebec said:
A good point, I felt it read a little odd too. Thanks for the suggestion. :)