2011-07-15 13 views
22

Niedawno zacząłem mały hobby project, w którym staram się wprowadzić sztuczną grę w karty Skat (dla 3 graczy). Aby było to możliwe, aby mieć różne rodzaje graczy (jak AI, sieci i lokalnych) gra razem, zaprojektował interface użyciu typeclass Player:Haskell - Jak uniknąć wpisywania tego samego kontekstu w kółko?

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

używam StateT omotać tych trzech graczy:

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

Ale teraz, muszę napisać wielki stos w kontekście każdego podpisu typu:

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

Jak można uniknąć pisząc ten duży kontekst raz za razem?

+0

Czy masz swój kod online w dowolnym miejscu? Możesz uzyskać więcej przydatnych porad z większym kontekstem. Kod wydaje mi się czymś, co jest bardziej ogólne niż to, czego naprawdę potrzebujesz, i można go uprościć, specjalizując się tylko w tym, co ma sens, ale nie mogę być tego pewien bez większego kontekstu. –

+0

@camccann Opublikowano na [GitHub] (https://github.com/fuzxxl/Unter). Kod na github jest nieco inny, ponieważ obecnie robię wiele refaktoryzacji, aby moje życie było łatwiejsze. – fuz

+2

@FUXxxl - losowy komentarz - powinieneś określać rozszerzenia na podstawie modułu – alternative

Odpowiedz

11
  • Jedyną rzeczą, jaką można obserwować z klasy gracza jest funkcją typu

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

    Stąd można wyeliminować całkowicie klasę i użyć zwykłej typ danych

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

    jest to w zasadzie my previous answer.

  • Alternatywnym rozwiązaniem jest przeniesienie kontekstu do typu danych za pomocą GADT.

    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 
    

    Innymi słowy, ograniczenia stają się częścią typu danych.

  • Najlepszym rozwiązaniem jest, moim zdaniem, złomowanie całej rzeczy i przeprojektowanie jej wzdłuż linii TicTacToe example z mojego operational package. Ten projekt pozwala ci napisać każdego gracza (człowieka, AI, powtórka, ...) w specjalistycznej monadzie, a następnie wstrzyknąć wszystko do zwykłego tłumacza.

+2

B .. ale .. to w zasadzie to, co powiedziałem. – yairchu

+1

Podoba mi się to podejście. Tak więc sposobem na obsłużenie stanu dla graczy jest użycie bardziej ogólnej funkcji, takiej jak 'playerMessageAI :: AiState -> odpowiedź wiadomości -> m (albo odpowiedź błędu, odtwarzacz) i użycie częściowej aplikacji? Okej, myślę, że to dostałem. PS: Brakujesz 'm' po' danych Player', ale nie wiem, czy to jest celowe. (Może również "nowy typ" działa?) – fuz

+0

@ yairchu Przegłosowałem obie twoje odpowiedzi, ponieważ Heinrich Apfelmus daje dodatkowe wyjaśnienie. – fuz

6

Aktualizacja: Kiedy próbowałem wykonawczych dealCards zdałem sobie sprawę, że moje rozwiązanie zmniejsza typu bezpieczeństwa przez co gracze wymienne. W ten sposób możesz łatwo użyć jednego gracza zamiast drugiego, co może być niepożądane.


Jeśli nie przeszkadza ExistentialQuantification, myślę, że może (i powinien?) Być tutaj. Przecież funkcja dealCards nie powinna się przejmować ani wiedzieć o tym, czy są one a, b i c, prawda?

{-# 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 

myślę, że powinno być możliwe do wyeliminowania Monad wymuszenie w podobny sposób.

Właściwie w takich przypadkach wydaje mi się, że zajęcia nadrzędne są nadużywane. Może to początkujący Haskell mówienia mnie, ale ja piszę to zamiast:

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

Uzgodnione. W moim i [Luke Palmer's opinion] (http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/), kwantyfikacja egzystencjalna jest zawsze znakiem, że zrobi to zwykły typ danych. –

+0

Jestem również skłonny powiedzieć, że jeśli klasa typu może być reprezentowana bezpośrednio przez zapis członków instancji, ta reprezentacja jest prawie zawsze lepsza niż błądzenie z irytującymi egzystencjalnymi. Zazwyczaj cytuję tutaj blog Luquiego, ale ... wygląda na to, że już zajął się tym @Heinrich, heh. –

+2

Należy zauważyć, że każdy typ argumentu wyeliminowany z typu funkcji poprzez częściowe zastosowanie staje się w sensie pojęciowym egzystencjalnym; istnieje, nawet jeśli nie możesz nic z tym zrobić. Bezpośrednie stosowanie typów egzystencjalnych polega głównie na reifikacji tej koncepcji, co rzadko jest konieczne. –

2

Oczywiście lepszą odpowiedzią jest posiadanie projektu, który nie wymaga wszystkich tych parametrów. Jednakże, jeśli naprawdę nie można się ich pozbyć, i odpowiedzieć na pytanie, jakie, oto sztuczka Grałem:

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

Teraz pisanie „(sygnał y) => ...” będzie implikuj wszystkie inne typologie i nie dopuść do szczegółów implementacji takich jak Storable od wchodzenia w każdy interfejs API. Musisz jednak zadeklarować instancje dla Signal. Jest to łatwe, ponieważ nie ma żadnych metod, ale prawdopodobnie jest najbardziej odpowiedni, gdy masz kilka instancji, ale wiele funkcji.

Powiązane problemy