user1198582
user1198582

Reputation:

Challenged by types

The problem:

Currently I have a type WorkConfig, which looks like this

data WorkConfig = PhaseZero_wc BuildConfig
                | PhaseOne_wc BuildConfig Filename (Maybe XMLFilepath)
                | PhaseTwo_wc String
                | SoulSucker_wc String
                | ImageInjector_wc String
                | ESX_wc String
                | XVA_wc String
                | VNX_wc String
                | HyperV_wc String
                | Finish_wc String
                    deriving Show

(I'm using String from PhaseTwo_wc on as a placeholder for what will actually be used)

I have a function updateConfig that takes a WorkConfig as one of it's parameters.

The problem is that I want to be able to enforce which constructor is used. For example in the function phaseOne I want to be able to guarantee that when updateConfig is invoked, only the PhaseTwo_wc constructor can be used.

In order to use a type class for this enforcement, I would have to make separate data constructors, for example:

 data PhaseOne_wc = PhaseOne_wc  BuildConfig Filename (Maybe XMLFilepath)

If I go this route, I have another problem to solve. I have other data types that have WorkConfig as a value, what would I do to address this? For example,

type ConfigTracker = TMVar (Map CurrentPhase WorkConfig)

How can I use the type system for the enforcement I would like, while keeping in mind what I mentioned above?

ConfigTracker would have to be able to know which data type I wanted.

* Clarification: I'm looking to restrict which WorkConfig that updateConfig may take as a parameter.

Upvotes: 1

Views: 140

Answers (3)

Owen
Owen

Reputation: 39366

Sorry, this is more of an extended comment than an answer. I'm a little confused. It sounds like you have some functions like

phaseOne = ...
           ... (updateConfig ...) ...

phaseTwo = ...
           ... (updateConfig ...) ...

and you're trying to make sure that eg, inside the definition of phaseOne, this never appears:

phaseOne = ...
           ... (updateConfig $ PhaseTwo_wc ...) ...

But now I ask you: is updateConfig a pure (non-monadic) function? Because if it is, than phaseOne can easily be perfectly correct and still invoke updateConfig with a PhaseTwo_wc; ie it could just throw away the result (and even if it's monadic too):

phaseOne = ...
           ... (updateConfig $ PhaseTwo_wc ...) `seq` ...

In other words, I'm wondering if the constraint you're trying to enforce is really the actual property you are looking for?

But now, if we're thinking of monads, there is a common pattern that what you describe is sort of like: making "special" monads that limit the kind of actions that can be performed; eg

data PhaseOneMonad a = PhaseOnePure a | PhaseOneUpdate BuildConfig Filename (Maybe XMLFilepath) a

instance Monad PhaseOneMonad where ...

Is this maybe what you're getting at? Note also that PhaseOneUpdate doesn't take a WorkConfig; it just takes the constructor parameters it is interested in. This is another common pattern: you can't constrain which constructor is used, but you can just take the arguments directly.

Hm... still not sure though.

Upvotes: 0

Gabriella Gonzalez
Gabriella Gonzalez

Reputation: 35099

Your question was a little vague, so I will answer in the general.

If you have a type of the form:

data MyType a b c d e f g = C1 a b | C2 c | C3 e f g

... and you want some function f that works on all three constructors:

f :: MyType a b c d e f g -> ...

... but you want some function g that works on just the last constructor, then you have two choices.

The first option is to create a second type embedded within C3:

data SecondType e f g = C4 e f g

... and then embed that within the original C3 constructor:

data MyType a b c d e f g = C1 a b | C2 c | C3 (SecondType e f g)

... and make g a function of SecondType:

g :: SecondType e f g -> ...

This only slightly complicates the code for f as you will have to first unpack C3 to access the C4 constructor.

The second solution is that you just make g a function of the values stored in the C3 constructor:

g :: e -> f -> g -> ...

This requires no modification to f or the MyType type.

Upvotes: 1

Chris Kuklewicz
Chris Kuklewicz

Reputation: 8153

To be more concrete and drive the discussion, how close does this GADT & Existential code get to what you want?

{-# LANGUAGE GADTs, KindSignatures, DeriveDataTypeable, ExistentialQuantification, ScopedTypeVariables, StandaloneDeriving #-}
module Main where

import qualified Data.Map as Map
import Data.Typeable
import Data.Maybe

data Phase0 deriving(Typeable)
data Phase1 deriving(Typeable)
data Phase2 deriving(Typeable)

data WC :: * -> * where
  Phase0_ :: Int -> WC Phase0
  Phase1_ :: Bool -> WC Phase1
 deriving (Typeable)
deriving instance Show (WC a)

data Some = forall a. Typeable a => Some (WC a)
deriving instance Show Some

things :: [Some]
things = [ Some (Phase0_ 6) , Some (Phase1_ True) ]

do'phase0 :: WC Phase0 -> WC Phase1
do'phase0 (Phase0_ i) = Phase1_ (even i)

-- Simplify by using TypeRep of the Phase* types as key
type M = Map.Map TypeRep Some

updateConfig :: forall a. Typeable a => WC a -> M -> M
updateConfig wc m = Map.insert key (Some wc) m
  where key = typeOf (undefined :: a)

getConfig :: forall a. Typeable a => M -> Maybe (WC a)
getConfig m = case Map.lookup key m of
                Nothing -> Nothing
                Just (Some wc) -> cast wc
   where key = typeOf (undefined :: a)

-- Specialization of updateConfig restricted to taking Phase0
updateConfig_0 :: WC Phase0 -> M -> M
updateConfig_0 = updateConfig

-- Example of processing from Phase0 to Phase1
process_0_1 :: WC Phase0 -> WC Phase1
process_0_1 (Phase0_ i) = (Phase1_ (even i))

main = do
  print things
  let p0 = Phase0_ 6
      m1 = updateConfig p0 Map.empty
      m2 = updateConfig (process_0_1 p0) m1
  print m2
  print (getConfig m2 :: Maybe (WC Phase0))
  print (getConfig m2 :: Maybe (WC Phase1))
  print (getConfig m2 :: Maybe (WC Phase2))

Upvotes: 0

Related Questions