2012-04-23 12 views
11

Często potrzebuję wykonać funkcję rdzeniową, która jest używana w wielu miejscach w jakiś sposób konfigurowalny - tzn. Może używać algorytmu A lub algorytmu B w zależności od przełącznika wiersza poleceń; lub jeśli jest on w stanie wydrukować bardzo szczegółowe informacje na standardowe wyjście, jeśli jakaś flaga "debugowania" jest ustawiona.Właściwy sposób traktowania globalnych flag w Haskell

Jak należy wdrożyć takie globalne flagi?

Widzę 4 opcje, wszystkie nie są naprawdę dobre.

1) Odczytaj argumenty wiersza poleceń od funkcji - bad, ponieważ wymaga to monady IO, a funkcje obliczeń rdzeniowych są czyste, nie chcę uzyskać IO;

2) Przepuścić parametr od strony głównej/IO do funkcji "liścia", która musi zmienić zachowanie - całkowicie bezużyteczny, ponieważ oznacza to zmianę kilkunastu niepowiązanych funkcji w różnych modułach w celu przekazania tego parametru, oraz Chcę wypróbować takie opcje konfiguracji wiele razy bez zmiany kodu zawijania za każdym razem;

3) Użyj unsafePerformIO, aby uzyskać prawdziwą zmienną globalną - czuje się brzydki i przesadny w przypadku tak prostego problemu;

4) W samym środku funkcji znajduje się kod dla obu opcji i komentarz jednego z nich. Albo funkcje do_stuff_A i do_stuff_B, i zmień, który z nich jest wywoływany w zależności od tego, co mówi funkcja globalna "needDebugInfo = True". Właśnie to robię teraz dla debuginfo, ale nie można go zmienić bez rekompilacji i nie powinien to być najlepszy dostępny sposób ...

Nie potrzebuję globalnej zmienności state - chcę mieć prostą globalną flagę, która jest niezmienna w czasie wykonywania, ale może być w jakiś sposób ustawiona podczas uruchamiania programu. Czy są jakieś opcje?

Odpowiedz

3

Nasz nowy HFlags biblioteka jest dokładnie w tym celu.

Jeśli chciałbyś, aby zobaczyć przykład użycia jak twój przykład spojrzeć na to:

https://github.com/errge/hflags/blob/master/examples/ImportExample.hs

https://github.com/errge/hflags/blob/master/examples/X/B.hs

https://github.com/errge/hflags/blob/master/examples/X/Y_Y/A.hs

No niby parametrów przejścia jest potrzebna pomiędzy modułami i możesz definiować nowe flagi z łatwą składnią. Wykorzystuje wewnętrznie niebezpieczne narzędzie, ale uważamy, że robi to w bezpieczny sposób i nie musisz się tym martwić.

Jest blogu o tych rzeczach w: http://blog.risko.hu/2012/04/ann-hflags-0.html

14

W dzisiejszych czasach wolę używać a Reader monad do struktury stanu tylko do odczytu aplikacji. Środowisko jest inicjalizowane przy uruchomieniu, a następnie dostępne na najwyższym poziomie programu.

Przykład is xmonad:

newtype X a = X (ReaderT XConf (StateT XState IO) a) 
    deriving (Functor, Monad, MonadIO, MonadReader XConf) 

Górne elementy szczebla z przebiegu programu w X zamiast IO; gdzie XConf jest strukturą danych inicjalizowaną przez flagi linii poleceń (i zmienne środowiskowe).

Stan XConf można następnie przekazać jako czyste dane do funkcji, które tego potrzebują. Z nowym typem uzyskujesz także możliwość ponownego użycia całego kodu MonadReader w celu uzyskania dostępu do stanu.

Podejście to zachowuje czystość semantyczną równą 2., ale daje mniej kodu do napisania, ponieważ monada wykonuje instalację wodno-kanalizacyjną.

Myślę, że jest to "prawdziwy" sposób Haskell, aby wykonać stan tylko do odczytu.

-

Podejścia, które używają unsafePerformIO zainicjować stan globalnego również pracować, oczywiście, ale mają tendencję do gryzienia w końcu (na przykład podczas wykonywania program równoczesne lub równoległe). Mają także funny initialization semantics.

9

Możesz użyć monady Reader, aby uzyskać taki sam efekt, jak przekazywanie parametru wszędzie.Styl aplikacyjny może sprawić, że narzut jest dość niski w porównaniu do normalnego kodu funkcjonalnego, ale nadal może być dość niezręczny. Jest to najczęstsze rozwiązanie problemu z konfiguracją, ale nie wydaje mi się to zbyt satysfakcjonujące; w rzeczy samej, bezpośrednie przekazanie parametru dookoła jest często mniej brzydkie.

Alternatywą jest pakiet reflection, który pozwala przekazywać typowe dane konfiguracyjne w ten sposób w kontekście kontekstów typeclass, co oznacza, że ​​żaden z kodów nie musi się zmieniać, aby uzyskać dodatkową wartość, tylko typy. Zasadniczo dodajesz nowy parametr do każdego typu wejścia/wyniku w swoim programie, tak aby wszystko działające w kontekście określonej konfiguracji miało typ odpowiadający tej konfiguracji w swoim typie. Ten typ zatrzymuje przypadkowe miksowanie wartości przy użyciu wielu konfiguracji i daje dostęp do powiązanej konfiguracji w czasie wykonywania.

Pozwala to uniknąć narzutu na pisanie wszystkiego w stylu aplikacyjnym, jednocześnie będąc bezpiecznym i umożliwiając miksowanie wielu konfiguracji. To znacznie prostsze niż się wydaje; tutaj jest an example.

(Full discloure. Pracowałam na opakowaniu refleksji)

2

Inną opcją jest GHC implicit parameters. Dają one mniej bolesną wersję twojej opcji (2): sygnatury typu pośredniego zostają zainfekowane, ale nie musisz zmieniać żadnego pośredniego kodu.

Oto przykład:

{-# LANGUAGE ImplicitParams #-} 
import System.Environment (getArgs)  

-- Put the flags in a record so you can add new flags later 
-- without affecting existing type signatures. 
data Flags = Flags { flag :: Bool } 

-- Leaf functions that read the flags need the implicit argument 
-- constraint '(?flags::Flags)'. This is reasonable. 
leafFunction :: (?flags::Flags) => String 
leafFunction = if flag ?flags then "do_stuff_A" else "do_stuff_B" 

-- Implicit argument constraints are propagated to callers, so 
-- intermediate functions also need the implicit argument 
-- constraint. This is annoying. 
intermediateFunction :: (?flags::Flags) => String 
intermediateFunction = "We are going to " ++ leafFunction 

-- Implicit arguments can be bound at the top level, say after 
-- parsing command line arguments or a configuration file. 
main :: IO() 
main = do 
    -- Read the flag value from the command line. 
    commandLineFlag <- (read . head) `fmap` getArgs 
    -- Bind the implicit argument. 
    let ?flags = Flags { flag = commandLineFlag } 
    -- Subsequent code has access to the bound implicit. 
    print intermediateFunction 

Jeśli uruchomić ten program z argumentu True drukuje We are going to do_stuff_A; z argumentem False drukuje We are going to do_stuff_B.

Myślę, że to podejście jest podobne do the reflection package mentioned in another answer, i myślę, że HFlags mentioned in the accepted answer jest prawdopodobnie lepszym wyborem, ale dodaję tę odpowiedź do kompletności.

Powiązane problemy