Haskell Refactoring Advice - haskell

I am looking for some refactoring / Best practice advice on the following code. I would like to try to avoid the extensions while maintaining separate modules for different "versions" that are mutually exclusive. My current solution is to use a class and use existential quantification to create a common type for each of the CountrySets.
This is an easy thing for me to accomplish if I was using OOP, but I can't seam to think "functional" yet.
Thanks for your time.
Province.hs
{-# LANGUAGE ExistentialQuantification, RankNTypes #-}
module Province where
class Country a where
--some stuff that is not important
data Power =
forall a. (Show a, Eq a, Country a) => Power a |
Netural |
Water
data Unit = Unit {
power :: forall a. (Show a, Eq a, Country a) => a,
piece :: Piece
data Piece = Tank | Plane
data Province = Province {
power :: Power,
provName :: String
} deriving (Eq)
instance Show Power where
show (Power b) = "Power " ++ show b
show (Netural) = "Netural"
show (Water) = "Water"
instance Eq Power where
(==) a b = Prelude.show a == Prelude.show b
Version1.hs
import Province
data CountrySet1 =
Country11 |
Country12
deriving (Eq, Show)
instance Country CountrySet1 where
provs =
one1:one2:[]
one1 = Province (Power Country11) "Place11"
one2 = Province (Power Country12) "Place12"
Version2.hs
import Province
data CountrySet2 =
Country21 |
Country22
deriving (Eq, Show)
instance Country CountrySet2 where
provs =
two1:two2:[]
two1 = Province (Power Country11) "Place21"
two2 = Province (Power Country12) "Place22"

You don't have to put class constraints in the data types. You can instead parametrize your data type on the a variable so that you can place the constraints on the type-class instances themselves, like so:
-- Note that I added a type variable to "Power"
data Power a = Power a | Neutral | Water
instance (Show a) => Show (Power a) where ...
instance (Eq a) => Eq (Power a) where ...
... or you could do what most people do and use deriving:
data Power a = Power a | Neutral | Water deriving (Eq, Show)
This generates the exact same instances you wrote (except the Eq one will be much more efficient than what you wrote). No extensions required!
Then if you want a to be a specific type, you just say so!
-- Version1.hs
myValue1 :: Power CountrySet1
myValue1 = ...
-- Version2.hs
myValue2 :: Power CountrySet2
myValue2 = ...
These are then completely compatible and both implementations can coexist alongside each other.

Related

How to have multiple types under a generic "super type" in Haskell?

[Never mind this question - In light of the feedback, I realize it is poorly phrased and too vague. I do not delete it because the answer could serve others. Thanks!]
I am looking for a type structure that allows me to put two similar ordered data types under one generic type.
For example, let's assume I have the following ordered types:
data Type1 = Type1Level1 | Type1Level2 | Type1Level3 | Type1Level4 deriving (Eq, Ord, Show, Read, Bounded, Enum)
data Type2 = Type2Level1 | Type2Level2 | Type2Level3 deriving (Eq, Ord, Show, Read, Bounded, Enum)
In the spirit, I would like a generic overarching type like:
data GenType = Type1 | Type2
But of course that won't work.
I happen to have entities that can be related to one of those types:
data Entity = Entity {
entityId :: String,
entityType :: -- here I want one of the TypeXLevelY (anything "under GenType")
} deriving (Eq, Ord, Read, Show)
But I'd also like those entities to have specific fields depending if they are of type1 or type2 (does this mean I have to make 2 different "entity" data types?). For example, an entity of type1 should have a field "input" but not the entity of type2.
Also, type1 and type2 should be able to share ordering and several functions in common, such that something like this would make sense:
testSuccType :: GenType -> GenType -> Bool
testSuccType type1 type2 = (type2 == succ type1)
It looks like I'm needing a class here but (1) I'm not sure that's the case and (2) when I tried to use qualified type for Entity, it didn't like it (apparently this kind of qualification is not supported anymore):
data GenType a => Entity = Entity {
entityId :: String,
entityType :: a
} deriving (Eq, Ord, Read, Show)
I hope this is clear enough.
Many thanks in advance for your help.
I guess I don't fully understand the question, but perhaps I can give some hints anyway.
Probably the data type definition you are looking for is:
data GenType = GT1 Type1 | GT2 Type2
Or, somewhat idiomatically, you could pun the constructor names with the type of their (only) field:
data GenType = Type1 Type1 | Type2 Type2
Then you can implement your shared function like this:
getNextLevelGenType (GT1 t1) = GT1 (getNextLevelType1 t1)
getNextLevelGenType (GT2 t2) = GT2 (getNextLevelType2 t2)
-- OR
getNextLevelGenType (Type1 t1) = Type1 (getNextLevelType1 t1)
getNextLevelGenType (Type2 t2) = Type2 (getNextLevelType2 t2)
Sometimes it makes sense to share method names via a type class, but whether this makes sense depends a lot on details not shared in the question. If it did make sense, it might look like this:
class Leveled a where getNextLevel :: a -> a
instance Leveled Type1 where getNextLevel = succ
instance Leveled Type2 where getNextLevel = succ
instance Leveled GenType where
getNextLevel (Type1 t1) = Type1 (getNextLevel t1)
getNextLevel (Type2 t2) = Type2 (getNextLevel t2)
If you go that route, there is a generic, parameterized sum type that may be worth considering as an alternative to GenType, named Either. The main reason to avoid considering it is if GenType's constructor names can serve as human-readable documentation of their meaning. (The examples above, where the names indicate the field type and nothing more, are not good human-readable documentation.) A secondary reason would be if you actually want a sum of three or more types, since nested Eithers get unwieldy.
instance (Leveled a, Leveled b) => Leveled (Either a b) where
getNextLevel (Left a) = Left (getNextLevel a)
getNextLevel (Right b) = Right (getNextLevel b)
-- OR, with Data.Bifunctor imported,
getNextLevel = bimap getNextLevel getNextLevel

How to create a lattice-type data structure in Haskell?

I am trying to build a lattice-type of FCA-type of data structure in Haskell where I could check if two entities have a join or not. Actually, I'm not even sure that the lattice is the right structure as it might be a bit "too much".
Here is the context. In a COBOL program analyzer, I have an ontology of data set names, files, records and fields. A data set name can have multiple file names depending on the program, a file can have multiple records and a record can have multiple fields. I'd like this hierarchy to be reflected in the Haskell data structure. But I'd like also to be able to have a relation inherited for file1 and file2 such that I can check if file1 and file2 belong to the same data set name. Actually, this relation could almost be that of "==". But it could simply be that they do have a join in dsn0, for instance.
I have other ontologies in that context that would benefit from a lattice or FCA data structure. For example, I have programs that belongs to job steps and job steps that belong to jobs. If I could easily figure out if two programs belong to the same job, that would be great. Here also, it seems like a "join" operator. Getting the extension (code) of a certain entity would be useful too.
I'm still a bit new to Haskell. I tried to look at the Lattice library but I'm not sure where to go from there, concretely. Any idea of how to get started? A small example of a lattice in Haskell would be very helpful. Thank you very much for your help (and patience).
UPDATE:
The Lattice might not be the best formalism for this as mentioned in the comments. I realize I might just have to use a regular class type of data structure along those lines:
data DSN = DSN {
programFiles :: [ProgramFile]
name :: String
ddn :: DDN
}
data ProgramFile = ProgramFile {
records :: [Record]
name :: String
}
data Record = Record {
fields :: [Field]
name :: String
}
data Field = Field {
name :: String
order :: Int
}
I guess my initial intention behind using a tree/lattice/FCA type of structure is to take full advantage of the functor potential in Haskell which should lead to interesting lattice operations including geting all the extension of a concept, checking that two concepts belong to the same higher-level concept, checking equality '==' of two files through their DSN,...
Maybe a non-binary tree structure would be better? Is it easy to do that in Haskell?
I recommend making an abstract data type to represent one-to-many relationships. It might look something like this:
module OneToMany (OMRel, empty, insert, delete, source, targets) where
import Data.Map (Map)
import Data.Set (Set)
import qualified Data.Map.Strict as M
import qualified Data.Set as S
data OMRel a b = OMRel
{ oneToMany :: Map a (Set b)
, manyToOne :: Map b a
} deriving (Eq, Ord, Read, Show)
empty :: OMRel a b
empty = OMRel M.empty M.empty
insert :: (Ord a, Ord b) => a -> b -> OMRel a b -> OMRel a b
insert a b (OMRel otm mto) = OMRel
{ oneToMany = M.insertWith S.union a (S.singleton b) $
case M.lookup b mto of
Just oldA -> M.adjust (S.delete b) oldA otm
Nothing -> otm
, manyToOne = M.insert b a mto
}
delete :: (Ord a, Ord b) => a -> b -> OMRel a b -> OMRel a b
delete a b (OMRel otm mto) = OMRel (M.adjust (S.delete b) a otm) (M.delete b mto)
source :: Ord b => b -> OMRel a b -> Maybe a
source b = M.lookup b . manyToOne
targets :: Ord a => a -> OMRel a b -> Set b
targets a = M.findWithDefault S.empty a . oneToMany
(Of course you can flesh out the API with more efficient bulk operations like merging, bulk insert, sequential composition, etc. But this is sort of the minimal construct/consume API that gets you where you need to go.)
Then you need a couple data types to represent your various ontological entries:
newtype Dataset = Dataset { dataset :: String }
newtype Record = Record { record :: String }
newtype Field = Field { order :: Int }
From there you can use values with types like OMRel Dataset FilePath to represent the fact that datasets "contain" files. For querying containment equality, you can write this once and for all via the OMRel API above:
sameSource :: (Eq a, Ord b) => OMRel a b -> b -> b -> Bool
sameSource rel b b' = source b rel == source b' rel
(You may need an extra clause if two missing targets should be considered inequal.) Then, e.g., this can be specialized to
sameSource :: OMRel Dataset FilePath -> FilePath -> FilePath -> Bool
and friends.
You will not be able to make a Functor (or Bifunctor) instance for OMRel because of the Ord constraints. It's not clear to me that fmap/bimap make a TON of sense for this particular data structure, though. For example, if we have associations x :: a <-> y :: b and x' :: a <-> y' :: b, and f b = f b', then should fmap f associate f b with a or a'? If they do turn out to have a sensible interpretation and be useful, you could either pursue the constrained-monads approach or simply offer a function named bimap from the OneToMany module with the appropriate type, but without making it an instance method.

Haskell's algebraic data types: "pseudo-extend"

I am learning about Algebraic DTs in haskell. What I would like to do is create a new ADT that kind of "extends" an existing one. I cannot find how to express what I would like, can someone sugest an alternative pattern or sugest a solution. I want them to be distinct types, but copying and pasting just seams like a silly solution. The code below best describes what I am seeking.
data Power =
Abkhazia |
-- A whole bunch of World powers and semi-powers
Transnistria
deriving (Eq, Show)
data Country =
--Everything in Power |
Netural |
Water
deriving (Eq, Show)
Edit: I think It need a little clarification... I want to be able to do this (in ghci)
let a = Abkhazia :: Country
and not
let a = Power Abkhazia :: Country
You need to represent them as a tree:
data Power
= Abkhazia
| Transnistria
deriving (Eq, Show)
data Country
= Powers Power -- holds values of type `Power`
| Netural -- extended with other values.
| Water
deriving (Eq, Show)
Edit: your extension to the question makes this a bit simpler: both the Country and Power types share some common behavior as "countries". This suggests you use the open, extensible type class feature of Haskell to given common behaviors to the data type. E.g.
data Power = Abkhazia | Transistria
data Countries = Neutral | Water
then, a type class for things both Power and Countries share:
class Countrylike a where
landarea :: a -> Int -- and other things country-like entities share
instance Countrylike Power where
landarea Abkhazia = 10
landarea Transistria = 20
instance Countrylike Countries where
landarea Neutral = 50
landarea Water = 0
then you can use landarea cleanly on either powers or countries. And you can extend it to new types in the future by adding more instances.
{-# LANGUAGE GADTs, StandaloneDeriving #-}
data POWER
data COUNTRY
data CountryLike a where
Abkhazia :: CountryLike a
Transnistria :: CountryLike a
Netural :: CountryLike COUNTRY
Water :: CountryLike COUNTRY
deriving instance Show (CountryLike a)
deriving instance Eq (CountryLike a)
type Power = CountryLike POWER
type Country = CountryLike COUNTRY
foo :: Power
foo = Abkhazia
bar :: Country
bar = Abkhazia
baz :: Country
baz = Netural
Edit: An alternative would be type Power = forall a. CountryLike a (Advantage: Makes Power a subtype of Country. Disadvantage: This would make e.g. Power -> Int a higher-rank type, which tend to be annoying (type inference etc.))

Subtypes for natural language types

I'm a linguist working on the formal syntax/semantics of Natural Languages. I've started
using Haskell quite recently and very soon I realized that I needed to add subtyping. For example given the types Human
and Animal,
I would like to have Human as a subtype of Animal. I found that this is possible using a coerce function where the instances are declared by the user, but I do not know how to define coerce in the instances I'm interested in. So basically I do not know what to add after 'coerce =' to make it work'. Here is the code up to that point:
{-# OPTIONS
-XMultiParamTypeClasses
-XFlexibleInstances
-XFunctionalDependencies
-XRankNTypes
-XTypeSynonymInstances
-XTypeOperators
#-}
module Model where
import Data.List
data Animal = A|B deriving (Eq,Show,Bounded,Enum)
data Man = C|D|E|K deriving (Eq,Show,Bounded,Enum)
class Subtype a b where
coerce :: a->b
instance Subtype Man Animal where
coerce=
animal:: [Animal]
animal = [minBound..maxBound]
man:: [Man]
man = [minBound..maxBound]
Thanks in advance
Just ignore the Subtype class for a second and examine the type of the coerce function you are writing. If the a is a Man and the b is an Animal, then the type of the coerce function you are writing should be:
coerce :: Man -> Animal
This means that all you have to do is write a sensible function that converts each one of your Man constructors (i.e. C | D | E | K) to a corresponding Animal constructor (i.e. A | B). That's what it means to subtype, where you define some function that maps the "sub" type onto the original type.
Of course, you can imagine that because you have four constructors for your Man type and only two constructors for your Animal type then you will end up with more than one Man constructor mapping to the same Animal constructor. There's nothing wrong with that and it just means that the coerce function is not reversible. I can't comment more on that without knowing exactly what those constructors were meant to represent.
The more general answer to your question is that there is no way to automatically know which constructors in Man should map to which constructors in Animal. That's why you have to write the coerce function to tell it what the relationship between men and animals is.
Note also that there is nothing special about the 'Subtype' class and 'coerce' function. You can just skip them and write an 'manToAnimal' function. After all there is no built-in language or compiler support for sub-typing and Subtype is just another class that some random guy came up with (and frankly, subtyping is not really idiomatic Haskell, but you didn't really ask about that). All that defining the class instance does is allow you to overload the function coerce to work on the Man type.
I hope that helps.
What level of abstraction are you working where you "need to add subtyping"?
Are you trying to create world model for your program encoded by Haskell types? (I can see this if your types are actually Animal, Dog, etc.)
Are you trying to create more general software, and you think subtypes would be a good design?
Or are you just learning haskell and playing around with things.
If (1), I think that will not work out for you so well. Haskell does not have very good reflective abilities -- i.e. ability to weave type logic into runtime logic. Your model would end up pretty deeply entangled with the implementation. I would suggest creaing a "world model" (set of) types, as opposed to a set of types corresponding to a specific world model. I.e., answer this question for Haskell: what is a world model?
If (2), think again :-). Subtyping is part of a design tradition in which Haskell does not participate. There are other ways to design your program, and they will end up playing nicer with the functional mindset then subtyping would have. It takes times to develop your functional design sense, so be patient with it. Just remember: keep it simple, stupid. Use data types and functions over them (but remember to use higher-order functions to generalize and share code). If you are reaching for advanced features (even typeclasses are fairly advanced in the sense I mean), you are probably doing it wrong.
If (3), see Doug's answer, and play with stuff. There are lots of ways to fake it, and they all kind of suck eventually.
I don't know much about Natural Languages so my suggestion may be missing the point, but this may be what you are looking for.
{-# OPTIONS
-XMultiParamTypeClasses
-XFlexibleContexts
#-}
module Main where
data Animal = Mammal | Reptile deriving (Eq, Show)
data Dog = Terrier | Hound deriving (Eq, Show)
data Snake = Cobra | Rattle deriving (Eq, Show)
class Subtype a b where
coerce :: a -> b
instance Subtype Animal Animal where
coerce = id
instance Subtype Dog Animal where
coerce _ = Mammal
instance Subtype Snake Animal where
coerce _ = Reptile
isWarmBlooded :: (Subtype a Animal) => a -> Bool
isWarmBlooded = (Mammal == ) . coerce
main = do
print $ isWarmBlooded Hound
print $ isWarmBlooded Cobra
print $ isWarmBlooded Mammal
Gives you:
True
False
True
Is that kind of what you are shooting for? Haskell doesn't have subtyping built-in, but this might do as a work-around. Admittedly, there are probably better ways to do this.
Note: This answer is not intended to point out the best, correct or idomatic way to solve the problem at hand. It is intended to answer the question which was "what to add after 'coerce=' to make it work."
You can't write the coerce function you're looking for — at least, not sensibly. There aren't any values in Animal that correspond with the values in Man, so you can't write a definition for coerce.
Haskell doesn't have subtyping as an explicit design decision, for various reasons (it allows type inference to work better, and allowing subtyping vastly complicates the language's type system). Instead, you should express relationships like this using aggregation:
data Animal = A | B | AnimalMan Man deriving (Eq, Show, Bounded, Enum)
data Man = C | D | E | K deriving (Eq, Show, Bounded, Enum)
AnimalMan now has the type Man -> Animal, exactly as you wanted coerce to have.
If I understood you correctly, it is quite possible. We will use type classes and generalized algebraic data types to implement this functionality.
If you want to be able to do something like this (where animals and humans can be fed but only humans can think):
animals :: [AnyAnimal]
animals = (replicate 5 . AnyAnimal $ SomeAnimal 10) ++ (replicate 5 . AnyAnimal $ SomeHuman 10 10)
humans :: [AnyHuman]
humans = replicate 5 . AnyHuman $ SomeHuman 10 10
animals' :: [AnyAnimal]
animals' = map coerce humans
animals'' :: [AnyAnimal]
animals'' = (map (\(AnyAnimal x) -> AnyAnimal $ feed 50 x) animals) ++
(map (\(AnyAnimal x) -> AnyAnimal $ feed 50 x) animals') ++
(map (\(AnyHuman x) -> AnyAnimal $ feed 50 x) humans)
humans' :: [AnyHuman]
humans' = (map (\(AnyHuman x) -> AnyHuman . think 100 $ feed 50 x) humans)
Then it's possible, for example:
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
-- | The show is there only to make things easier
class (Show a) => IsAnimal a where
feed :: Int -> a -> a
-- other interface defining functions
class (IsAnimal a) => IsHuman a where
think :: Int -> a -> a
-- other interface defining functions
class Subtype a b where
coerce :: a -> b
data AnyAnimal where
AnyAnimal :: (IsAnimal a) => a -> AnyAnimal
instance Show AnyAnimal where
show (AnyAnimal x) = "AnyAnimal " ++ show x
data AnyHuman where
AnyHuman :: (IsHuman a) => a -> AnyHuman
instance Show AnyHuman where
show (AnyHuman x) = "AnyHuman " ++ show x
data SomeAnimal = SomeAnimal Int deriving Show
instance IsAnimal SomeAnimal where
feed = flip const
data SomeHuman = SomeHuman Int Int deriving Show
instance IsAnimal SomeHuman where
feed = flip const
instance IsHuman SomeHuman where
think = flip const
instance Subtype AnyHuman AnyAnimal where
coerce (AnyHuman x) = AnyAnimal x
animals :: [AnyAnimal]
animals = (replicate 5 . AnyAnimal $ SomeAnimal 10) ++ (replicate 5 . AnyAnimal $ SomeHuman 10 10)
humans :: [AnyHuman]
humans = replicate 5 . AnyHuman $ SomeHuman 10 10
animals' :: [AnyAnimal]
animals' = map coerce humans
Few comments:
You can make AnyAnimal and AnyHuman instances of their respective classes for convenience (atm. you have to unpack them first and pack them afterwards).
We could have single GADT AnyAnimal like this (both approaches have their use I would guess):
data AnyAnimal where
AnyAnimal :: (IsAnimal a) => a -> AnyAnimal
AnyHuman :: (IsHuman a) => a -> AnyAnimal
instance Show AnyHuman where
show (AnyHuman x) = "AnyHuman " ++ show x
show (AnyAnimal x) = "AnyAnimal " ++ show x
instance Subtype AnyAnimal AnyAnimal where
coerce (AnyHuman x) = AnyAnimal x
coerce (AnyAnimal x) = AnyAnimal x
It's rather advanced, but have a look at Edward Kmett's work on using the new Constraint kinds for this kind of functionality.

Haskell polymorphic functions with records and class types

this post is the following of this one.
I'm realizing a simple battle system as toy project, the typical system you can find in games like Final Fantasy et simila. I've solved the notorious "Namespace Pollution" problem with a class type + custom instances. For example:
type HitPoints = Integer
type ManaPoints = Integer
data Status = Sleep | Poison | .. --Omitted
data Element = Fire | ... --Omitted
class Targetable a where
name :: a -> String
level :: a -> Int
hp :: a -> HitPoints
mp :: a -> ManaPoints
status :: a -> Maybe [Status]
data Monster = Monster{monsterName :: String,
monsterLevel :: Int,
monsterHp :: HitPoints,
monsterMp :: ManaPoints,
monsterElemType :: Maybe Element,
monsterStatus :: Maybe [Status]} deriving (Eq, Read)
instance Targetable Monster where
name = monsterName
level = monsterLevel
hp = monsterHp
mp = monsterMp
status = monsterStatus
data Player = Player{playerName :: String,
playerLevel :: Int,
playerHp :: HitPoints,
playerMp :: ManaPoints,
playerStatus :: Maybe [Status]} deriving (Show, Read)
instance Targetable Player where
name = playerName
level = playerLevel
hp = playerHp
mp = playerMp
status = playerStatus
Now the problem: I have a spell type, and a spell can deal damage or inflict a status (like Poison, Sleep, Confusion, etc):
--Essentially the result of a spell cast
data SpellEffect = Damage HitPoints ManaPoints
| Inflict [Status] deriving (Show)
--Essentially a magic
data Spell = Spell{spellName :: String,
spellCost :: Integer,
spellElem :: Maybe Element,
spellEffect :: SpellEffect} deriving (Show)
--For example
fire = Spell "Fire" 20 (Just Fire) (Damage 100 0)
frogSong = Spell "Frog Song" 30 Nothing (Inflict [Frog, Sleep])
As suggested in the linked topic, I've created a generic "cast" function like this:
--cast function
cast :: (Targetable t) => Spell -> t -> t
cast s t =
case spellEffect s of
Damage hp mana -> t
Inflict statList -> t
As you can see the return type is t, here showed just for consistency. I want be able to return a new targetable (i.e. a Monster or a Player) with some field value altered (for example a new Monster with less hp, or with a new status). The problem is that i can't just to the following:
--cast function
cast :: (Targetable t) => Spell -> t -> t
cast s t =
case spellEffect s of
Damage hp' mana' -> t {hp = hp', mana = mana'}
Inflict statList -> t {status = statList}
because hp, mana and status "are not valid record selector". The problem is that I don't know a priori if t will be a monster or a player, and I don't want to specify "monsterHp" or "playerHp", I want to write a pretty generic function.
I know that Haskell Records are clumsy and not much extensibile...
Any idea?
Bye and happy coding,
Alfredo
Personally, I think hammar is on the right track with pointing out the similarities between Player and Monster. I agree you don't want to make them the same, but consider this: Take the type class you have here...
class Targetable a where
name :: a -> String
level :: a -> Int
hp :: a -> HitPoints
mp :: a -> ManaPoints
status :: a -> Maybe [Status]
...and replace it with a data type:
data Targetable = Targetable { name :: String
, level :: Int
, hp :: HitPoints
, mp :: ManaPoints
, status :: Maybe [Status]
} deriving (Eq, Read, Show)
Then factor out the common fields from Player and Monster:
data Monster = Monster { monsterTarget :: Targetable
, monsterElemType :: Maybe Element,
} deriving (Eq, Read, Show)
data Player = Player { playerTarget :: Targetable } deriving (Eq, Read, Show)
Depending on what you do with these, it might make more sense to turn it inside-out instead:
data Targetable a = Targetable { target :: a
, name :: String
-- &c...
}
...and then have Targetable Player and Targetable Monster. The advantage here is that any functions that work with either can take things of type Targetable a--just like functions that would have taken any instance of the Targetable class.
Not only is this approach nearly identical to what you have already, it's also a lot less code, and keeps the types simpler (by not having class constraints everywhere). In fact, the Targetable type above is roughly what GHC creates behind the scenes for the type class.
The biggest downside to this approach is that it makes accessing fields clumsier--either way, some things end up being two layers deep, and extending this approach to more complicated types can nest them deeper still. A lot of what makes this awkward is the fact that field accessors aren't "first class" in the language--you can't pass them around like functions, abstract over them, or anything like that. The most popular solution is to use "lenses", which another answer mentioned already. I've typically used the fclabels package for this, so that's my recommendation.
The factored-out types I suggest, combined with strategic use of lenses, should give you something that's simpler to use than the type class approach, and doesn't pollute the namespace the way having lots of record types does.
I can suggest three possible solutions.
1) Your types are very OO-like, but Haskell can also express "sum" types with parameters:
data Unit = UMon Monster | UPlay Player
cast :: Spell -> Unit -> Unit
cast s t =
case spellEffect s of
Damage hp' mana' -> case t of
UMon m -> UMon (m { monsterHp = monsterHp m - hp', monsterMana = undefined})
UPluy p -> UPlay (p { playerHp = playerHp p - hp'})
Inflict statList -> undefined
Thing that are similar in OO-design often become "sum" types with parameters in Haskell.
2) You can do what Carston suggests and add all your methods to type classes.
3) You can change your read-only methods in Targetable to be "lenses" that expose both getting and setting. See the stack overflow discussion. If your type class returned lenses then it would make your spell damage possible to apply.
Why don't you just include functions like
InflicteDamage :: a -> Int -> a
AddStatus :: a -> Status -> a
into your type-class?

Resources