Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell - How to avoid typing the same context over and over again?

Tags:

haskell

I recently started a little hobby project, where I try to implement the trick-taking card-game Skat (for 3 players). To make it possible to have different kinds of players (like AI, network and local) playing together, I designed an interface using a typeclass Player:

class Monad m => Player p m | p -> m where
  playerMessage :: Message answer -> p -> m (Either Error answer,p)

I use a StateT to wrap up those three players:

type PST a b c m x = StateT (Players a b c) m x

But now, I have to write a big pile of context in each type signature:

dealCards :: (Player a m, Player b m, Player c m, RandomGen g)
  => g -> PST a b c m (SomeOtherState,g)

How can I avoid writing this big context again and again?

like image 242
fuz Avatar asked Jul 15 '11 11:07

fuz


3 Answers

  • The only thing you can observe from the player class is a function of type

    playerMessage' :: Message answer -> m (Either Error answer, p)
    

    Hence, you can eliminate the class entirely and use an ordinary data type

    data Player m = Player { playerMessage'
                  :: Message answer -> m (Either Error answer, Player m) }
    

    This is basically my previous answer.

  • An alternative solution is to move the context into the data type by using GADTs.

    data PST a b c m x where
        PST :: (Player a m, Player b m, Player c m)
            => StateT (Players a b c) m x -> PST a b c m x
    

    In other words, the constraints become part of the data type.

  • The best solution is, in my opinion, to scrap the whole thing and redesign it along the lines of the TicTacToe example from my operational package. This design allows you to write each player (human, AI, replay,...) in a specialized monad and later inject everything into a common interpreter.

like image 192
Heinrich Apfelmus Avatar answered Nov 15 '22 14:11

Heinrich Apfelmus


Update: When I tried implementing dealCards I have realised that my solution decreases the type-safety by making the players interchangeable. This way you can easily use one player instead of the other which may be undesirable.


If you don't mind using ExistentialQuantification, I think it can (and should?) be used here. After all, the dealCards function should not care or know about a, b and c, right?

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

import Control.Monad.State
import System.Random

type Message answer = answer
type Error = String

class Monad m => Player p m | p -> m where
  playerMessage :: Message answer -> p -> m (Either Error answer,p)

data SomePlayer m = forall p. Player p m => SomePlayer p

data Players m = Players (SomePlayer m) (SomePlayer m) (SomePlayer m)

type PST m x = StateT (Players m) m x

dealCards :: (RandomGen g, Monad m) => g -> PST m x
dealCards = undefined

I think it should be possible to eliminate the Monad constraint in a similar way.

Actually, in cases like this I feel like type classes are being overused. Maybe that's a Haskell novice speaking in me, but I'd write this instead:

data Player m = Player { playerMessage :: Message answer -> m (Either Error answer, Player m) }
like image 6
Rotsor Avatar answered Nov 15 '22 14:11

Rotsor


Clearly the better answer is to have a design that doesn't need all those type parameters in the first place. However, if you really can't get rid of them, and to answer the question as posed, here's a trick I've played:

class (Storable.Storable (X, y), Y y) => Signal y

Now writing '(Signal y) => ...' will imply all the other typeclasses, and prevent implementation details like Storable from getting into every API. However, you have to declare instances for Signal. It's easy because it has no methods, but is probably mostly suitable for when you have few instances but many functions.

like image 2
Evan Laforge Avatar answered Nov 15 '22 15:11

Evan Laforge